json-rest-api 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/docs/API.md +262 -54
- package/docs/GUIDE/01-getting-started.md +1 -1
- package/docs/GUIDE/07-polymorphic-relationships.md +4 -2
- package/docs/GUIDE/09-pagination-and-sorting.md +4 -2
- package/docs/GUIDE/13-hooks-and-lifecycle.md +34 -6
- package/docs/GUIDE/14-custom-resource-methods.md +3 -2
- package/docs/GUIDE/16-autofiltering.md +54 -2
- package/docs/GUIDE/17-row-policies.md +15 -5
- package/docs/GUIDE/19-managed-transactions.md +8 -9
- package/docs/GUIDE/20-transaction-outcomes.md +3 -4
- package/docs/GUIDE/21-schema-and-migrations.md +7 -1
- package/docs/GUIDE/22-version-field-migrations.md +3 -3
- package/docs/GUIDE/27-many-row-writes.md +458 -0
- package/docs/GUIDE/28-socketio.md +10 -2
- package/docs/GUIDE/30-backend-capabilities.md +42 -2
- package/docs/GUIDE/31-positioning.md +3 -1
- package/docs/GUIDE/33-migrating-to-v2.md +2 -2
- package/docs/GUIDE/34-scoped-reads-and-locks.md +464 -0
- package/docs/GUIDE/35-migrating-to-v3.md +325 -0
- package/docs/GUIDE/index.md +5 -2
- package/docs/QUICKSTART.md +2 -2
- package/docs/contributing.md +1 -1
- package/index.d.ts +8 -1
- package/index.js +4 -0
- package/lib/rest-api-errors.js +79 -0
- package/lib/runtime/json-rest-api.js +18 -3
- package/lib/system-context.js +42 -0
- package/package.json +3 -2
- package/plugins/core/bulk-operations-plugin.js +54 -436
- package/plugins/core/connectors/lib/transport-http-helpers.js +34 -3
- package/plugins/core/file-handling-plugin.js +17 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +3 -2
- package/plugins/core/lib/anyapi/canonical-link-store.js +5 -2
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +56 -60
- package/plugins/core/lib/dbIntrospection.js +106 -6
- package/plugins/core/lib/querying/aggregate-query.js +215 -22
- package/plugins/core/lib/querying/include-query-helpers.js +11 -13
- package/plugins/core/lib/querying/include-to-one.js +54 -59
- package/plugins/core/lib/querying/knex-pagination-helpers.js +9 -2
- package/plugins/core/lib/querying/query-constraint.d.ts +11 -0
- package/plugins/core/lib/querying/query-constraint.js +45 -0
- package/plugins/core/lib/querying/read-only-query.js +92 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +14 -0
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +40 -3
- package/plugins/core/lib/storage/canonical-data-helpers.js +103 -39
- package/plugins/core/lib/storage/many-row-data-helpers.js +738 -0
- package/plugins/core/lib/storage/ordinary-data-helpers.js +63 -79
- package/plugins/core/lib/storage/single-row-writes.js +71 -0
- package/plugins/core/lib/storage/storage-adapter.js +18 -8
- package/plugins/core/lib/storage/storage-mapping.js +4 -1
- package/plugins/core/lib/storage/storage-types.d.ts +21 -3
- package/plugins/core/lib/writing/many-to-many-manipulations.js +10 -5
- package/plugins/core/lib/writing/relationship-processor.js +33 -37
- package/plugins/core/rest-api-anyapi-knex-plugin.js +3 -0
- package/plugins/core/rest-api-autofilter-plugin.js +137 -10
- package/plugins/core/rest-api-knex-plugin.js +2 -0
- package/plugins/core/rest-api-plugin-methods/aggregate.js +3 -1
- package/plugins/core/rest-api-plugin-methods/common.js +144 -84
- package/plugins/core/rest-api-plugin-methods/delete-many.js +132 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +22 -0
- package/plugins/core/rest-api-plugin-methods/get.js +48 -42
- package/plugins/core/rest-api-plugin-methods/many-row-common.js +274 -0
- package/plugins/core/rest-api-plugin-methods/patch-many.js +333 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -1
- package/plugins/core/rest-api-plugin-methods/post-many.js +295 -0
- package/plugins/core/rest-api-plugin-methods/put.js +5 -1
- package/plugins/core/rest-api-plugin-methods/scoped-reads.js +221 -0
- package/plugins/core/rest-api-plugin.js +17 -0
- package/plugins/core/rest-api-positioning-plugin.js +22 -4
- package/plugins/core/rest-api-query-projections-plugin.js +9 -1
- package/plugins/core/rest-api-row-policy-plugin.js +4 -0
- package/plugins/core/socketio-plugin.js +80 -2
- package/types/autofilter.d.ts +5 -1
- package/types/data-access-methods.d.ts +141 -0
- package/types/errors.d.ts +45 -0
- package/types/hook-context.d.ts +21 -0
- package/types/plugin-options.d.ts +3 -1
- package/types/resource-methods.d.ts +27 -4
- package/types/row-policy.d.ts +3 -0
- package/types/runtime.d.ts +7 -1
- package/docs/GUIDE/27-bulk-operations.md +0 -277
- package/types/bulk-methods.d.ts +0 -63
package/README.md
CHANGED
|
@@ -73,8 +73,13 @@ app.listen(3000, () => {
|
|
|
73
73
|
|
|
74
74
|
## Documentation
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
Version 3 adds many-row writes (`postMany`, `patchMany`, `patchWhereIds`,
|
|
77
|
+
`patchWhere`, `deleteMany`, `deleteWhere`), scoped reads (`scopedQuery`,
|
|
78
|
+
`queryAll`), row locks (`lockByIds`, `lockWhere`, `claim`), lock versions, shared
|
|
79
|
+
rows and system contexts, and replaces the 2.x bulk methods. The
|
|
80
|
+
[v3 migration guide](docs/GUIDE/35-migrating-to-v3.md) lists the breaking changes
|
|
81
|
+
with old → new tables; the [v2 migration guide](docs/GUIDE/33-migrating-to-v2.md)
|
|
82
|
+
covers upgrades from 1.x.
|
|
78
83
|
|
|
79
84
|
|
|
80
85
|
- [Quick Start](docs/QUICKSTART.md) - Quick start
|
package/docs/API.md
CHANGED
|
@@ -8,7 +8,8 @@ This reference provides comprehensive documentation for all methods, parameters,
|
|
|
8
8
|
|
|
9
9
|
## Current contract
|
|
10
10
|
|
|
11
|
-
This reference describes the
|
|
11
|
+
This reference describes the v3 API. See the migration guides
|
|
12
|
+
([to v2](GUIDE/33-migrating-to-v2.md), [to v3](GUIDE/35-migrating-to-v3.md)) and
|
|
12
13
|
[backend limits](GUIDE/30-backend-capabilities.md) before upgrading.
|
|
13
14
|
|
|
14
15
|
Pass method controls in the first `params` object and application information in
|
|
@@ -60,8 +61,10 @@ compound writes and JSON:API extensions are not enabled by accepting a document.
|
|
|
60
61
|
On a resource configured with `versionField`, PUT/PATCH/DELETE and relationship
|
|
61
62
|
writes accept an `expectedVersion` string; unconditional writes may omit it.
|
|
62
63
|
POST does not accept that condition. See [optimistic concurrency](GUIDE/33-migrating-to-v2.md)
|
|
63
|
-
for version-field configuration
|
|
64
|
-
|
|
64
|
+
for version-field configuration and the separate opt-in HTTP validator
|
|
65
|
+
contract. Many-row writes refuse versioned resources; a
|
|
66
|
+
[`lockVersionField`](GUIDE/34-scoped-reads-and-locks.md#lock-versions) with
|
|
67
|
+
`onlyIf` gives them compare-and-set instead.
|
|
65
68
|
|
|
66
69
|
Read calls can borrow raw Knex transactions. Library writes must use the handle
|
|
67
70
|
from `api.transaction` when grouping operations; they reject unmanaged raw
|
|
@@ -79,25 +82,31 @@ a separate context for each enlisted operation. See [transaction support](#trans
|
|
|
79
82
|
- [PATCH - Update Resource](#patch---update-resource)
|
|
80
83
|
- [DELETE - Remove Resource](#delete---remove-resource)
|
|
81
84
|
- [AGGREGATE - Grouped Totals](#aggregate---grouped-totals)
|
|
82
|
-
3. [
|
|
85
|
+
3. [Data-Access Methods](#data-access-methods)
|
|
86
|
+
- [scopedQuery - Scoped Read-Only Builder](#scopedquery---scoped-read-only-builder)
|
|
87
|
+
- [queryAll - Every Matching Record](#queryall---every-matching-record)
|
|
88
|
+
- [Many-Row Writes](#many-row-writes)
|
|
89
|
+
- [lockByIds, lockWhere and claim - Row Locks](#lockbyids-lockwhere-and-claim---row-locks)
|
|
90
|
+
- [System Contexts](#system-contexts)
|
|
91
|
+
4. [Relationship Methods](#relationship-methods)
|
|
83
92
|
- [getRelated - Retrieve Related Resources](#getrelated---retrieve-related-resources)
|
|
84
93
|
- [getRelationship - Retrieve Relationship Identifiers](#getrelationship---retrieve-relationship-identifiers)
|
|
85
94
|
- [postRelationship - Add to Relationship](#postrelationship---add-to-relationship)
|
|
86
95
|
- [patchRelationship - Replace Relationship](#patchrelationship---replace-relationship)
|
|
87
96
|
- [deleteRelationship - Remove from Relationship](#deleterelationship---remove-from-relationships)
|
|
88
|
-
|
|
97
|
+
5. [Hook System](#hook-system)
|
|
89
98
|
- [Complete Hook Execution Order](#complete-hook-execution-order)
|
|
90
99
|
- [Hook Context Objects](#hook-context-objects)
|
|
91
|
-
|
|
100
|
+
6. [Query Features](#query-features)
|
|
92
101
|
- [Filtering](#filtering)
|
|
93
102
|
- [Sorting](#sorting)
|
|
94
103
|
- [Pagination](#pagination)
|
|
95
104
|
- [Sparse Fieldsets](#sparse-fieldsets)
|
|
96
105
|
- [Including Related Resources](#including-related-resources)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
7. [Configuration Options](#configuration-options)
|
|
107
|
+
8. [Schema Configuration](#schema-configuration)
|
|
108
|
+
9. [Error Handling](#error-handling)
|
|
109
|
+
10. [Advanced Features](#advanced-features)
|
|
101
110
|
|
|
102
111
|
---
|
|
103
112
|
|
|
@@ -1154,8 +1163,8 @@ await api.resources.sales.aggregate(params, context)
|
|
|
1154
1163
|
|
|
1155
1164
|
| Parameter | Type | Description |
|
|
1156
1165
|
|-----------|------|-------------|
|
|
1157
|
-
| `groupBy` | string[] | Fields to group by
|
|
1158
|
-
| `measures` | object | Output names mapped to `{ count \| countDistinct \| sum \| avg \| min \| max: field }`. Only `count` accepts `'*'`. |
|
|
1166
|
+
| `groupBy` | (string \| object)[] | Fields to group by, or date buckets `{ field, bucket, timezone, as }`. Omit for one row over all selected records. |
|
|
1167
|
+
| `measures` | object | Output names mapped to `{ count \| countDistinct \| sum \| avg \| min \| max: field, where? }`. Only `count` accepts `'*'`. |
|
|
1159
1168
|
| `queryParams.filters` | object | Search schema filters, validated as for `query`. |
|
|
1160
1169
|
| `queryParams.sort` | string[] | Grouped field or measure names, `-` for descending. Grouped fields break ties. |
|
|
1161
1170
|
| `queryParams.page` | object | `size` and `number`, for grouped results only. `size` defaults to `queryDefaultLimit` and cannot exceed `queryMaxLimit`. |
|
|
@@ -1165,7 +1174,56 @@ await api.resources.sales.aggregate(params, context)
|
|
|
1165
1174
|
times. Grouping and counting accept any scalar stored field, including belongs-to
|
|
1166
1175
|
foreign keys. Fields that are `hidden`, virtual, computed, or that have a getter or
|
|
1167
1176
|
`storage.serialize` are rejected, because SQL would group or sum values other than
|
|
1168
|
-
the ones clients read. `
|
|
1177
|
+
the ones clients read. The exception is a `date` or `dateTime` field with
|
|
1178
|
+
`storage.serialize`: it may be bucketed and take `min` and `max`, whose results
|
|
1179
|
+
are read back as that field's values. `normallyHidden` fields are allowed.
|
|
1180
|
+
In a [system context](GUIDE/34-scoped-reads-and-locks.md#system-contexts), a
|
|
1181
|
+
hidden ownership (autofilter) field may also be grouped and take
|
|
1182
|
+
`countDistinct`, so a job can find which owners have rows; other callers are
|
|
1183
|
+
refused it, since one whose optional owner filter does not resolve would see
|
|
1184
|
+
every owner.
|
|
1185
|
+
|
|
1186
|
+
#### Conditional measures
|
|
1187
|
+
|
|
1188
|
+
A measure's `where` limits the rows it counts or adds up, so one call can
|
|
1189
|
+
return "this week" next to "this month". Every entry must hold:
|
|
1190
|
+
|
|
1191
|
+
```javascript
|
|
1192
|
+
measures: {
|
|
1193
|
+
open: { count: '*', where: { status: 'open' } },
|
|
1194
|
+
bigOpen: { sum: 'amount', where: { status: 'open', amount: { gte: 1000 } } },
|
|
1195
|
+
late: { count: '*', where: { status: ['overdue', 'disputed'] } },
|
|
1196
|
+
unassigned: { count: '*', where: { owner_id: null } }
|
|
1197
|
+
}
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
A condition is a value, an array (IN), `null`, or an object of `eq`, `ne`,
|
|
1201
|
+
`gt`, `gte`, `lt`, `lte`, `in` and `notIn`. Fields may be any stored scalar field
|
|
1202
|
+
aggregates accept; values are translated as filter values are. The SQL is
|
|
1203
|
+
`fn(CASE WHEN … THEN value END)`, so conditional and plain measures share one
|
|
1204
|
+
statement.
|
|
1205
|
+
|
|
1206
|
+
#### Date buckets
|
|
1207
|
+
|
|
1208
|
+
A `groupBy` entry `{ field, bucket, timezone, as }` groups a `date` or
|
|
1209
|
+
`dateTime` field by `'day'`, `'week'`, `'month'` or `'year'`. The group value
|
|
1210
|
+
is the `'YYYY-MM-DD'` of the bucket's first day; weeks start on Monday. The
|
|
1211
|
+
output name is `as`, default the field name.
|
|
1212
|
+
|
|
1213
|
+
```javascript
|
|
1214
|
+
await api.resources.sales.aggregate({
|
|
1215
|
+
groupBy: [{ field: 'closedAt', bucket: 'month', timezone: 'Europe/Rome', as: 'month' }],
|
|
1216
|
+
measures: { total: { sum: 'amount' } },
|
|
1217
|
+
queryParams: { sort: ['month'] }
|
|
1218
|
+
}, context)
|
|
1219
|
+
// { data: [{ month: '2026-01-01', total: 1250 }, ...] }
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
`timezone` is `'UTC'` (default), a fixed offset such as `'-09:00'`, or an IANA
|
|
1223
|
+
name. PostgreSQL has zone data; MySQL and MariaDB need their time zone tables
|
|
1224
|
+
loaded (checked once per name); SQLite takes offsets only. `date` fields have
|
|
1225
|
+
no time of day and take no time zone. Buckets need table storage; AnyAPI
|
|
1226
|
+
resources refuse them.
|
|
1169
1227
|
|
|
1170
1228
|
#### Return Value
|
|
1171
1229
|
|
|
@@ -1201,6 +1259,145 @@ const totals = await api.resources.sales.aggregate({
|
|
|
1201
1259
|
|
|
1202
1260
|
---
|
|
1203
1261
|
|
|
1262
|
+
## Data-Access Methods
|
|
1263
|
+
|
|
1264
|
+
Methods for work that does not fit one page or one record: reports, imports and
|
|
1265
|
+
jobs. They keep the caller's ownership filters (autofilter) and row policies.
|
|
1266
|
+
The guides have the details:
|
|
1267
|
+
[many-row writes](GUIDE/27-many-row-writes.md) and
|
|
1268
|
+
[scoped reads, locks and system work](GUIDE/34-scoped-reads-and-locks.md).
|
|
1269
|
+
Many-row writes and locks need table storage (`RestApiKnexPlugin`); AnyAPI
|
|
1270
|
+
resources refuse them with `RestApiBulkWriteUnsupportedError`. `queryAll` and
|
|
1271
|
+
`scopedQuery` work on both storages; on AnyAPI the scoped builder reads the
|
|
1272
|
+
canonical rows, whose fields live in storage slots.
|
|
1273
|
+
|
|
1274
|
+
### scopedQuery - Scoped Read-Only Builder
|
|
1275
|
+
|
|
1276
|
+
```javascript
|
|
1277
|
+
const { query } = await api.resources.entries.scopedQuery(
|
|
1278
|
+
{ filters, alias, purpose, transaction }, context)
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
Resolves to `{ query }`, a Knex builder over a derived table of the resource's
|
|
1282
|
+
rows with ownership filters, row policy and the search `filters` applied, so
|
|
1283
|
+
conditions the caller adds (`orWhere` included) only narrow them. It is wrapped
|
|
1284
|
+
because a builder is thenable. `purpose` is `'report'` (default; includes
|
|
1285
|
+
shared rows), `'bulk-write'`, `'lock'` or `'claim'`; row policies receive it as
|
|
1286
|
+
`queryPurpose`. The caller needs `query` permission. The builder refuses
|
|
1287
|
+
writes, locking reads, table changes, clearing predicates, set operations,
|
|
1288
|
+
common table expressions and access to its Knex client, on clones too
|
|
1289
|
+
(`RestApiScopedQueryReadOnlyError`); joined tables are not scoped. It guards
|
|
1290
|
+
against mistakes, not hostile code.
|
|
1291
|
+
|
|
1292
|
+
### queryAll - Every Matching Record
|
|
1293
|
+
|
|
1294
|
+
```javascript
|
|
1295
|
+
const { data, included } = await api.resources.entries.queryAll(
|
|
1296
|
+
{ queryParams, pageSize, rowLimit, format, transaction }, context)
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
Reads every record `query` would select, in cursor pages of `pageSize` rows
|
|
1300
|
+
(default 1,000) in id order or `queryParams.sort`. `queryParams` takes
|
|
1301
|
+
`filters`, `sort`, `fields` and `include`, but not `page`. Past `rowLimit` rows
|
|
1302
|
+
it throws `RestApiRowLimitError`. Works on both storages.
|
|
1303
|
+
|
|
1304
|
+
### Many-Row Writes
|
|
1305
|
+
|
|
1306
|
+
```javascript
|
|
1307
|
+
await resource.postMany({ data | document, onConflict, returning, format, queryParams, chunkSize, transaction }, context)
|
|
1308
|
+
await resource.patchMany({ data | document, onlyIf, missing, returning, format, queryParams, chunkSize, transaction }, context)
|
|
1309
|
+
await resource.patchWhereIds({ ids, data | document, onlyIf, missing, returning, format, queryParams, chunkSize, transaction }, context)
|
|
1310
|
+
await resource.patchWhere({ filters | all: true, data | document, onlyIf, returning, format, queryParams, chunkSize, transaction }, context)
|
|
1311
|
+
await resource.deleteMany({ ids, missing, returning, chunkSize, transaction }, context)
|
|
1312
|
+
await resource.deleteWhere({ filters | all: true, returning, chunkSize, transaction }, context)
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
| Parameter | Description |
|
|
1316
|
+
| --- | --- |
|
|
1317
|
+
| `data` / `document` | `postMany`, `patchMany`: an array of plain records, or `{ data: [resource objects] }`; `patchMany` rows carry `id`. `patchWhereIds`, `patchWhere`: one record of changes. |
|
|
1318
|
+
| `ids` / `filters` / `all` | The records to change: listed ids, or non-empty search filters validated as `query` validates them, or `all: true` for every row the caller can see. |
|
|
1319
|
+
| `returning` | `'none'` (default), `'minimal'` (`{ data: [{ type, id }] }`), `'full'` (`{ data: [records] }`, not for deletes) or `'count'` (`{ meta: { count } }`, the number of rows written, with no row data). |
|
|
1320
|
+
| `format`, `queryParams` | Output format, `fields` and `include` of full records. |
|
|
1321
|
+
| `onConflict` | Upsert: `{ keys, action: 'ignore' \| 'merge', merge? }`. `keys` must include every ownership field, and a unique index must cover them; rows may not carry an `id` or repeat keys. On a resource with no ownership fields and no row policy, `keys: ['id']` (or the `idProperty`) keys the upsert on the id, and every row carries one; keys compare exactly (a match only by collation fails with 409); only a match on the keys counts, and a clash on another unique index fails the batch. `merge` updates only the fields each row supplies (never `defaultTo` values), runs the patch checks (`checkPermissions` with `method: 'patch'`) on the records it updates and is refused under a row policy. |
|
|
1322
|
+
| `onlyIf` | Compare-and-set: only rows whose stored values equal these change and are returned. |
|
|
1323
|
+
| `missing` | Id lists only: `'throw'` (default; `RestApiRecordsNotFoundError`, 404) or `'skip'` for ids the caller cannot see. `patchWhere` and `deleteWhere` refuse it: they write the rows their selection finds, with the scope and filters inside each write, and return only the rows written. |
|
|
1324
|
+
| `chunkSize` | Rows per statement (default 500), lowered to the database's parameter limit. |
|
|
1325
|
+
| `transaction` | A transaction from `api.transaction` to join; otherwise the call owns one. |
|
|
1326
|
+
|
|
1327
|
+
Each call is all-or-nothing. Every row is validated, stamped, passed through
|
|
1328
|
+
setters and authorized with `checkPermissions` (`method` `post`, `patch` or
|
|
1329
|
+
`delete`, `originalContext.bulkIndex` the row's position); belongs-to targets
|
|
1330
|
+
are checked with one read per related type; then one statement runs per chunk.
|
|
1331
|
+
Patches refuse the id, ownership fields (also through a polymorphic
|
|
1332
|
+
relationship) and the `lockVersionField`, and accept real `knex.raw()` values
|
|
1333
|
+
for stored, non-relationship fields. The UPDATE, DELETE and lock statements
|
|
1334
|
+
repeat the caller's ownership conditions, so a row that moved to another owner
|
|
1335
|
+
after the visibility read is left alone and counts as missing. Deletes remove
|
|
1336
|
+
the records' pivot rows. Batch hooks run once per call:
|
|
1337
|
+
`beforeProcessingPostMany`, `beforeDataCallPostMany`, `afterDataCallPostMany`,
|
|
1338
|
+
the `PatchMany` trio and `beforeDataCallDeleteMany` / `afterDataCallDeleteMany`.
|
|
1339
|
+
|
|
1340
|
+
`RestApiBulkWriteUnsupportedError` (422) is thrown before writing for AnyAPI
|
|
1341
|
+
storage, versioned resources (and those whose writes advance a version),
|
|
1342
|
+
per-record write hooks not registered with `{ batch: true }`, rows that set
|
|
1343
|
+
many-to-many or reverse relationships, file fields, or positions.
|
|
1344
|
+
`BulkOperationsPlugin` exposes `postMany`, `patchMany` and `deleteMany` as
|
|
1345
|
+
`POST`, `PATCH` and `DELETE /RESOURCE/bulk`.
|
|
1346
|
+
|
|
1347
|
+
### lockByIds, lockWhere and claim - Row Locks
|
|
1348
|
+
|
|
1349
|
+
```javascript
|
|
1350
|
+
await api.transaction(async transaction => {
|
|
1351
|
+
const { data: locked } = await resource.lockByIds({ ids, transaction, missing, format, queryParams: { fields } }, context)
|
|
1352
|
+
const { data: matching } = await resource.lockWhere({ filters, transaction, format, queryParams: { fields } }, context)
|
|
1353
|
+
const { data: taken } = await resource.claim({ filters, sort, limit, transaction, format, queryParams: { fields } }, context)
|
|
1354
|
+
})
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
All three need a transaction (`RestApiLockOutsideTransactionError` otherwise)
|
|
1358
|
+
and return the records as the locking read saw them, through the read hooks of
|
|
1359
|
+
`query` (`enrichRecord`, `finish`, `finishQuery`) and `checkDataPermissions`
|
|
1360
|
+
for each row. The locking read is the first statement and applies the scope
|
|
1361
|
+
itself, so plain reads later in the transaction see what a previous lock holder
|
|
1362
|
+
committed. `lockByIds` locks the visible rows among `ids` in id order
|
|
1363
|
+
(`FOR UPDATE`), sorting lists longer than its 1,000-id statements first; hidden
|
|
1364
|
+
or missing ids throw `RestApiRecordsNotFoundError` unless `missing: 'skip'`. On
|
|
1365
|
+
MySQL and MariaDB it locks every listed id, hidden ones too, until commit, so
|
|
1366
|
+
pass ids from a scoped read, never a raw request list. `lockWhere` locks the
|
|
1367
|
+
visible rows the filters select in id order, waiting for other transactions and
|
|
1368
|
+
including rows that start to match meanwhile; more than 1,000 throw
|
|
1369
|
+
`RestApiRowLimitError`. On MySQL and MariaDB it has no `ORDER BY` and forces an
|
|
1370
|
+
index that starts with the whole ownership columns (or, without ownership
|
|
1371
|
+
conditions, a filtered column), and throws `RestApiBulkWriteUnsupportedError`
|
|
1372
|
+
when the table has none.
|
|
1373
|
+
`claim` takes up to `limit` (default 1, at most 1,000) matching rows that no
|
|
1374
|
+
other transaction holds (`SKIP LOCKED`), in `sort` order (stored fields, default
|
|
1375
|
+
`['id']`). A row policy that joins tables is refused for all three. On SQLite,
|
|
1376
|
+
which has one writer, they take the database's write lock with a write that
|
|
1377
|
+
changes nothing, then read.
|
|
1378
|
+
|
|
1379
|
+
A resource option `lockVersionField: 'lock_version'` names a stored integer
|
|
1380
|
+
that every update (single, many-row and upsert merge) sets to `column + 1`.
|
|
1381
|
+
Callers cannot set it; use it with `onlyIf`.
|
|
1382
|
+
|
|
1383
|
+
### System Contexts
|
|
1384
|
+
|
|
1385
|
+
```javascript
|
|
1386
|
+
import { createSystemContext, isSystemContext, readOnlyQueryBuilder } from 'json-rest-api'
|
|
1387
|
+
|
|
1388
|
+
const context = createSystemContext('nightly report', { jobId })
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
A system context skips autofilter ownership predicates; creates must set every
|
|
1392
|
+
ownership field explicitly. Row policies receive `system: true`. The marker is
|
|
1393
|
+
a private symbol that object spread keeps and serialization drops; a context
|
|
1394
|
+
with `system: true` but no marker throws `REST_API_SYSTEM_CONTEXT_FORGED`.
|
|
1395
|
+
Autofilter filters may be `shared: true` (on a nullable field): reads include
|
|
1396
|
+
rows whose field is NULL, while many-row writes, locks and single writes stay
|
|
1397
|
+
on the owner's rows.
|
|
1398
|
+
|
|
1399
|
+
---
|
|
1400
|
+
|
|
1204
1401
|
## Relationship Methods
|
|
1205
1402
|
|
|
1206
1403
|
### getRelated - Retrieve Related Resources
|
|
@@ -1820,8 +2017,9 @@ const result = await api.resources.articles.query({
|
|
|
1820
2017
|
```
|
|
1821
2018
|
|
|
1822
2019
|
Pagination metadata is under `meta.pagination`. With counts disabled, it includes
|
|
1823
|
-
`page` and `
|
|
1824
|
-
|
|
2020
|
+
`page`, `pageSize` and `hasMore`, without totals: the page reads one extra row
|
|
2021
|
+
instead of running a count query, so it is one statement and links to `first`,
|
|
2022
|
+
`prev` and `next` but not `last`. The library supplies pagination links appropriate
|
|
1825
2023
|
to the available count/boundary information.
|
|
1826
2024
|
|
|
1827
2025
|
#### Cursor Pagination
|
|
@@ -1939,6 +2137,27 @@ purpose. See [row policies](GUIDE/17-row-policies.md),
|
|
|
1939
2137
|
[include configuration](GUIDE/33-migrating-to-v2.md#include-configuration-is-validated-before-publication)
|
|
1940
2138
|
and the query hook contract above.
|
|
1941
2139
|
|
|
2140
|
+
#### Per-Parent Limits (Window Includes)
|
|
2141
|
+
|
|
2142
|
+
A to-many relationship can include only the first few related records of each
|
|
2143
|
+
parent, such as the newest snapshot per booking. Configure it on the
|
|
2144
|
+
relationship, not in the query:
|
|
2145
|
+
|
|
2146
|
+
```javascript
|
|
2147
|
+
relationships: {
|
|
2148
|
+
snapshots: {
|
|
2149
|
+
type: 'hasMany', target: 'snapshots', foreignKey: 'booking_id',
|
|
2150
|
+
include: { strategy: 'window', limit: 1, orderBy: ['-revision'] }
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
```
|
|
2154
|
+
|
|
2155
|
+
`include: ['snapshots']` then returns at most `limit` visible snapshots per
|
|
2156
|
+
booking, ordered by `orderBy`, in one query. The default (`standard`) strategy
|
|
2157
|
+
applies one limit to the whole included set instead. Window includes need
|
|
2158
|
+
database window functions (`api.knex.capabilities.windowFunctions`). See
|
|
2159
|
+
[window includes](GUIDE/34-scoped-reads-and-locks.md#window-includes).
|
|
2160
|
+
|
|
1942
2161
|
## Configuration Options
|
|
1943
2162
|
|
|
1944
2163
|
### Plugin Configuration
|
|
@@ -2175,8 +2394,22 @@ For example, a resource not-found error maps to:
|
|
|
2175
2394
|
| `REST_API_FIELDSET_INVALID`, `REST_API_INCLUDE_INVALID` | 400 |
|
|
2176
2395
|
| `REST_API_VERSION_CONFLICT` | 409 |
|
|
2177
2396
|
| `REST_API_PRECONDITION_FAILED` | 412 |
|
|
2397
|
+
| `RestApiRecordsNotFoundError` (subtype `not_found`) | 404 |
|
|
2398
|
+
| `RestApiBulkWriteUnsupportedError` / `REST_API_BULK_WRITE_UNSUPPORTED` | 422 |
|
|
2399
|
+
| `RestApiLockOutsideTransactionError` (a validation error) | 422 |
|
|
2400
|
+
| `RestApiBulkInsertIdsError` / `REST_API_BULK_INSERT_IDS` | 500 |
|
|
2401
|
+
| `RestApiScopedQueryReadOnlyError` / `REST_API_SCOPED_QUERY_READ_ONLY` | programmatic only |
|
|
2402
|
+
| `RestApiRowLimitError` / `REST_API_ROW_LIMIT` | 500 |
|
|
2403
|
+
| `REST_API_SYSTEM_CONTEXT_FORGED` | 500 |
|
|
2178
2404
|
| Unexpected or temporal output failure | 500 |
|
|
2179
2405
|
|
|
2406
|
+
A 500 answer carries the error's message only for the library's own
|
|
2407
|
+
`RestApi…Error` classes. Any other error, a database error or a plain `Error`
|
|
2408
|
+
thrown by a hook, answers with the detail `An unexpected error occurred`: a
|
|
2409
|
+
database error's message repeats the SQL and its values. The connector's
|
|
2410
|
+
`HTTP request error` or `Fastify request error` log entry records only the
|
|
2411
|
+
`name` and `code` of a database error.
|
|
2412
|
+
|
|
2180
2413
|
Validation violations produce individual HTTP errors with their message and
|
|
2181
2414
|
field location; generic validation errors retain their available details.
|
|
2182
2415
|
Relationship validation/access failures follow their actual typed classification,
|
|
@@ -2197,15 +2430,14 @@ before owned commit; borrowed transactions remain the owner's responsibility.
|
|
|
2197
2430
|
See the [callback migration guide](GUIDE/33-migrating-to-v2.md#setter-getter-and-computed-field-failures)
|
|
2198
2431
|
for examples and intentional fallback handling.
|
|
2199
2432
|
|
|
2200
|
-
Resource, relationship,
|
|
2433
|
+
Resource, relationship, many-row and canonical registry write failures use the root
|
|
2201
2434
|
export `RestApiWriteError`. Its immutable `transactionOutcome` is `none`,
|
|
2202
2435
|
`pending`, `committed`, `rolledBack` or `unknown`. Its `cause` preserves the
|
|
2203
2436
|
original thrown value, including frozen errors, while classification fields
|
|
2204
2437
|
such as `code`, `subtype` and `details` remain directly readable. Read errors
|
|
2205
2438
|
retain their existing contract.
|
|
2206
2439
|
|
|
2207
|
-
HTTP JSON:API write errors include `meta.transactionOutcome
|
|
2208
|
-
entries include `error.transactionOutcome`. A failed call can represent a
|
|
2440
|
+
HTTP JSON:API write errors include `meta.transactionOutcome`. A failed call can represent a
|
|
2209
2441
|
committed write when a later hook rejects. Retrying that call can duplicate its
|
|
2210
2442
|
effects; neither an HTTP status nor an unknown outcome authorizes automatic
|
|
2211
2443
|
replay. See the [transaction migration guide](GUIDE/33-migrating-to-v2.md#transactions-and-errors)
|
|
@@ -2346,44 +2578,20 @@ For full examples and dialect notes, see [Knex Schema and Migrations](GUIDE/21-s
|
|
|
2346
2578
|
|
|
2347
2579
|
### Batch Operations
|
|
2348
2580
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
data,
|
|
2359
|
-
transaction,
|
|
2360
|
-
format: 'plain',
|
|
2361
|
-
returning: 'minimal'
|
|
2362
|
-
});
|
|
2363
|
-
results.push(result);
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
return results;
|
|
2367
|
-
});
|
|
2368
|
-
};
|
|
2369
|
-
|
|
2370
|
-
// Batch update
|
|
2371
|
-
const updateArticles = async (updates) => {
|
|
2372
|
-
return api.transaction(async transaction => {
|
|
2373
|
-
for (const { id, data } of updates) {
|
|
2374
|
-
await api.resources.articles.patch({
|
|
2375
|
-
id,
|
|
2376
|
-
data,
|
|
2377
|
-
transaction,
|
|
2378
|
-
format: 'plain',
|
|
2379
|
-
returning: 'none'
|
|
2380
|
-
});
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
});
|
|
2384
|
-
};
|
|
2581
|
+
Write many records with the [many-row methods](#many-row-writes): one statement
|
|
2582
|
+
per chunk, all-or-nothing, with the same validation and permissions as single
|
|
2583
|
+
writes. Group them with other calls in `api.transaction`:
|
|
2584
|
+
|
|
2585
|
+
```javascript
|
|
2586
|
+
await api.transaction(async transaction => {
|
|
2587
|
+
await api.resources.articles.postMany({ data: articlesData, transaction })
|
|
2588
|
+
await api.resources.articles.patchWhereIds({ ids: staleIds, data: { status: 'archived' }, transaction })
|
|
2589
|
+
})
|
|
2385
2590
|
```
|
|
2386
2591
|
|
|
2592
|
+
When each record needs its own hooks (or the resource refuses many-row writes),
|
|
2593
|
+
call the single-record methods in a loop inside one `api.transaction`.
|
|
2594
|
+
|
|
2387
2595
|
### Computed Fields
|
|
2388
2596
|
|
|
2389
2597
|
Declare computed fields in `schema`, with `computed: true`, `dependencies` and
|
|
@@ -12,7 +12,7 @@ related resources. The [quickstart](../QUICKSTART.md) provides a complete
|
|
|
12
12
|
Express server with related publishers and authors.
|
|
13
13
|
|
|
14
14
|
Use Node 24 or newer; development verification runs on Node 24. These examples
|
|
15
|
-
use the
|
|
15
|
+
use the v3 contract; see the [migration guide](35-migrating-to-v3.md). Until
|
|
16
16
|
its release is published, install the intended library tarball instead of the
|
|
17
17
|
registry package.
|
|
18
18
|
|
|
@@ -98,8 +98,10 @@ const authorReviewDocument = await api.resources.reviews.post({
|
|
|
98
98
|
|
|
99
99
|
Plain polymorphic input uses `{ _type, id }` under the relationship alias.
|
|
100
100
|
JSON:API uses `{ type, id }` under `data.relationships.reviewable.data`.
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
Writes refuse `reviewable_type` and `reviewable_id` in attributes (rule
|
|
102
|
+
`foreign_key_in_attributes`), as they refuse a belongs-to foreign key: those
|
|
103
|
+
logical fields describe storage, and the public relationship sets them together,
|
|
104
|
+
after checking that the caller can see the target.
|
|
103
105
|
Plain output uses `_type` to identify the related resource type as well.
|
|
104
106
|
|
|
105
107
|
## Include either direction
|
|
@@ -65,8 +65,10 @@ enabled, metadata is:
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Page 10 returns an empty array. `enablePaginationCounts: false` suppresses the
|
|
68
|
-
count query and count-derived metadata
|
|
69
|
-
contains totals.
|
|
68
|
+
count query and count-derived metadata (`total`, `pageCount` and the `last`
|
|
69
|
+
link); do not assume every collection response contains totals. The page then
|
|
70
|
+
reads one extra row, so `hasMore` and the `next` link remain, and a numbered
|
|
71
|
+
page costs one statement. Every mode respects `queryMaxLimit`.
|
|
70
72
|
|
|
71
73
|
Server code that reads every row, for example an export or a nightly job, can
|
|
72
74
|
raise the cap for one call with `pageSizeLimit`, a parameter beside
|
|
@@ -342,7 +342,7 @@ Built-in nested operations use separate context objects with shallow inheritance
|
|
|
342
342
|
| --- | --- | --- |
|
|
343
343
|
| Full write-response GET | Application properties and referenced cache/auth objects; the active transaction is passed explicitly. Write input is available for virtual-field response enrichment. | GET sets its own method, ID, schema, query selection and read result. Replacing a child top-level property does not replace the parent's property. |
|
|
344
344
|
| Relationship PATCH and reverse relationship child writes | Application properties and referenced objects; the active transaction is passed explicitly. | Child writes set their own method, input, schema and stored records. The parent retains its own operation identity and finish context. |
|
|
345
|
-
|
|
|
345
|
+
| Many-row writes | One context for the whole call, with `inputRecords` and `ids`. | Each row's permission check gets its own wrapper, with the row's position in `originalContext.bulkIndex`. |
|
|
346
346
|
| Ordinary CRUD permission checks and attribute enrichment | A reference to the relevant operation through `originalContext` or `parentContext`. | A fresh wrapper for that permission/enrichment call. Adding a wrapper property does not add it to the originating context. Relationship permission hooks instead receive the relationship operation context directly. |
|
|
347
347
|
| Included-resource and relationship visibility queries | Application context and the active connection/transaction. | The library sets the target resource/schema and query selection; parent filters are not blindly applied as target filters. |
|
|
348
348
|
|
|
@@ -393,7 +393,12 @@ a handler to these resource write stages.
|
|
|
393
393
|
## Read order and output enrichment
|
|
394
394
|
|
|
395
395
|
GET authorizes through `checkPermissions`, then runs `beforeData` and
|
|
396
|
-
`beforeDataGet`.
|
|
396
|
+
`beforeDataGet`. These see `context.minimalRecord`, the stored row read before
|
|
397
|
+
the full read. A resource with none of these three hooks and no
|
|
398
|
+
`checkDataPermissions` or `checkDataPermissionsGet` hook (registered for it or
|
|
399
|
+
globally) reads its row once, with its scope and row policy in that read, and
|
|
400
|
+
leaves `minimalRecord` unset; a missing or hidden row is still `not_found`, and
|
|
401
|
+
answers before an invalid fieldset does. After fetching, it runs `checkDataPermissions`,
|
|
397
402
|
`checkDataPermissionsGet` and `enrichRecord`. Field getters and computed output
|
|
398
403
|
run during attribute enrichment, followed by `enrichAttributes`.
|
|
399
404
|
`enrichRecordWithRelationships`, `finish` and `finishGet` follow. Final
|
|
@@ -412,7 +417,7 @@ undeclared output key does not establish a supported field or bypass final
|
|
|
412
417
|
field filtering. Finish hooks are late observers; they are not a substitute
|
|
413
418
|
for schema validation or query policy.
|
|
414
419
|
|
|
415
|
-
## Relationship and
|
|
420
|
+
## Relationship and many-row composition
|
|
416
421
|
|
|
417
422
|
Relationship POST and DELETE run `beforeDataCall`, followed by
|
|
418
423
|
`beforeDataCallPostRelationship` or `beforeDataCallDeleteRelationship`, after
|
|
@@ -448,9 +453,32 @@ hooks. Reverse relationship mutations can run child PATCH lifecycles inside
|
|
|
448
453
|
the parent's operation. Direct pivot/link changes do not synthesize
|
|
449
454
|
pivot-resource hook calls.
|
|
450
455
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
456
|
+
Many-row writes (`postMany`, `patchMany`, `patchWhereIds`, `patchWhere`,
|
|
457
|
+
`deleteMany`, `deleteWhere`) do not run the per-record write hooks. They run
|
|
458
|
+
batch hooks once per call, with every row in `context.inputRecords` and the
|
|
459
|
+
written ids in `context.ids`:
|
|
460
|
+
|
|
461
|
+
| Method | Batch hooks |
|
|
462
|
+
| --- | --- |
|
|
463
|
+
| `postMany` | `beforeProcessingPostMany`, `beforeDataCallPostMany`, `afterDataCallPostMany` |
|
|
464
|
+
| `patchMany`, `patchWhereIds`, `patchWhere` | `beforeProcessingPatchMany`, `beforeDataCallPatchMany`, `afterDataCallPatchMany` |
|
|
465
|
+
| `deleteMany`, `deleteWhere` | `beforeDataCallDeleteMany`, `afterDataCallDeleteMany` |
|
|
466
|
+
|
|
467
|
+
`checkPermissions`, `afterCommit` and `afterRollback` still run. A resource with
|
|
468
|
+
a per-record write hook (`beforeProcessing*`, `beforeSchemaValidate*`,
|
|
469
|
+
`afterSchemaValidate*`, `beforeDataCall*`, `afterDataCall*`, `finish*` for post,
|
|
470
|
+
patch or delete) has its many-row writes refused, unless the hook was
|
|
471
|
+
registered with `{ batch: true }`: a promise that a batch hook does its work, or
|
|
472
|
+
that it does not apply to many-row writes.
|
|
473
|
+
|
|
474
|
+
```js
|
|
475
|
+
api.addHook('finishPost', 'notify-owner', { batch: true }, notifyOne)
|
|
476
|
+
api.addHook('afterDataCallPostMany', 'notify-owners', {}, ({ context }) => notifyMany(context.ids))
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Method and hook handlers receive `perRecordHooks(events, scope)`, which lists
|
|
480
|
+
the `{ event, name }` of the handlers that would block a resource. See
|
|
481
|
+
[many-row writes](27-many-row-writes.md#hooks-and-refusals).
|
|
454
482
|
|
|
455
483
|
## Completion and failure
|
|
456
484
|
|
|
@@ -108,8 +108,9 @@ The current implementation depends on more than individual CRUD helpers:
|
|
|
108
108
|
- Writes need an actual transaction factory and the managed ownership contract,
|
|
109
109
|
including acknowledged commit/rollback, uncertain completion, completion
|
|
110
110
|
hooks and failure diagnostics. Remote side effects need their own semantics.
|
|
111
|
-
-
|
|
112
|
-
and publication boundaries.
|
|
111
|
+
- Many-row writes and schema changes must preserve their documented atomicity
|
|
112
|
+
and publication boundaries. A backend without the many-row storage helpers
|
|
113
|
+
(`dataInsertMany` and the others) has its many-row writes refused. Supporting a single-record happy path is not
|
|
113
114
|
sufficient evidence for the backend.
|
|
114
115
|
|
|
115
116
|
The source checkout's `plugins/core/lib/storage/storage-types.d.ts` describes
|
|
@@ -276,6 +276,58 @@ await api.addResource('system_settings', {
|
|
|
276
276
|
|
|
277
277
|
If a resource has no `autofilter` setting, the plugin does nothing for that resource.
|
|
278
278
|
|
|
279
|
+
## Shared Rows
|
|
280
|
+
|
|
281
|
+
Reference data that every owner reads, such as default categories, can live in
|
|
282
|
+
the same table as owned rows. Mark the filter `shared: true`; a NULL value in
|
|
283
|
+
its field then marks a shared row. The field must be `nullable`.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
await api.use(AutoFilterPlugin, {
|
|
287
|
+
resolvers: { workspace: ({ context }) => context.session?.workspaceId },
|
|
288
|
+
presets: {
|
|
289
|
+
workspaceOrShared: { filters: [{ field: 'workspace_id', resolver: 'workspace', shared: true }] }
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
await api.addResource('categories', {
|
|
293
|
+
schema: {
|
|
294
|
+
name: { type: 'string', required: true },
|
|
295
|
+
workspace_id: { type: 'string', nullable: true }
|
|
296
|
+
},
|
|
297
|
+
autofilter: 'workspaceOrShared'
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- Reads (`query`, `get`, includes, `aggregate`, `scopedQuery`) see the owner's
|
|
302
|
+
rows and the shared rows.
|
|
303
|
+
- Many-row writes, `lockByIds`, `lockWhere`, `claim` and `scopedQuery` with purpose
|
|
304
|
+
`'bulk-write'`, `'lock'` or `'claim'` see only the owner's rows.
|
|
305
|
+
- Single `patch`, `put` and `delete` of a shared row fail as `forbidden`, and so
|
|
306
|
+
does a create whose owner value resolves to NULL.
|
|
307
|
+
- Shared rows are created and changed with a system context (below).
|
|
308
|
+
|
|
309
|
+
`shared` must be `true` or `false` (the default).
|
|
310
|
+
|
|
311
|
+
## System Contexts
|
|
312
|
+
|
|
313
|
+
Work that legitimately spans every owner, such as nightly reports or imports,
|
|
314
|
+
uses a context from `createSystemContext(reason, values)`:
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
import { createSystemContext } from 'json-rest-api'
|
|
318
|
+
|
|
319
|
+
const context = createSystemContext('rebuild search index')
|
|
320
|
+
const everyProject = await api.resources.projects.queryAll({}, context)
|
|
321
|
+
await api.resources.categories.post({ data: { name: 'General', workspace_id: null } }, context)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
For a system context the plugin applies no ownership filters, and creates
|
|
325
|
+
must set every autofilter field explicitly (NULL creates a shared row on a
|
|
326
|
+
`shared` filter; a missing value fails with `system_ownership_required`). The
|
|
327
|
+
context carries a private marker that request input cannot produce. A context
|
|
328
|
+
that sets `system: true` without it throws `REST_API_SYSTEM_CONTEXT_FORGED`.
|
|
329
|
+
See [system contexts](34-scoped-reads-and-locks.md#system-contexts).
|
|
330
|
+
|
|
279
331
|
## Runtime Behavior
|
|
280
332
|
|
|
281
333
|
### Query Scoping
|
|
@@ -454,8 +506,8 @@ api.autofilter.getScopeConfig('projects')
|
|
|
454
506
|
// {
|
|
455
507
|
// preset: 'workspace_user',
|
|
456
508
|
// filters: [
|
|
457
|
-
// { field: 'workspace_id', resolver: 'workspace', required: true },
|
|
458
|
-
// { field: 'user_id', resolver: 'user', required: true }
|
|
509
|
+
// { field: 'workspace_id', resolver: 'workspace', required: true, shared: false },
|
|
510
|
+
// { field: 'user_id', resolver: 'user', required: true, shared: false }
|
|
459
511
|
// ]
|
|
460
512
|
// }
|
|
461
513
|
```
|