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;
}
}