Documentation

BaseModel

The wrapper every record carries — system fields, optimistic concurrency, and the pk vs sk split.

Every record AppEngine reads or writes is wrapped in BaseModel\<T\>. The wrapper carries identity, versioning, lifecycle state, and audit metadata; the payload you actually care about sits at data. Master this shape once and you've understood the data layer.

The shape

From @jaclight/dbsdk (models/base.model.d.ts):

interface BaseModel\<T\> {
  pk: string;                 // primary key (record id)
  sk: string;                 // sort key (org-scoped namespace)
  name: string;               // human label
  data?: T;                   // your domain payload
  datatype?: string;          // collection name
  subschema?: string;         // sub-type within a collection
  version: number;            // optimistic concurrency token
  state?: ModelState;         // workflow state (draft, published, ...)
  isNew?: boolean;
  workflow?: TaskModel;
  post?: PostSubModel;        // share/comment/category settings
  style?: StyleSubModel;      // CMS-style styling (theme, css)
  createdate?: Date;
  modifydate?: Date;
  publishedDate?: Date;
  author?: string;
  owner?: { datatype?; id?; name?; email? };
  requiredRole?: RequiredRoleModel;  // per-record RBAC overrides
  comments?: CommentModel[];
  activities?: ActivityModel[];
  messages?: MessageModel[];
  shares?: number;
  likes?: number;
  dislikes?: number;
  averageRating?: number;
  ratingCount?: number;
  reactions?: any[];
  search?: string;            // denormalized search blob
  views?: number;
  last_viewed?: Date;
  modified_by?: string;
  created_by?: string;
  client?: string;
}

The system reserves these fields under baseModelSystemFields — never put your own properties at the top level; they go inside data.

pk vs sk

pk is the record's identity. It's stable, unique within an org, and what you pass to /repository/get/{datatype}/{id}.

sk is the org-scoped sort key — typically <orgid>/<datatype> or a hierarchical path. The repository layer uses it to:

  • enforce tenant isolation (an sk always begins with the orgid),
  • group records for efficient range queries,
  • support hierarchical reads (e.g. all pages under a site).

You almost never construct sk yourself. RepositoryService.createNewData() and the repository providers fill it in. Read it; don't write it.

Optimistic concurrency

Every write increments version. To update safely, send the version you read:

// fetch
const record = await fetch('/repository/get/contact/abc123', { headers });
const body = await record.json();

// modify
body.data.email = '[email protected]';
body.version;  // e.g. 4 — must be sent back

// write
await fetch('/repository/update/abc123', {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});

If another writer has bumped the record since your read, the update will reject with a version conflict. Refetch, reapply, retry.

ModelState — workflow

state drives the publishing/workflow machinery. The enum lives in dbsdk:

enum ModelState {
  draft, new, pending, inprogress, reviewed, approved,
  published, completed, hold, rejected, cancelled, archived, deleted
}

Most domain code only cares about draft, published, and archived. The rest are used by the approval flow when a collection has enableWorkflow: true. See State and workflow.

Reading a record

curl https://appengine.appmint.io/repository/get/contact/contact-abc123 \
  -H "orgid: my-org" \
  -H "Authorization: Bearer $JWT"
GET/repository/get/{datatype}/{id}JWT+APIKEY

Writing a record

POST /repository/create for new records, POST /repository/update/{id} for existing ones.

curl -X POST https://appengine.appmint.io/repository/create \
  -H "orgid: my-org" \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "datatype": "contact",
    "data": { "email": "[email protected]", "name": "Alice" }
  }'
POST/repository/createJWT+APIKEY
POST/repository/update/{id}JWT+APIKEY

The server fills in pk, sk, version, createdate, modifydate, and state. You provide datatype and data.

Don't flatten the payload

Sending { "email": "...", "name": "..." } at the top level won't work — those go inside data. The system fields are reserved.

Partial updates

For single-field updates, use the partial endpoint — no version check, but smaller payload.

POST/repository/update-partial/{datatype}/{id}JWT+APIKEY
{ "data.email": "[email protected]" }

The server merges this into the existing record. Use it for status flips and counter increments; use /repository/update/{id} when you care about concurrency.