Documentation

Repository CRUD

The generic create/read/update/delete surface that works for every collection.

Almost every collection in AppEngine — built-in or custom — is reachable through one controller: RepositoryController. Once you know its shape, every datatype (products, contacts, leads, posts, your custom widgets) follows the same conventions.

The BaseModel\<T\> envelope

Every record on the wire is wrapped:

// confirm shape against AppEngine: src/repositories/repository.controller.ts
interface BaseModel\<T\> {
  pk: string;          // partition key (usually orgId-scoped)
  sk: string;          // sort key — the record's stable id
  name?: string;       // unique-within-datatype name
  datatype: string;    // collection name, e.g. "product", "contact"
  data: T;             // YOUR fields live here
  version: number;     // monotonic; increments on update
  state?: string;      // draft | published | archived | deleted | ...
  createdate?: string;
  modifydate?: string;
  author?: string;
  owner?: { id: string; email?: string; name?: string; datatype?: string };
}

Two rules:

  1. Your fields go inside data. record.data.email, never record.email.
  2. For new records, leave pk and sk empty and set isNew: true — the server fills them in.

Endpoints at a glance

GET/repository/get/{datatype}/{id}JWT+APIKEY
GET/repository/find-any-id/{datatype}/{id}?en=trueJWT+APIKEY
GET/repository/find-page-data/{datatype}JWT+APIKEY
POST/repository/find/{datatype}JWT+APIKEY
POST/repository/search/{datatype}JWT+APIKEY
PUT/repository/createJWT+APIKEY
POST/repository/createJWT+APIKEY
POST/repository/update/{id}JWT+APIKEY
POST/repository/update-partial/{datatype}/{id}JWT+APIKEY
DELETE/repository/delete/{datatype}/{id}JWT+APIKEY
POST/repository/delete/{datatype}JWT+APIKEY
POST/repository/aggregate/{datatype}JWT+APIKEY

PUT /create and POST /create are equivalent — the controller registers both verbs. POST /delete/{datatype} accepts a JSON array of ids for bulk delete.

Read

Get one by id

// from base-app/src/lib/repository-api.ts
const path = `repository/get/${datatype}/${id}`;
const response = await client.processRequest('get', path);
const record = response?.data; // BaseModel\<T\>

find-any-id is a tolerant variant: it tries sk, name, slug, and a few other keys before giving up. Pass ?en=true to inflate referenced records (e.g. category, owner).

List with filters

find-page-data is GET, takes attribute filters in query params, and returns a paged envelope:

// from base-app/src/lib/repository-api.ts
const params = new URLSearchParams();
params.append('p', '1');
params.append('ps', '20');
params.append('s', 'modifydate');
params.append('st', 'desc');
params.append('keyword', 'mug');
const res = await client.processRequest('get',
  `repository/find-page-data/product?${params.toString()}`);
// res = { data: BaseModel\<T\>[], total, hasNext }

Query with a body (Mongo-style)

find is POST and takes a { query, options } body. Use this when filters won't fit in a URL or when you want operator syntax.

const res = await client.processRequest('post', `repository/find/product`, {
  query: { 'data.price': { $lte: 50 }, 'data.stock': { $gt: 0 } },
  options: { page: 1, pageSize: 20, sort: 'data.price', sortType: 'asc' },
});

See MongoDB operators for the full operator list.

Create

POST /repository/create accepts a BaseModel\<T\> shaped body. Set isNew: true and leave pk/sk empty.

const newRecord = {
  pk: '',
  sk: '',
  name: '',
  datatype: 'product',
  data: { title: 'Wireless Mouse', price: 29.99, stock: 150 },
  isNew: true,
  version: 0,
};
const created = await client.processRequest('post', 'repository/create', newRecord);

There is also PUT /repository/create-extended for richer creation flows that need to seed comments, schedules, or activities in one round-trip. Most integrators don't need it.

Update

There are two flavors:

  • POST /repository/update/{id} — full-record update. Send back the whole BaseModel\<T\> you fetched, with data modified. The server bumps version.
  • POST /repository/update-partial/{datatype}/{id} — patch a subset of data fields. Only the keys you send are merged.
FieldTypeDescription
data*object

The full data payload (full update) or only the fields to merge (partial update).

versionnumber

Optimistic concurrency. If supplied and stale, the server returns 409.

// partial update (recommended)
await client.processRequest(
  'post',
  `repository/update-partial/product/${id}`,
  { data: { price: 24.99 } }
);

Delete

// soft delete — moves the record to trash and bumps state to "deleted"
await client.processRequest('delete', `repository/delete/product/${id}`);

// bulk
await client.processRequest('post', `repository/delete/product`, [id1, id2, id3]);

Records are soft-deleted by default — they go to trash and can be restored via POST /repository/trash-restore. To hard-delete a whole collection, ConfigAdmin can call DELETE /repository/truncate/{datatype}.

Workflow verbs

The controller also exposes lifecycle actions on top of CRUD:

POST/repository/publish/{datatype}/{id}JWT
POST/repository/unpublish/{datatype}/{id}JWT
GET/repository/request-approval/{datatype}/{id}JWT
POST/repository/approve/{datatype}/{id}JWT
POST/repository/reject/{id}JWT
POST/repository/trash-restoreJWT

These move the state field on the record and emit corresponding events. They're used by the CMS approval workflow but available on any datatype.

Domain controllers vs the generic surface

When a domain has its own controller (storefront, CRM, banking, etc.), prefer it. Domain controllers run business logic — pricing, inventory holds, KYC checks — that the generic CRUD bypasses. Use /repository/* for collections that don't have a domain, and for admin tooling that needs raw access.

Aliases under /data/*

Some clients call the same surface under /repository/get/{collection}/{id} shorthand. Internally these route to the same RepositoryController handlers — pick whichever your codebase uses and stay consistent.