Entities & Custom Fields
Entity extensibility is central to Open Mercato. Every endpoint published by the module lives under /api/entities/*; the tables below list the suffix you append to your API origin.
export BASE_URL="http://localhost:3000/api"
export API_KEY="<paste your API key secret here>"
export ENTITY_ID="example.todo" # generated or custom entity id
export CUSTOM_ENTITY_ID="<uuid from POST /entities/entities>"
export RECORD_ID="<record uuid>"
Provision an API key with the required entities.* features as described in Managing API keys. Feature flags are defined in packages/core/src/modules/entities/acl.ts:1.
Heads-up: The dispatcher checks feature requirements exported from each route’s
metadata. If a call returns403, confirm the feature listed in the section header is granted to the caller.
Custom entity catalogue — /entities/entities
List entities — GET /entities/entities
- Auth-only guard; no feature requirement (
packages/core/src/modules/entities/api/entities.ts:8). - Returns generated MikroORM entities plus tenant/org scoped overrides with field counts.
curl -X GET "$BASE_URL/entities/entities" \
-H "X-Api-Key: $API_KEY" \
-H "Accept: application/json"
Upsert custom entity — POST /entities/entities
- Feature:
entities.definitions.manage. - Body matches
upsertCustomEntitySchema(packages/core/src/modules/entities/data/validators.ts:72):entityId(string) identifies the logical entity (e.g.custom.ticket).- Display metadata:
label, optionaldescription,labelField,defaultEditor,showInSidebar. - Flags:
isActive(default true).
curl -X POST "$BASE_URL/entities/entities" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entityId": "custom.ticket",
"label": "Support Ticket",
"description": "Tenant defined issue tracker",
"labelField": "title",
"defaultEditor": "markdown",
"showInSidebar": true
}'
Soft delete custom entity — DELETE /entities/entities
- Feature:
entities.definitions.manage. - Request accepts JSON
{ "entityId": "<id>" }; the implementation scopes deletion to the caller’s tenant/org (packages/core/src/modules/entities/api/entities.ts:121).
curl -X DELETE "$BASE_URL/entities/entities" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "entityId": "custom.ticket" }'
Sidebar entities — GET /entities/sidebar-entities
- Auth-only guard (
packages/core/src/modules/entities/api/sidebar-entities.ts:7). - Returns custom entities flagged with
showInSidebarso you can build scoped navigation.
curl -X GET "$BASE_URL/entities/sidebar-entities" \
-H "X-Api-Key: $API_KEY" \
-H "Accept: application/json"
Field definitions — /entities/definitions*
Load active definitions — GET /entities/definitions
- Auth-only guard; no feature gate so record forms can load fields (
packages/core/src/modules/entities/api/definitions.ts:8). - Accepts repeated
entityId=<id>parameters or a comma-delimitedentityIdsquery. - Response merges global, org, and tenant scopes, omitting tombstoned keys and preserving priority metadata (
packages/core/src/modules/entities/api/definitions.ts:120).
curl -X GET "$BASE_URL/entities/definitions?entityId=$ENTITY_ID" \
-H "X-Api-Key: $API_KEY" \
-H "Accept: application/json"
Management snapshot — GET /entities/definitions.manage
- Feature:
entities.definitions.manage. - Returns
{ items, deletedKeys }, showing winners per scope plus keys tombstoned in the current tenant/org (packages/core/src/modules/entities/api/definitions.manage.ts:15).
curl -X GET "$BASE_URL/entities/definitions.manage?entityId=$ENTITY_ID" \
-H "X-Api-Key: $API_KEY" \
-H "Accept: application/json"
Upsert definition — POST /entities/definitions
- Feature:
entities.definitions.manage. - Body matches
upsertCustomFieldDefSchema(packages/core/src/modules/entities/data/validators.ts:97):- Required:
entityId,key,kind. configJsonlets you setlabel,options,filterable,listVisible,formEditable, dictionaries, validation rules, and editor hints.- Optional
isActivesoft disables fields without deleting history.
- Required:
curl -X POST "$BASE_URL/entities/definitions" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"entityId\": \"$ENTITY_ID\",
\"key\": \"priority\",
\"kind\": \"enum\",
\"configJson\": {
\"label\": \"Priority\",
\"options\": [\"low\", \"medium\", \"high\"],
\"filterable\": true,
\"listVisible\": true
},
\"isActive\": true
}"
Batch upsert — POST /entities/definitions.batch
- Feature:
entities.definitions.manage. - Body
{ entityId, definitions: [ ... ] }lets you send an ordered array of definition payloads (packages/core/src/modules/entities/api/definitions.batch.ts:19). - Optional
fieldsetsarray (matchingcustomFieldsetSchema) andsingleFieldsetPerRecordflag let you manage the drop-down metadata programmatically. Each field definition can also setconfigJson.fieldsetandconfigJson.groupto map the field into the new layout.
curl -X POST "$BASE_URL/entities/definitions.batch" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"entityId\": \"$ENTITY_ID\",
\"definitions\": [
{ \"key\": \"status\", \"kind\": \"enum\", \"configJson\": { \"label\": \"Status\", \"options\": [\"open\",\"won\",\"lost\"], \"priority\": 0 } },
{ \"key\": \"assignee\", \"kind\": \"string\", \"configJson\": { \"label\": \"Assignee\", \"filterable\": true, \"priority\": 1 } }
],
\"fieldsets\": [
{
\"code\": \"service_booking\",
\"label\": \"Services · Booking\",
\"icon\": \"solar:calendar-linear\",
\"groups\": [
{ \"code\": \"timing\", \"title\": \"Timing\" },
{ \"code\": \"resources\", \"title\": \"Resources\" }
]
}
],
\"singleFieldsetPerRecord\": true
}"
Soft delete definition — DELETE /entities/definitions
- Feature:
entities.definitions.manage. - Accepts JSON
{ "entityId": "...", "key": "..." }and marks the definition inactive (packages/core/src/modules/entities/api/definitions.ts:189).
curl -X DELETE "$BASE_URL/entities/definitions" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"entityId\": \"$ENTITY_ID\",
\"key\": \"priority\"
}"
Restore definition — POST /entities/definitions.restore
- Feature:
entities.definitions.manage. - Body
{ entityId, key }reactivates a tombstoned definition and invalidates the cache (packages/core/src/modules/entities/api/definitions.restore.ts:9).
curl -X POST "$BASE_URL/entities/definitions.restore" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"entityId\": \"$ENTITY_ID\",
\"key\": \"priority\"
}"
Records — /entities/records
List records — GET /entities/records
- Feature:
entities.records.view(packages/core/src/modules/entities/api/records.ts:18). - Required query:
entityId. Optional filters include:- Paging:
page,pageSize(max 100),withDeleted. - Sorting:
sortField,sortDir. - Export:
format=csv|json|xml|markdown,exportScope=full,all=true,full=true. - Arbitrary field filters; prefix custom fields with
cf_.
- Paging:
curl -X GET "$BASE_URL/entities/records?entityId=$ENTITY_ID&page=1&pageSize=25&sortField=created_at&sortDir=desc" \
-H "X-Api-Key: $API_KEY" \
-H "Accept: application/json"
Create record — POST /entities/records
- Feature:
entities.records.manage. - Body
{ entityId, recordId?, values }wherevaluesis a flat object; prefixingcf_is optional because the handler normalises keys (packages/core/src/modules/entities/api/records.ts:204). - Supplying a non-UUID
recordIdtriggers auto-generation; use this to support optimistic UI drafts.
curl -X POST "$BASE_URL/entities/records" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"entityId\": \"$ENTITY_ID\",
\"values\": {
\"title\": \"Onboard new merchant\",
\"description\": \"Kick off data import\",
\"cf_priority\": \"high\"
}
}"
Update record — PUT /entities/records
- Feature:
entities.records.manage. - Body
{ entityId, recordId, values }with the same shape as the create payload (packages/core/src/modules/entities/api/records.ts:246). - The handler re-validates values against active custom fields and, if the provided
recordIdis not a UUID, falls back to create semantics (useful for spreadsheet imports).
curl -X PUT "$BASE_URL/entities/records" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"entityId\": \"$ENTITY_ID\",
\"recordId\": \"$RECORD_ID\",
\"values\": {
\"title\": \"Onboard new merchant (updated)\",
\"cf_priority\": \"medium\"
}
}"
Delete record — DELETE /entities/records
- Feature:
entities.records.manage. - Accepts query parameters (
?entityId=...&recordId=...) or JSON body; deletion is soft and scoped to the active tenant/org (packages/core/src/modules/entities/api/records.ts:337).
curl -X DELETE "$BASE_URL/entities/records?entityId=$ENTITY_ID&recordId=$RECORD_ID" \
-H "X-Api-Key: $API_KEY"
Relation options — GET /entities/relations/options
- Feature:
entities.definitions.view. - Parameters (
packages/core/src/modules/entities/api/relations/options.ts:12):entityId(target entity providing options).labelField(field used for display text).- Optional
q(search),pageSize,cursor.
curl -X GET "$BASE_URL/entities/relations/options?entityId=$ENTITY_ID&labelField=title&q=demo" \
-H "X-Api-Key: $API_KEY" \
-H "Accept: application/json"
Response example:
{
"items": [
{ "value": "6d44ce18-4bc7-49d6-8b25-a94288363f25", "label": "Demo Ticket" }
],
"nextCursor": null
}
Implementation helpers
- The module also exports
createDefinitionsCacheKeyandinvalidateDefinitionsCacheinpackages/core/src/modules/entities/api/definitions.cache.ts:6. These utilities are meant for internal cache management when definitions change; there is no public HTTP route at/entities/definitions.cache, even though the generator lists the file for completeness. - If you mirror the caching strategy, tag invalidations with
entities:definitions:<tenant>so UI fragments and API consumers observe new fields immediately.
Pair these endpoints with the DI queryEngine for server-side filtering or plug them into shared admin components (packages/shared/src/lib/crud/factory.ts:23) to deliver tenant-aware experiences without rebuilding CRUD plumbing.