# Document store

Source: https://apigenie.pl/docs/reference/server/store
Part of the reference of [apigenie-server](https://apigenie.pl/docs/reference/server.md).
Its other pages: [Responses](https://apigenie.pl/docs/reference/server/responses.md), [Scenarios and extensions](https://apigenie.pl/docs/reference/server/scenarios.md), [Scripts](https://apigenie.pl/docs/reference/server/scripts.md), [Several services](https://apigenie.pl/docs/reference/server/services.md), [Validation and security](https://apigenie.pl/docs/reference/server/validation.md), [Contract testing (validating proxy)](https://apigenie.pl/docs/reference/server/contract-testing.md), [Session recording and replay](https://apigenie.pl/docs/reference/server/recording.md), [AsyncAPI](https://apigenie.pl/docs/reference/server/asyncapi.md), [Protobuf / gRPC](https://apigenie.pl/docs/reference/server/grpc.md), [GraphQL](https://apigenie.pl/docs/reference/server/graphql.md), [Configuration](https://apigenie.pl/docs/reference/server/configuration.md), [Dashboard and admin API](https://apigenie.pl/docs/reference/server/dashboard.md), [TLS](https://apigenie.pl/docs/reference/server/tls.md).

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`.

```js
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](https://www.rfc-editor.org/rfc/rfc7396) (`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](#crud-resources) 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](/docs/reference/server/services) 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.

```yaml
paths:
  /shipments:
    x-apigenie-mock: {resource: {name: shipments, schema: Shipment}}
    get: ...
  /shipments/{id}:
    get: ...
```
