CreateHandler#

com.palmyralabs.palmyra.handlers.CreateHandler

Insert lifecycle. Composes PreProcessor (validation + ACL) and a rollback failure hook.

Request / response — User#

Assuming the User model from QueryHandler → Worked example:

Request#

POST /api/v1/admin/user
Content-Type: application/json
{
  "loginName": "ada@example.com",
  "firstName": "Ada",
  "lastName":  "Lovelace",
  "status":    "ACTIVE",
  "tenantId":  3
}

Fields flagged DropMode.INCOMING (createdAt, createdBy) are ignored if the client supplies them — the server is the authoritative writer.

Response#

The newly-inserted row, with server-populated fields filled in:

{
  "id": 102,
  "loginName": "ada@example.com",
  "firstName": "Ada",
  "lastName":  "Lovelace",
  "status":    "ACTIVE",
  "tenantId":  3,
  "createdAt": "2026-04-01T09:12:00Z",
  "createdBy": "admin@example.com"
}

Methods#

Method Signature
validate void validate(Tuple tuple, HandlerContext ctx)
preProcessRaw Tuple preProcessRaw(Tuple tuple, HandlerContext ctx)
preProcess Tuple preProcess(Tuple tuple, HandlerContext ctx)
aclCheck int aclCheck(Tuple tuple, HandlerContext ctx)
getAcl int getAcl(Tuple tuple, HandlerContext ctx)
preCreate Tuple preCreate(Tuple tuple, HandlerContext ctx)
onCreate Tuple onCreate(Tuple tuple, HandlerContext ctx)
postCreate Tuple postCreate(Tuple tuple, HandlerContext ctx)
rollback default Tuple rollback(Tuple tuple, HandlerContext ctx)

Example#

@Component
@CrudMapping(value = "/v1/admin/user", type = User.class)
public class UserCreateHandler implements CreateHandler {

    @Autowired
    private UserProvider userProvider;
    @Autowired
    private AuditService audit;
    @Autowired
    private OutboxService outbox;

    // PreProcessor hooks -----------------------------------------------------

    @Override
    public void validate(Tuple tuple, HandlerContext ctx) {
        if (tuple.get("loginName") == null) {
            throw new ValidationException("loginName is required");
        }
    }

    @Override
    public int aclCheck(Tuple tuple, HandlerContext ctx) {
        return userProvider.hasRole("USER_CREATE") ? 0 : -1;
    }

    @Override
    public Tuple preProcessRaw(Tuple tuple, HandlerContext ctx) {
        // normalize raw input before validation
        String login = (String) tuple.get("loginName");
        if (login != null) tuple.set("loginName", login.trim().toLowerCase());
        return tuple;
    }

    @Override
    public Tuple preProcess(Tuple tuple, HandlerContext ctx) {
        tuple.set("status", "PENDING");
        return tuple;
    }

    // Lifecycle hooks --------------------------------------------------------

    @Override
    public Tuple preCreate(Tuple tuple, HandlerContext ctx) {
        tuple.set("createdBy", userProvider.getUserId());
        tuple.set("createdAt", Instant.now());
        return tuple;
    }

    @Override
    public Tuple onCreate(Tuple tuple, HandlerContext ctx) {
        // hash password in-band with the insert
        String raw = (String) tuple.get("password");
        if (raw != null) tuple.set("passwordHash", BCrypt.hash(raw));
        tuple.remove("password");
        return tuple;
    }

    @Override
    public Tuple postCreate(Tuple tuple, HandlerContext ctx) {
        outbox.publish("user.created", tuple);
        audit.record(ctx, "USER_CREATE", tuple.get("id"));
        return tuple;
    }

    @Override
    public Tuple rollback(Tuple tuple, HandlerContext ctx) {
        // undo any out-of-transaction side effects
        outbox.discardPending(tuple.get("id"));
        return tuple;
    }
}