Document store

Scripts keep documents in collections: ctx.db.<collection>, made the first time a script names it. Writes are kept in memory for the run (see --db-file), shared by every script and shown by the admin API’s /__apigenie/db.

export default {
  onRequest(req, ctx) {
    if (req.method === 'POST' && req.path === '/notes') {
      return { status: 201, body: ctx.db.notes.insert(req.body) };
    }
    if (req.path === '/notes') {
      return { body: ctx.db.notes.find({ tags: { $contains: 'home' } }, { sort: '-id', limit: 10 }) };
    }
  },
};
Method Returns
insert(document) The document as stored, with its id.
get(id) The document, or undefined. "3" finds 3.
find(where, {limit, offset, sort}) An array of documents.
count(where) A number.
all() Every document, in the order they were inserted.
update(id, patch) The document after a JSON merge patch (null removes a property), or undefined for an unknown id. The id stays.
replace(id, document) The new document with the old id, or undefined.
delete(id) true when there was such a document.
clear() How many documents went.

ctx.db.collections() lists the collections and ctx.db.reset() (reset('notes') for one) puts them back to their seeds.

  • Queries. where is an object of fields and values, all of which must match: {status: 'open'}, {'address.city': 'Kraków'} (a path), or operators: $eq, $ne, $in (an array of values), $gt, $gte, $lt, $lte (numbers and strings), $contains (a substring, or an item of an array): {weight: {$gte: 5, $lt: 20}}. Numbers and strings are never equal. sort is 'field', '-field' (descending), 'a,-b', an array of them or [{field, descending}]; missing values sort last. Anything fancier is a JavaScript filter on a limited result.
  • Ids. A document keeps its own id (a string or an integer), else gets the next one: 1, 2, 3… ("1", "2"… for a resource whose id is a string, a UUID made from the run’s --seed for format: uuid). An id in use is refused, and the script’s call throws.
  • Schemas. The collection of a resource is bound to its schema: a new document gets the readOnly properties it lacks (generated, like createdAt), and every write is checked. A document that doesn’t match is kept and reported in the log, or refused (422 for a request, a throw for a script) when validation.response is enforce (--strict).
  • Seeds. db.seed names a file of documents (a JSON or YAML array) per collection, or a directory of them (seed/shipments.json seeds shipments). A resource’s collection without one is seeded from its list operation’s example, else with db.generate (3) documents generated with the run’s seed. ctx.db.reset() and POST /__apigenie/db/reset go back to the seeds, not to nothing. A seed that doesn’t match the schema is kept and reported at the start; a file that can’t be read stops the server with exit status 2.
  • Tests. A request with an X-Apigenie-Test: <id> header reads and writes a namespace of its own, which starts as a copy of the seeds: parallel test runs don’t see each other’s documents. The admin API’s /db endpoints take the same header (or ?test=<id>).
  • Snapshots. PUT /__apigenie/db/snapshots/{name} copies the documents of every collection, POST /__apigenie/db/snapshots/{name}/restore puts them back.
  • Persistence. In memory by default, so a restart is a clean slate. With --db-file (or db.file), the store is read from that JSON file at the start (a file that can’t be read is reported, and the store starts empty) and written there when the server stops or on POST /__apigenie/db/save. The file holds formatVersion 1, the collections’ seeds, the documents of each namespace and the snapshots. Seed files don’t replace what the file holds.
  • Log. Every write is a db event: the collection, insert 3, update 3, delete 3, clear 2 documents, reset, restore <name>, seed 3 documents from file, who wrote (script:mock.mjs, admin) and the test’s namespace (test). onMessage and onGraphQL use the default namespace.
  • After a reload. The documents stay; collections are bound again, new ones seeded, and documents that no longer match their schema are reported.
  • Several services. Each service has a store of its own, which its scripts’ ctx.db and its resources use; db.seed seeds every service’s collections, and the admin API’s /db endpoints take ?service= when several could be meant. --db-file (and db.file) keep the store of a server of one document; with several services it exits with status 2.
  • Limits. A collection keeps up to db.max_documents (10,000) documents per namespace. The store is not a database: no transactions, relations or durability promises.

CRUD resources

With db.crud: auto (or --crud), a path and the path of its items, such as /shipments and /shipments/{id}, make a resource whose collection (shipments) its operations read and write; no script is needed. Without it (db.crud: explicit, the default), only the paths an x-apigenie-mock: {resource: ...} extension names are resources; db.crud: off serves none.

Operation Answer
GET /shipments The documents, paged and filtered by the operation’s query parameters.
POST /shipments The document stored, with its id and readOnly properties; 201 when the operation declares it.
GET /shipments/{id} The document, or 404.
PUT /shipments/{id} The document replaced (the id and readOnly properties stay), or 404.
PATCH /shipments/{id} The document after a JSON merge patch, or 404.
DELETE /shipments/{id} 204 when the operation declares it, else the document that went; 404 for an unknown id.
  • The schema is the list response’s items, or the items of its object’s array property (an envelope such as {data: [...], total: 3}: one array property, or the one called data, items, results or like the collection), else the item response’s or the request body’s. The id is the schema’s id property, else the path parameter when it is a property; integers count, strings with format: uuid are UUIDs. Answers leave writeOnly properties out.
  • Lists page by the list operation’s parameters: offset and limit, page (from 1) and per_page (page_size, pageSize, size, limit), or cursor (after, page_token) and a size, the size’s default when not given (20 without one). An envelope gets the page in its array property, and total (totalCount, total_count…), hasMore, nextCursor, page, offset and limit filled in when it declares them; its other members are generated. Other query parameters named like a property of the documents filter by it (?status=open), and sort (order_by) sorts: -createdAt, status:desc,id.
  • What comes first. Requests are validated before the store sees them (422 under validation.request: enforce). A request’s Prefer (or __ parameters), a scenario’s or an override’s rule, an extension that sets the status, an example or a body, and stubs answer instead of the store. The store’s answers are validated like any other, and say X-Apigenie-Mock-Source: db.
  • The extension, on a path item or an operation of either path: resource: shipments (the collection’s name), resource: false (not a resource, also with db.crud: auto) or resource: {name: parcels, id: code, schema: Parcel, list: entries} (the id property, the schema under components/schemas or as a #/ reference, the envelope’s array property).
  • Proxies. A document served with --proxy-to has no resources: the server’s state is the upstream’s.
paths:
  /shipments:
    x-apigenie-mock: {resource: {name: shipments, schema: Shipment}}
    get: ...
  /shipments/{id}:
    get: ...
Was this helpful?