What this is about
Some data exists only once: a person’s settings, a tenant’s configuration, the basic settings of the whole installation. For these cases you mark a model in the hub as a singleton.
One object per what?
How many objects there are in total is decided by the model level:
- one object for the whole installation
- in the system database
- Example: basic settings of the platform
- one object per tenant
- in the tenant database
- Example: a company's configuration
- one object per person
- in the tenant database, filtered by
_userId - Example: personal settings
The endpoints without an ID
| Normal model | Singleton | |
|---|---|---|
| create | POST /create | POST /create |
| read | POST /read/{id} | POST /read |
| quick look | GET /read/{id} | GET /read |
| replace | PUT /update/{id} | PUT /update |
| update | PATCH /update/{id} | PATCH /update |
| delete | DELETE /delete/{id} | DELETE /delete |
| search | POST /query | does not exist |
| download file | GET /{id}/file | GET /file |
| history | POST /{id}/history | POST /history |
| rollback | POST /{id}/rollback/{revision} | POST /rollback/{revision} |
The object still has an id. It appears in the response, but the client never has to send it.
Only the search is missing, because there is nothing to search for. History and rollback, on the other hand, work exactly as for every other audited model: the history is not a list of objects but the course of one object over time.
How the server finds the one object
Before every operation, the server looks up the existing object, with the same rules as a normal search:
-
1CDMSpicks the database by model level (system DB or the one of the tenant from the token)
-
2CDMSadds the filter
_userId = signed-in personfor user singletons -
3CDMS→Databaselooks up the object
-
4CDMSfound → its
idis used for the operation. Not found → there is none yet
Every operation step by step
When: POST /create
-
1CDMSIs there already an object in this scope?
-
2CDMS→Clientyes → 400
object-already-exists|use-update -
3CDMS→Databaseno → creates the object as for a normal model, with permission check, validation and hooks
Result: The one object now exists.
When: POST /read with response, or GET /read
-
1CDMSlooks up the object
-
2CDMS→Clientnot there → 404
no-object-found -
3CDMS→Clientthere → returns it with the requested fields
When: PUT /update
-
1CDMSlooks up the object
-
2CDMS→Clientnot there → 404
no-data-exists|use-create -
3CDMSthere → puts its
idinto the payload itself and replaces as with a normal PUT
When: PATCH /update
-
1CDMSlooks up the object
-
2CDMS→Clientnot there → 404
no-object-found -
3CDMSthere → puts in the
iditself and updates as with a normal PATCH: a missing field stays,nullclears
When: DELETE /delete
-
1CDMSlooks up the object
-
2CDMSthere → deletes it as with a normal DELETE. Not there → nothing to do, no error
Result: Afterwards there is no object; a new create is possible again.
When: GET /file, POST /history
As for a normal model, just without an id in the path. Both answer 404 if there is no object yet. History exists for audited models only.
When: POST /rollback/{revision}, for audited models with a rollback endpoint
-
1CDMSlooks up the object
-
2CDMS→Clientnot there → 404
data-not-found -
3CDMSthere → checks the rollback role, runs the ROLLBACK hooks and restores the named revision, exactly as for a normal model
Result: The old state is back, as a new revision. The revision number comes from the history (revisionMeta.ref).
The lifeline of a singleton
stateDiagram-v2
direction LR
[*] --> Empty
Empty --> Present: POST /create
Empty --> Empty: read / update / patch → 404
Present --> Present: read, PUT, PATCH, rollback
Present --> Present: create → 400 use-update
Present --> Empty: DELETE
A typical pattern in the client
Because create and update are separate, a settings dialog needs a small switch:
| Did GET /read return an object when the dialog opened? | Call on save |
|---|---|
| yes | PATCH /update with the changed fields |
| no (404) | POST /create with all fields |