﻿# models — entity model schema system

A model is a named, versioned schema registered per tenant. Every entity in the system is an instance of exactly one model. Models are identified by `(ent…

<em>cyoda-go version <a href="https://github.com/Cyoda-platform/cyoda-go/releases/tag/v0.6.2">0.6.2</a></em>

# models

## NAME

models — entity model schema system: registration, lifecycle, import, export, and validation.

## SYNOPSIS

```
GET    /api/model/
GET    /api/model/export/{converter}/{entityName}/{modelVersion}
POST   /api/model/import/{dataFormat}/{converter}/{entityName}/{modelVersion}
POST   /api/model/validate/{entityName}/{modelVersion}
DELETE /api/model/{entityName}/{modelVersion}
POST   /api/model/{entityName}/{modelVersion}/changeLevel/{changeLevel}
PUT    /api/model/{entityName}/{modelVersion}/lock
PUT    /api/model/{entityName}/{modelVersion}/unlock
GET    /api/model/{entityName}/{modelVersion}/workflow/export
POST   /api/model/{entityName}/{modelVersion}/workflow/import
```

Context path prefix is `CYODA_CONTEXT_PATH` (default `/api`).

## DESCRIPTION

A model is a named, versioned schema registered per tenant. Every entity in the system is an instance of exactly one model. Models are identified by `(entityName, modelVersion)`. The model ID is a deterministic UUID v5 derived from that key: `UUID.newSHA1(NameSpaceURL, "{entityName}.{modelVersion}")`.

Models have two lifecycle states: `UNLOCKED` and `LOCKED`. A `LOCKED` model blocks further imports. An `UNLOCKED` model accepts re-import and schema merging. Entities can only be created against a `LOCKED` model. Deletion is blocked while any entities reference the model.

Schema inference is additive: importing sample data against an existing model merges the incoming schema with the stored one. The model's `changeLevel` field controls which structural changes are allowed during entity ingestion on a locked model.

## ENDPOINTS

**GET /api/model/**

List all models for the authenticated tenant.

Response: `200 OK`, `application/json`, array of `EntityModelDto`.

**POST /api/model/import/{dataFormat}/{converter}/{entityName}/{modelVersion}**

Import or update a model schema from sample data. Body size limit: 10 MiB.

- `dataFormat` (path): `JSON` or `XML`
- `converter` (path): `SAMPLE_DATA` (only supported converter; `JSON_SCHEMA` and `SIMPLE_VIEW` are defined in the OpenAPI but return `400 BAD_REQUEST` in this implementation)
- `entityName` (path): string — model name
- `modelVersion` (path): int32 — model version number

If the model does not exist, it is created with state `UNLOCKED`. If it exists and is `UNLOCKED`, the incoming schema is merged (additive). If it exists and is `LOCKED`, returns `409 CONFLICT`.

Response: `200 OK`, `application/json`, UUID string — the model ID.

**GET /api/model/export/{converter}/{entityName}/{modelVersion}**

Export a model schema in the specified format.

- `converter` (path): `JSON_SCHEMA` or `SIMPLE_VIEW`
- `entityName` (path): string
- `modelVersion` (path): int32

Response: `200 OK`, `application/json` — format depends on converter.

**POST /api/model/validate/{entityName}/{modelVersion}**

Validate a JSON payload against the model's schema. Returns a result object, not an HTTP error, on validation failure.

- `entityName` (path): string
- `modelVersion` (path): int32

Request body: any JSON object.

Response: `200 OK`, `application/json`, `EntityModelActionResultDto`.

**DELETE /api/model/{entityName}/{modelVersion}**

Delete a model. Blocked if the model is `LOCKED` or if any entities reference it (entity count > 0).

Response: `200 OK`, `application/json`, `EntityModelActionResultDto` on success. `409 CONFLICT` if entities exist.

**POST /api/model/{entityName}/{modelVersion}/changeLevel/{changeLevel}**

Set or update the change level on a model. Meaningful for locked models; unlocked models always allow all changes.

- `changeLevel` (path): `ARRAY_LENGTH`, `ARRAY_ELEMENTS`, `TYPE`, or `STRUCTURAL`

Change levels are hierarchical (most restrictive to most permissive):

- `ARRAY_LENGTH` — permits only increases in uni-type array width
- `ARRAY_ELEMENTS` — allows multi-type array changes without adding new types
- `TYPE` — permits modifications to existing types
- `STRUCTURAL` — allows fundamental model changes including new fields

Response: `200 OK`, `application/json`, `EntityModelActionResultDto`.

**PUT /api/model/{entityName}/{modelVersion}/lock**

Lock a model. The model must be `UNLOCKED`. Returns `409 CONFLICT` if already locked.

Response: `200 OK`, `application/json`, `EntityModelActionResultDto`.

**PUT /api/model/{entityName}/{modelVersion}/unlock**

Unlock a model. The model must be `LOCKED` and have zero associated entities. Returns `409 CONFLICT` if entities exist or model is not locked.

Response: `200 OK`, `application/json`, `EntityModelActionResultDto`.

**GET /api/model/{entityName}/{modelVersion}/workflow/export**

Export all workflow configurations for the model. Returns `404 WORKFLOW_NOT_FOUND` if no workflows exist.

Response: `200 OK`, `application/json`:

```json
{
  "entityName": "nobel-prize",
  "modelVersion": 1,
  "workflows": []
}
```

**POST /api/model/{entityName}/{modelVersion}/workflow/import**

Import or replace workflow configurations for the model. See `workflows` topic.

## REQUEST SCHEMAS

**EntityModelDto** (returned by `GET /api/model/`):

```json
{
  "id": "31134900-d9cb-11ee-b913-ae468cd3ed16",
  "modelName": "nobel-prize",
  "modelVersion": 1,
  "currentState": "LOCKED",
  "modelUpdateDate": "2025-08-02T13:31:48.141053-07:00"
}
```

- `id` — UUID (deterministic v5, derived from name+version)
- `modelName` — string
- `modelVersion` — int32
- `currentState` — `"LOCKED"` or `"UNLOCKED"`
- `modelUpdateDate` — RFC 3339 timestamp, nullable

**EntityModelActionResultDto** (returned by lock, unlock, delete, changeLevel, validate):

```json
{
  "success": true,
  "message": "Model nobel-prize:1 locked",
  "modelId": "cee334fa-c0ac-11f0-ba79-ae468cd3ed16",
  "modelKey": {
    "name": "nobel-prize",
    "version": 1
  }
}
```

- `success` — boolean
- `message` — human-readable result string
- `modelId` — UUID
- `modelKey.name` — string
- `modelKey.version` — int32

**Import request body** (sample data, JSON format):

```json
{
  "category": "physics",
  "year": "2024",
  "laureates": [
    {
      "firstname": "John",
      "surname": "Hopfield",
      "id": "1037",
      "motivation": "for foundational discoveries",
      "share": "2"
    }
  ]
}
```

The importer walks the JSON structure and infers a typed schema. Subsequent imports are merged additively.

**Export — SIMPLE_VIEW format**:

```json
{
  "currentState": "LOCKED",
  "model": {
    "$": {
      "#.laureates": "OBJECT",
      ".category": "STRING",
      ".year": "STRING"
    },
    "$.laureates[*]": {
      "#": "ARRAY_ELEMENT",
      ".firstname": "STRING",
      ".id": "STRING",
      ".motivation": "STRING",
      ".share": "STRING",
      ".surname": "STRING"
    }
  }
}
```

The `"$"` bucket includes a `"#.fieldname": "OBJECT"` entry for each array field in the root object. The `"$.fieldname[*]"` bucket contains the array element schema with `"#": "ARRAY_ELEMENT"` as a type marker.

**Export — JSON_SCHEMA format**:

```json
{
  "currentState": "LOCKED",
  "model": {
    "type": "object",
    "properties": {
      "category": { "type": "string" },
      "year": { "type": "string" },
      "laureates": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "firstname": { "type": "string" },
            "share": { "type": "string" },
            "id": { "type": "string" },
            "surname": { "type": "string" },
            "motivation": { "type": "string" }
          }
        }
      }
    }
  }
}
```

## LIFECYCLE

A model moves between two states:

- `UNLOCKED` — initial state after first import; re-import is permitted; entities cannot be created
- `LOCKED` — entities can be created; re-import is blocked; change-level controls in-flight schema extension

Transitions:

- `UNLOCKED` → `LOCKED` via `PUT /model/{name}/{version}/lock`
- `LOCKED` → `UNLOCKED` via `PUT /model/{name}/{version}/unlock` (only when entity count is zero)

The `changeLevel` field controls schema evolution on locked models. When set, entity ingestion that introduces new structure triggers an additive schema extension (delta computed via `schema.Diff`, appended via `ModelStore.ExtendSchema`, committed with the entity transaction).

## ERRORS

- `errors.MODEL_NOT_FOUND` — `404` — model does not exist for the given name and version
- `errors.CONFLICT` — `409` — import blocked (model is locked), lock failed (already locked), unlock failed (entities exist or model not locked), delete failed (entities exist)
- `errors.VALIDATION_FAILED` — `400` — workflow import validation failed (static analysis)
- `errors.BAD_REQUEST` — `400` — unsupported converter, invalid changeLevel, malformed body

## EXAMPLES

**Import a model from sample JSON:**

```
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"category":"physics","year":"2024","laureates":[{"firstname":"John","surname":"Hopfield","id":"1037"}]}' \
  "http://localhost:8080/api/model/import/JSON/SAMPLE_DATA/nobel-prize/1"
```

Response: `"1d1e1b10-1155-11f0-bcd5-ae468cd3ed16"`

**Lock the model:**

```
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/api/model/nobel-prize/1/lock"
```

**Set change level (allow structural evolution on locked model):**

```
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/api/model/nobel-prize/1/changeLevel/STRUCTURAL"
```

**List all models:**

```
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/api/model/"
```

**Export as SIMPLE_VIEW:**

```
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/api/model/export/SIMPLE_VIEW/nobel-prize/1"
```

**Validate a payload against the model:**

```
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"category":"physics","year":"2024"}' \
  "http://localhost:8080/api/model/validate/nobel-prize/1"
```

**Delete a model (must be unlocked and have zero entities):**

```
curl -s -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/api/model/nobel-prize/1"
```

## SEE ALSO

- crud
- workflows
- search
- errors.MODEL_NOT_FOUND
- errors.VALIDATION_FAILED
- errors.CONFLICT
- openapi

## See also

- [`cyoda help crud`](/help/crud/) — Entities are instances of models. Each entity has a UUID, a model reference (`entityName`, `modelVersion`), and a lifecycle state managed by the workflow engine. Creating an entity requires the referenced model to be in `LOCKED` state. All write operations run within a Cyoda transaction and return a `transactionId` alongside the affected entity IDs.
- [`cyoda help workflows`](/help/workflows/) — A workflow definition is a named finite state machine attached to an entity model. Workflows are stored per model reference `(entityName, modelVersion)`. A model may have multiple workflow definitions; the engine selects the matching one per entity using the workflow-level `criterion` field evaluated at entity creation time. When no `criterion` matches, the engine uses the default built-in workflow.
- [`cyoda help search`](/help/search/) — Search operates against a specific entity model `(entityName, modelVersion)`. Two modes are supported:
- [`cyoda help errors MODEL_NOT_FOUND`](/help/errors/model_not_found/) — The entity type or model name specified in the request does not exist in the tenant's model registry. Occurs when creating entities with an unknown type, importing data that references a missing model, or performing model lifecycle transitions on a model ID that does not exist.
- [`cyoda help errors VALIDATION_FAILED`](/help/errors/validation_failed/) — Unlike `BAD_REQUEST` (which covers parse failures), this error is returned when the payload is parseable but violates the registered model schema — for example, a required field is missing, a value is out of the allowed range, or a workflow guard condition is not satisfied. The error detail includes the specific validation failure.
- [`cyoda help errors CONFLICT`](/help/errors/conflict/) — The server detected that the entity was modified by another writer between the time it was read and the time the current write was committed. Normal outcome under concurrent load.
- [`cyoda help openapi`](/help/openapi/) — cyoda-go generates its OpenAPI 3.1 specification from the embedded `api/openapi.yaml` file compiled into the binary at build time. The spec is served at `/openapi.json` with runtime-patched server URLs. The Scalar API Reference UI is served at `/docs` and loads the spec from `/openapi.json`.

## Raw formats

- [`/help/models.json`](/help/models.json) — full descriptor (matches `GET /help/{topic}` envelope)
- [`/help/models.md`](/help/models.md) — body only