CodamAIDocs
Topicdone

A write across all layers

A form is saved: from the click through BFF, token check, permissions, validation, hooks, saving, audit and commit to the response.

Variants
creating (POST /create)changing (PUT and PATCH)deleting (DELETE)refusal at every stagesuccess: commit before the responsefailure: rollback of the whole request

What this is about

On a read, only what comes back is decided. On a write, what ends up in the database is decided — and for that, noticeably more stations run, in a fixed order. This page plays a write through from the “Save” button to the commit and says at every point who is checking and what happens on a refusal.

The read path sits next to it on From login to the data. The first half is the same; this page starts where they part.

The whole path in one picture

sequenceDiagram
    autonumber
    participant U as User
    participant F as BFF
    participant FK as Filter chain (CIAS)
    participant R as REST layer (CDMS)
    participant S as System layer (CDMS)
    participant H as Hook
    participant DB as Database
    U->>F: clicks "Save"
    F->>F: decrypt the cookie, get the access token (renew if needed)
    F->>FK: PUT /api/rest/crm/customer/update/42<br/>bearer token, data + response
    FK->>FK: check and exchange the token, resolve the tenant,<br/>tenant gate, roles and attributes
    FK->>R: RequestContext filled
    R->>R: read the body, resolve response, take over files
    R->>S: updateObject(DTO)
    S->>DB: is row 42 visible to this person? load the object
    Note over S,DB: first database access: the transaction starts here
    S->>S: recursion over object and children:<br/>check the role per object, transfer values,<br/>remember rule violations, queue hooks
    S->>H: before hooks
    H-->>S: may still change fields
    S->>S: validation: re-check the remembered violations
    S->>DB: hand the change to the database
    S->>H: after hooks
    S->>DB: flush: the SQL runs, database rules apply,<br/>Envers writes the revision
    S->>DB: read back with the response (READ hook per object)
    S-->>R: DTO
    R->>DB: COMMIT
    R-->>F: 200 with data and meta
    F-->>U: the form shows the saved state

Up to and including “RequestContext filled” this is the same path as a read. From the REST layer on, it becomes a different one.

Who checks when

The order is not a matter of taste: every stage assumes the previous one passed.

PUT /api/rest/crm/customer/update/42
  1. Filter chain
    Token
    Is the signature valid and the token not expired?
    ↳ no 401
  2. CIAS
    Tenant
    Which tenant, and is it served?
    ↳ no 403 with cias.authentication.…
  3. CDMS
    Field selection
    Is there a response in the body, and can the body be read?
    ↳ no 400
  4. CDMS
    Visibility
    Is the row visible to this person at all — with the same filters as on a read?
    ↳ no 404, as if it did not exist
  5. CDMS
    Write role
    Does one of the effective roles allow changing customer? Likewise for every child written along
    ↳ no 403 missing-permission|<role>
  6. CDMS
    Transfer the values
    Write the fields from data into the object, set defaults, create or link children. Rule violations are only remembered
    ↳ no 400 / 404 for children that cannot be resolved
  7. Hook
    Before hooks
    The project's own business logic sees the fully populated object and may still change it
    ↳ no The hook can refuse on its own, with its own response
  8. CDMS
    Validation
    Every remembered violation is checked against the value that stands on the object now
    ↳ no 422 validation-failed with all violations at once
  9. Database
    Save and flush
    The SQL runs, the database checks its own rules, for example unique values
    ↳ no 409 already-exists for a value already taken · 400 constraint-violation if a value does not fit the column · 503 if the database cannot be reached
  10. CDMS
    Read back
    The object is read with the response and with the read permissions of the person
    ↳ no 403 / 404 – and the change is taken back with it
  11. CDMS
    Commit
    Commit the transaction before the response is written
    ↳ no rollback; 409 on a detected concurrent change, otherwise a server error
  12. 200 with the saved object – the state that is now in the database

Why this order

OrderWhy it has to be this way
Tenant before roleWhich roles apply depends on the tenant. Roles in the active tenant replace the global ones, see Effective roles.
Visibility before everything elseOtherwise an error message would reveal that a foreign row exists. Knowing an id is not enough.
Role before hooksIf a role is missing, not a single hook runs. So a hook cannot trigger anything the person was not allowed to do.
Hooks before validationA before hook may fill a required field the client does not know about — a running number, for instance. See The order within a write operation.
Read back before the commitThe response should show the saved state. If reading fails, writing is void as well.
Commit before the responseOtherwise the client would get a “200” for something that can still fail afterwards.

What a refusal leaves behind, stage by stage

Not every refusal leaves the same state. What matters is whether the request had already reached the database.

Refused at stageWho answersWhat is in the databaseDid hooks run?
token, tenantfilter chain (CIAS)nothing, there was no transactionno
field selection, bodyREST layernothingno
visibility, rolesystem layernothing; the transaction was opened for reading at mostno
transfer the valuessystem layernothingno
a before hook refuseshooknothing in the database — outside it, whatever the hook triggered staysyes, up to that hook
validationsystem layernothingyes, all before hooks
flush (database rule)persistencenothing, everything is rolled backyes, before and after
read backsystem layernothing, the change is taken backyes, all
commitpersistencenothingyes, all

Two sentences about this that are often missing in practice:

  • From validation downwards, your before hooks have already run. Everything a hook does outside the database — a mail, a call into another system — stays, even when the request ends in an error afterwards.
  • After hooks do not yet see a secured state. After them come flush, read back and commit. Each of those three can still bring the whole request down.
What ends up in the database
All stages passedCommit succeedsResult
yesyes200 with the object, the change is durable
yesnorollback; 409 on a detected concurrent change, otherwise a server error
no–error response, everything rolled back — including the parts already written before

The transaction bracket

A transaction is the bracket around the changes: either all of them or none. In CDMS that bracket is exactly one request.

Where the bracket opens and closes
  1. 1
    CDMS→Database
    start: at the first database access of the request. On a change that is the visibility check, on a create the first save
    Not already when the request arrives. A request that fails at token, tenant or field selection never opened a transaction.
  2. 2
    CDMS→Database
    flush: the collected SQL goes to the database. It checks its rules; other requests do not see any of it yet
  3. 3
    CDMS→Database
    commit: immediately before the response body is written
    Result: When the success response arrives at the client, a request right after it already sees the change.

If anything fails, the whole request is marked as failed: every open transaction is rolled back at once, and nothing of this request is committed at the end. That also covers databases that were touched earlier.

What is inside the bracket
Inside the transaction
succeeds or vanishes together
  • the object itself
  • all nested children created, changed or deleted
  • what hooks write through CDMS on models of the same level
  • the revision of the audit
  • reading back for the response
Outside
survives a rollback
  • file contents in the file storage
  • mails, HTTP calls and messages from a hook
  • everything that concerns a second database
  • everything from an earlier request

A request that writes a system model and a tenant model has two transactions in two databases. They are committed one after the other, not together. See No atomicity across two databases and One request, one transaction.

Audit and revision

If the model is audited, a revision is created on a write: a copy of the object after the change, together with who made it, when and from where.

From the request to the revision
  1. 1
    Filter chain
    reads IP address and user agent from the request and user id and name from the token, and puts them into the RequestContext
  2. 2
    CDMS→Database
    at the flush, Envers writes a row into revinfo and, for every changed object, a row into its _AUD table
    The four values come from the RequestContext, not from the object. Without a request context — in a background job, for instance — they stay empty.
  3. 3
    CDMS→Database
    with the commit the revision becomes durable, together with the change
    Result: If the request fails, the revision vanishes with everything else. There is no revision without a change.

All changes of one request get the same revision number — one per database. What it holds and how to read it: What a revision records and Reading the history.

CIAS keeps a separate trail of its own, for role grants, suspensions and tenant changes. The two trails have nothing to do with each other, see CIAS audit and CDMS history.

The three kinds of write compared

The path is the same for all three. What differs is what it checks along the way.

Creating, changing, deleting

When: POST /create – a new object, without an id.

No visibility check, because there is nothing to see yet. What is checked is the create role, and for every child created along with it. Validation looks at every field of the model; a missing field counts as empty. Default values are already in place at that point.

Result: 200 with the new object and its id, and a revision of type ADD in the audit. See Creating an object.

When: PUT /update/{id} or PATCH /update/{id}.

Visibility first, then the stored object is loaded, then the update role. PUT describes the whole target state and therefore checks every field; PATCH names only the change and checks only the sent fields. A child without an id inside a PATCH is created — and checked with the rules for creating.

Result: 200 with the new state, and a revision of type MOD in the audit. See PUT or PATCH? The null trap.

When: DELETE /delete/{id}.

Visibility, then the delete role per object, then dependent children are queued for removal and other relations are detached. There is no validation, no reading back and no READ hook — nothing comes back, after all.

Result: 200 without a body, and a revision of type DEL holding only the id. See How a DELETE runs.

And in the standalone operating mode?

Exactly the same. The difference between “CIAS embedded” and “CIAS as its own service” lies entirely before the REST layer: the tenant gate and the attribute lookup ask their question once through a method call and once over HTTP. From the RequestContext on, the write path is the same code with the same checks.

The two ways side by side are on From login to the data, all differences on Embedded and standalone compared.

Traps

Where to go next

Sources in the code and the knowledge base
  • hub-frontend – server/utils/backendFetch.ts, useCmsApi.ts, server/api/hub/projects/[projectId].put.ts
  • CIAS/cias-authentication – JwtSessionFilter (RequestContext with IP and user agent), TokenParser.admit, TenantGate, EffectiveRoles, EffectiveAttributes
  • CDMS/cdms-rest-api – AbstractRestApi.createObject/updateObject/patchObject (getFileMap), RequestTransactionCommitter (beforeBodyWrite, postHandle, settle), RequestFailureMarker (markRollbackOnly, settle), CdmsExceptionMapper (markRollbackOnly, settle), payloads/WritePayload
  • CDMS/cdms-system-layer – AbstractSystemLayer.createObject/updateObject/patchObject/deleteObject (beginValidation, recursive*, runAllBefore, assertValid, persistence.*, runAllAfter, flush, readObject); AbstractLayer.recursivePrepare (role check per object), assertVisibleForWrite; session/ValidationRequestContext, session/HookRequestContext
  • CDMS/cdms-authorization – AbstractAuthorizationLayer (createAccessAllowedByClass, updateAccessAllowedByClass, deleteAccessAllowedByClass)
  • CDMS/cdms-persistence-database – AbstractDatabasePersistence, auditing/AuditRevisionEntity, AuditRevisionListener (values from the RequestContext)
  • commons-persistence – DatabaseRequestContext (getEntityManager, resolveTenant, commitThreadTransactions, markRollbackOnly), TenantEntityManagerFactory
Search