json-rest-api 3.0.0-rc.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 +2 -2
- package/docs/API.md +60 -27
- 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 +6 -1
- package/docs/GUIDE/16-autofiltering.md +1 -1
- package/docs/GUIDE/17-row-policies.md +8 -6
- package/docs/GUIDE/21-schema-and-migrations.md +7 -1
- package/docs/GUIDE/27-many-row-writes.md +195 -28
- package/docs/GUIDE/30-backend-capabilities.md +16 -2
- package/docs/GUIDE/33-migrating-to-v2.md +2 -2
- package/docs/GUIDE/34-scoped-reads-and-locks.md +165 -26
- package/docs/GUIDE/35-migrating-to-v3.md +141 -10
- package/lib/rest-api-errors.js +6 -5
- package/package.json +2 -2
- package/plugins/core/connectors/lib/transport-http-helpers.js +31 -3
- package/plugins/core/file-handling-plugin.js +2 -1
- 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 +12 -6
- 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 +50 -13
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +40 -3
- package/plugins/core/lib/storage/canonical-data-helpers.js +72 -41
- package/plugins/core/lib/storage/many-row-data-helpers.js +409 -124
- package/plugins/core/lib/storage/ordinary-data-helpers.js +47 -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 +8 -1
- package/plugins/core/lib/writing/many-to-many-manipulations.js +5 -2
- package/plugins/core/lib/writing/relationship-processor.js +33 -37
- package/plugins/core/rest-api-autofilter-plugin.js +30 -8
- package/plugins/core/rest-api-plugin-methods/aggregate.js +3 -1
- package/plugins/core/rest-api-plugin-methods/common.js +117 -77
- package/plugins/core/rest-api-plugin-methods/delete-many.js +54 -28
- 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 +47 -5
- package/plugins/core/rest-api-plugin-methods/patch-many.js +81 -34
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -1
- package/plugins/core/rest-api-plugin-methods/post-many.js +139 -21
- package/plugins/core/rest-api-plugin-methods/put.js +5 -1
- package/plugins/core/rest-api-plugin-methods/scoped-reads.js +72 -27
- package/plugins/core/rest-api-plugin.js +2 -1
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/types/data-access-methods.d.ts +32 -11
- package/types/errors.d.ts +5 -4
- package/types/hook-context.d.ts +2 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Reads that keep the resource's scope: the read-only scoped builder, every-row
|
|
2
2
|
// reads in cursor pages, and row locks inside a transaction.
|
|
3
|
-
import { RestApiBulkWriteUnsupportedError, RestApiRowLimitError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
3
|
+
import { RestApiBulkWriteUnsupportedError, RestApiRecordsNotFoundError, RestApiRowLimitError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
4
4
|
import { readOnlyQueryBuilder } from '../lib/querying/read-only-query.js'
|
|
5
5
|
import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
|
|
6
6
|
import { filterResponseFields, getResourceFieldset } from '../lib/querying-writing/field-utils.js'
|
|
@@ -9,21 +9,24 @@ import { resolveFormat } from '../lib/querying-writing/response-options.js'
|
|
|
9
9
|
import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
|
|
10
10
|
import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
|
|
11
11
|
import { chunk } from '../lib/storage/many-row-data-helpers.js'
|
|
12
|
-
import {
|
|
12
|
+
import { filterToOneResponseLinkage, snapshotReadRecord } from './common.js'
|
|
13
|
+
import { refuseDuplicateIds, requireLockTransaction, resolveMissing, validateFilters } from './many-row-common.js'
|
|
13
14
|
|
|
14
15
|
export const SCOPED_QUERY_PURPOSES = new Set(['report', 'bulk-write', 'lock', 'claim'])
|
|
15
16
|
const DEFAULT_QUERY_ALL_PAGE = 1000
|
|
16
17
|
const MAX_CLAIM = 1000
|
|
18
|
+
const MAX_LOCK_WHERE = 1000
|
|
17
19
|
|
|
18
20
|
const invalid = (field, message) => new RestApiValidationError(message, { fields: [field], violations: [{ field, rule: 'scoped_read', message }] })
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* The resource's table with ownership and row policy applied, as a read-only
|
|
22
|
-
* Knex builder
|
|
23
|
-
*
|
|
24
|
+
* Knex builder over a derived table named after the alias (or the table), so
|
|
25
|
+
* nothing a caller adds, `orWhere` included, can widen the scope. Resolves to
|
|
26
|
+
* `{ query }`: a builder is thenable, so returning it from an async method would run it.
|
|
24
27
|
* @param {any} args
|
|
25
28
|
*/
|
|
26
|
-
export async function scopedQueryMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName }) {
|
|
29
|
+
export async function scopedQueryMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
|
|
27
30
|
const { transaction, alias, purpose = 'report', filters, ...unknown } = params
|
|
28
31
|
if (Object.keys(unknown).length) throw invalid(Object.keys(unknown)[0], `Unknown scopedQuery options: ${Object.keys(unknown).join(', ')}`)
|
|
29
32
|
if (!SCOPED_QUERY_PURPOSES.has(purpose)) throw invalid('purpose', `purpose must be one of ${[...SCOPED_QUERY_PURPOSES].join(', ')}`)
|
|
@@ -41,8 +44,9 @@ export async function scopedQueryMethod ({ params, context, vars, helpers, scope
|
|
|
41
44
|
context.queryParams = { filters: validateFilters({ filters, scopeName, schemaInfo: context.schemaInfo, vars }) || {} }
|
|
42
45
|
// A caller who may not query the resource may not build reports on it either.
|
|
43
46
|
await scope.checkPermissions({ method: 'query', originalContext: { ...context, method: 'query', scopedQuery: true } })
|
|
44
|
-
const { query } = await helpers.dataScopedQuery({ scopeName, context, runHooks, alias, purpose, filters: context.queryParams.filters })
|
|
45
|
-
|
|
47
|
+
const { query, alias: name } = await helpers.dataScopedQuery({ scopeName, context, runHooks, alias, purpose, filters: context.queryParams.filters })
|
|
48
|
+
const derived = (transaction || api.knex.instance).from(query.select(`${name}.*`).as(name))
|
|
49
|
+
return { query: readOnlyQueryBuilder(derived, { resourceType: scopeName }) }
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -76,13 +80,26 @@ export async function queryAllMethod ({ params, context, vars, scopeName, api })
|
|
|
76
80
|
return resolvedFormat === 'jsonapi' && included.size ? { data, included: [...included.values()] } : { data }
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
// The read
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
// The rows a lock read leave through the read pipeline `query` runs after its
|
|
84
|
+
// data call, so masking and denials apply to them too: checkDataPermissions for
|
|
85
|
+
// each row as `get` runs it, enrichRecord, computed fields, the finish hooks,
|
|
86
|
+
// sparse fieldsets and the plain format. Hooks see method 'query' and lock: true.
|
|
87
|
+
async function finishLockedRead ({ document, context, scope, scopes, scopeName, runHooks, hasHooks }) {
|
|
88
|
+
context.method = 'query'
|
|
89
|
+
context.lock = true
|
|
90
|
+
context.record = normalizeRecordAttributes(document, scopes)
|
|
91
|
+
if (hasHooks('checkDataPermissions') || hasHooks('checkDataPermissionsGet')) {
|
|
92
|
+
for (const entry of context.record.data) {
|
|
93
|
+
const recordContext = { ...context, method: 'get', id: entry.id, record: { data: entry } }
|
|
94
|
+
await runHooks('checkDataPermissions', recordContext)
|
|
95
|
+
await runHooks('checkDataPermissionsGet', recordContext)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
snapshotReadRecord(context, scopes, hasHooks, 'query')
|
|
99
|
+
await runHooks('enrichRecord')
|
|
83
100
|
const computedFields = scope.vars.schemaInfo?.computed || {}
|
|
84
101
|
const requestedComputedFields = getRequestedComputedFields(scopeName, getResourceFieldset(context.queryParams.fields, scopeName), computedFields)
|
|
85
|
-
for (const entry of record.data) {
|
|
102
|
+
for (const entry of context.record.data) {
|
|
86
103
|
entry.attributes = await scope.enrichAttributes({
|
|
87
104
|
id: entry.id,
|
|
88
105
|
attributes: entry.attributes,
|
|
@@ -92,7 +109,10 @@ async function finishRecords ({ document, context, scope, scopes, scopeName }) {
|
|
|
92
109
|
computedDependencies: context.computedDependencies
|
|
93
110
|
})
|
|
94
111
|
}
|
|
95
|
-
|
|
112
|
+
await runHooks('finish')
|
|
113
|
+
await runHooks('finishQuery')
|
|
114
|
+
await filterToOneResponseLinkage(context, scopes)
|
|
115
|
+
const record = normalizeRecordAttributes(context.record, scopes, { source: 'response' })
|
|
96
116
|
filterResponseFields(record, context.queryParams.fields)
|
|
97
117
|
if (context.format !== 'plain') return { data: record.data }
|
|
98
118
|
const schemaInfo = scope.vars.schemaInfo
|
|
@@ -123,30 +143,55 @@ async function setupLockRead ({ params, context, vars, helpers, scopes, scopeNam
|
|
|
123
143
|
|
|
124
144
|
/**
|
|
125
145
|
* Locks the visible rows among `ids` until the transaction ends, in id order,
|
|
126
|
-
* and returns them as the locking read saw them.
|
|
146
|
+
* and returns them as the locking read saw them. That read is the first
|
|
147
|
+
* statement on the rows and decides visibility itself, so the transaction's
|
|
148
|
+
* snapshot starts after any lock wait. On MySQL it locks every listed row,
|
|
149
|
+
* hidden ones too, before it checks the scope (see guide 34).
|
|
127
150
|
* @param {any} args
|
|
128
151
|
*/
|
|
129
152
|
export async function lockByIdsMethod (args) {
|
|
130
|
-
const { params, context, helpers, scope, scopes, runHooks, scopeName, scopeOptions, vars } = args
|
|
153
|
+
const { params, context, helpers, scope, scopes, runHooks, hasHooks, scopeName, scopeOptions, vars } = args
|
|
131
154
|
await setupLockRead(args, 'lockByIds', 'dataLockByIds')
|
|
132
155
|
if (!Array.isArray(params.ids) || !params.ids.length) throw invalid('ids', 'ids must be a non-empty array')
|
|
133
156
|
const ids = params.ids.map(id => requireExistingResourceId(id, { scopeOptions, vars, scopeName }))
|
|
134
157
|
const storageAdapter = helpers.getStorageAdapter(scopeName)
|
|
135
158
|
refuseDuplicateIds(ids, storageAdapter, 'ids')
|
|
136
159
|
const missing = resolveMissing(params.missing)
|
|
137
|
-
|
|
138
|
-
const visible = new Set()
|
|
139
|
-
for (const part of chunk(ids, 1000)) {
|
|
140
|
-
for (const id of await helpers.dataVisibleIds({ scopeName, context, runHooks, ids: part, queryPurpose: 'lock' })) visible.add(String(id))
|
|
141
|
-
}
|
|
142
|
-
const kept = keepVisible({ requested: ids, visible, missing, scopeName, storageAdapter })
|
|
143
|
-
if (!kept.length) return { data: [] }
|
|
144
160
|
await scope.checkPermissions({ method: 'query', originalContext: { ...context, method: 'query', lock: true } })
|
|
161
|
+
// Sorted before chunking, so lists longer than a chunk still lock in one
|
|
162
|
+
// order across transactions: a list of integers numerically, any other list
|
|
163
|
+
// by code unit (the database's order for a binary collation).
|
|
164
|
+
const keys = ids.map(id => String(storageAdapter.translateFilterValue('id', id)))
|
|
165
|
+
const numeric = keys.every(key => /^-?\d+$/.test(key))
|
|
166
|
+
const sorted = ids
|
|
167
|
+
.map((id, index) => ({ id, key: numeric ? BigInt(keys[index]) : keys[index] }))
|
|
168
|
+
.sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0)
|
|
169
|
+
.map(entry => entry.id)
|
|
145
170
|
const document = { data: [] }
|
|
146
|
-
for (const part of chunk(
|
|
147
|
-
document.data.push(...(await helpers.dataLockByIds({ scopeName, context, ids: part })).data)
|
|
171
|
+
for (const part of chunk(sorted, 1000)) {
|
|
172
|
+
document.data.push(...(await helpers.dataLockByIds({ scopeName, context, runHooks, ids: part })).data)
|
|
173
|
+
}
|
|
174
|
+
if (document.data.length < ids.length && missing === 'throw') {
|
|
175
|
+
throw new RestApiRecordsNotFoundError({ resourceType: scopeName, missing: ids.length - document.data.length })
|
|
148
176
|
}
|
|
149
|
-
|
|
177
|
+
if (!document.data.length) return { data: [] }
|
|
178
|
+
return finishLockedRead({ document, context, scope, scopes, scopeName, runHooks, hasHooks })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Locks the visible rows the filters select until the transaction ends, in id
|
|
183
|
+
* order, waiting for transactions that hold them, so a row that starts to match
|
|
184
|
+
* while the lock waits is locked and returned too. At most 1,000 rows.
|
|
185
|
+
* @param {any} args
|
|
186
|
+
*/
|
|
187
|
+
export async function lockWhereMethod (args) {
|
|
188
|
+
const { params, context, vars, helpers, scope, scopes, runHooks, hasHooks, scopeName } = args
|
|
189
|
+
await setupLockRead(args, 'lockWhere', 'dataLockWhere')
|
|
190
|
+
const filters = validateFilters({ filters: params.filters, scopeName, schemaInfo: context.schemaInfo, vars })
|
|
191
|
+
await scope.checkPermissions({ method: 'query', originalContext: { ...context, method: 'query', lock: true } })
|
|
192
|
+
const document = await helpers.dataLockWhere({ scopeName, context, runHooks, filters, limit: MAX_LOCK_WHERE + 1 })
|
|
193
|
+
if (document.data.length > MAX_LOCK_WHERE) throw new RestApiRowLimitError({ resourceType: scopeName, rowLimit: MAX_LOCK_WHERE, operation: 'lockWhere' })
|
|
194
|
+
return finishLockedRead({ document, context, scope, scopes, scopeName, runHooks, hasHooks })
|
|
150
195
|
}
|
|
151
196
|
|
|
152
197
|
/**
|
|
@@ -155,7 +200,7 @@ export async function lockByIdsMethod (args) {
|
|
|
155
200
|
* @param {any} args
|
|
156
201
|
*/
|
|
157
202
|
export async function claimMethod (args) {
|
|
158
|
-
const { params, context, vars, helpers, scope, scopes, runHooks, scopeName } = args
|
|
203
|
+
const { params, context, vars, helpers, scope, scopes, runHooks, hasHooks, scopeName } = args
|
|
159
204
|
await setupLockRead(args, 'claim', 'dataClaim')
|
|
160
205
|
const limit = params.limit ?? 1
|
|
161
206
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CLAIM) throw invalid('limit', `limit must be an integer from 1 to ${MAX_CLAIM}`)
|
|
@@ -172,5 +217,5 @@ export async function claimMethod (args) {
|
|
|
172
217
|
})
|
|
173
218
|
await scope.checkPermissions({ method: 'query', originalContext: { ...context, method: 'query', lock: true } })
|
|
174
219
|
const document = await helpers.dataClaim({ scopeName, context, runHooks, filters, sort, limit })
|
|
175
|
-
return
|
|
220
|
+
return finishLockedRead({ document, context, scope, scopes, scopeName, runHooks, hasHooks })
|
|
176
221
|
}
|
|
@@ -19,7 +19,7 @@ import deleteMethod from './rest-api-plugin-methods/delete.js'
|
|
|
19
19
|
import postManyMethod from './rest-api-plugin-methods/post-many.js'
|
|
20
20
|
import { patchManyMethod, patchWhereIdsMethod, patchWhereMethod } from './rest-api-plugin-methods/patch-many.js'
|
|
21
21
|
import { deleteManyMethod, deleteWhereMethod } from './rest-api-plugin-methods/delete-many.js'
|
|
22
|
-
import { claimMethod, lockByIdsMethod, queryAllMethod, scopedQueryMethod } from './rest-api-plugin-methods/scoped-reads.js'
|
|
22
|
+
import { claimMethod, lockByIdsMethod, lockWhereMethod, queryAllMethod, scopedQueryMethod } from './rest-api-plugin-methods/scoped-reads.js'
|
|
23
23
|
import enrichAttributesMethod from './rest-api-plugin-methods/enrich-attributes.js'
|
|
24
24
|
import checkPermissionsMethod from './rest-api-plugin-methods/check-permissions.js'
|
|
25
25
|
import applyQueryFiltersMethod from './rest-api-plugin-methods/apply-query-filters.js'
|
|
@@ -112,6 +112,7 @@ export const RestApiPlugin = {
|
|
|
112
112
|
addResourceMethod('queryAll', withAvailableContext(queryAllMethod))
|
|
113
113
|
addResourceMethod('scopedQuery', withAvailableContext(scopedQueryMethod))
|
|
114
114
|
addResourceMethod('lockByIds', withAvailableContext(lockByIdsMethod))
|
|
115
|
+
addResourceMethod('lockWhere', withAvailableContext(lockWhereMethod))
|
|
115
116
|
addResourceMethod('claim', withAvailableContext(claimMethod))
|
|
116
117
|
|
|
117
118
|
// Relationship methods
|
|
@@ -214,7 +214,7 @@ export const PositioningPlugin = {
|
|
|
214
214
|
// create no positioned rows and change no positions.
|
|
215
215
|
const refusePositions = ({ context, scopeName }) => {
|
|
216
216
|
if (!enabled(scopeName)) return
|
|
217
|
-
const moves = context.inputRecords.some(record => {
|
|
217
|
+
const moves = [config.field, config.beforeIdField].some(field => context.rawFields?.includes(field)) || context.inputRecords.some(record => {
|
|
218
218
|
const attributes = record?.data?.attributes || {}
|
|
219
219
|
return attributes[config.field] !== undefined || attributes[config.beforeIdField] !== undefined
|
|
220
220
|
})
|
|
@@ -3,11 +3,16 @@ import type { ManagedTransaction } from './transactions.js'
|
|
|
3
3
|
import type { DirectResourceId, InputResourceId, JsonApiResource, PlainResource, ResourceFormat, ResourceIdentifier } from './representations.js'
|
|
4
4
|
import type { JsonApiWriteDocument, QueryParams, RemovedResourceOptions, SelectionParams } from './resource-methods.js'
|
|
5
5
|
|
|
6
|
-
// Many-row writes return nothing by default; 'minimal' returns identifiers, 'full' the records
|
|
7
|
-
|
|
6
|
+
// Many-row writes return nothing by default; 'minimal' returns identifiers, 'full' the records,
|
|
7
|
+
// 'count' the number of rows written.
|
|
8
|
+
export type ManyRowReturning = 'none' | 'minimal' | 'full' | 'count'
|
|
9
|
+
export type ManyRowCount = { meta: { count: number } }
|
|
8
10
|
export type ManyRowResult<Format extends ResourceFormat, Returning extends ManyRowReturning, Fields extends object, Type extends string> =
|
|
9
11
|
Returning extends 'none' ? void
|
|
12
|
+
: Returning extends 'count' ? ManyRowCount
|
|
10
13
|
: { data: Array<Returning extends 'minimal' ? ResourceIdentifier<Type> : Format extends 'plain' ? PlainResource<Fields> : JsonApiResource<Fields, Type>> }
|
|
14
|
+
type DeleteResult<Returning extends 'none' | 'minimal' | 'count', Type extends string> =
|
|
15
|
+
Returning extends 'none' ? void : Returning extends 'count' ? ManyRowCount : { data: ResourceIdentifier<Type>[] }
|
|
11
16
|
export type RecordsResult<Format extends ResourceFormat, Fields extends object, Type extends string> =
|
|
12
17
|
{ data: Array<Format extends 'plain' ? PlainResource<Fields> : JsonApiResource<Fields, Type>>; included?: JsonApiResource[] }
|
|
13
18
|
// knex.raw values: accepted by many-row patches for stored, non-relationship fields.
|
|
@@ -27,12 +32,17 @@ export type ManyRowCreateInput<Input extends object, Type extends string> =
|
|
|
27
32
|
{ data: Array<Partial<Input> & { id?: InputResourceId }>; document?: never } |
|
|
28
33
|
{ data?: never; document: { data: Array<JsonApiWriteDocument<Input, Type>['data']> } }
|
|
29
34
|
export interface UpsertOptions<Input extends object> {
|
|
30
|
-
// Must include every ownership (autofilter) field.
|
|
31
|
-
|
|
35
|
+
// Must include every ownership (autofilter) field. Rows may not carry an id or repeat a key.
|
|
36
|
+
// ['id'] (or the idProperty) alone is allowed on a resource without ownership fields or a
|
|
37
|
+
// row policy; every row then carries its id.
|
|
38
|
+
keys: Array<keyof Input & string> | ['id']
|
|
39
|
+
// merge also runs the patch permission checks on every row it updates.
|
|
32
40
|
action: 'ignore' | 'merge'
|
|
33
41
|
// merge only: fields to update; default every stored field the rows supply but the keys.
|
|
34
42
|
merge?: Array<keyof Input & string>
|
|
35
43
|
}
|
|
44
|
+
// patchWhere and deleteWhere need filters, or all: true to act on every visible row.
|
|
45
|
+
export type WhereScope = { filters: Record<string, unknown>; all?: never } | { filters?: never; all: true }
|
|
36
46
|
type Patchable<Input extends object> = { [Field in keyof Input]?: Input[Field] | RawValue }
|
|
37
47
|
export type ManyRowPatchInput<Input extends object, Type extends string> =
|
|
38
48
|
{ data: Array<Patchable<Input> & { id: DirectResourceId }>; document?: never } |
|
|
@@ -54,7 +64,8 @@ export interface ScopedQueryParams {
|
|
|
54
64
|
purpose?: 'report' | 'bulk-write' | 'lock' | 'claim'
|
|
55
65
|
filters?: Record<string, unknown>
|
|
56
66
|
}
|
|
57
|
-
// A Knex builder
|
|
67
|
+
// A Knex builder over a derived table of the scoped rows. It refuses writes, locks,
|
|
68
|
+
// table changes, set operations, common table expressions and access to the client.
|
|
58
69
|
export type ReadOnlyQueryBuilder = Knex.QueryBuilder
|
|
59
70
|
|
|
60
71
|
export interface DataAccessMethods<
|
|
@@ -88,17 +99,18 @@ export interface DataAccessMethods<
|
|
|
88
99
|
context?: Context
|
|
89
100
|
): Promise<ManyRowResult<Format, Returning, Fields, Type>>
|
|
90
101
|
patchWhere<Format extends ResourceFormat = DefaultFormat, Returning extends ManyRowReturning = 'none'>(
|
|
91
|
-
params: ManyRowWriteOptions<Format, Returning> & SharedPatchInput<Input, Type> & Omit<PatchConditionOptions<Input>, 'missing'> &
|
|
102
|
+
params: ManyRowWriteOptions<Format, Returning> & SharedPatchInput<Input, Type> & Omit<PatchConditionOptions<Input>, 'missing'> & WhereScope,
|
|
92
103
|
context?: Context
|
|
93
104
|
): Promise<ManyRowResult<Format, Returning, Fields, Type>>
|
|
94
|
-
deleteMany<Returning extends 'none' | 'minimal' = 'none'>(
|
|
105
|
+
deleteMany<Returning extends 'none' | 'minimal' | 'count' = 'none'>(
|
|
95
106
|
params: RemovedResourceOptions & { ids: DirectResourceId[]; transaction?: ManagedTransaction; returning?: Returning; chunkSize?: number; missing?: MissingRows },
|
|
96
107
|
context?: Context
|
|
97
|
-
): Promise<Returning
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
): Promise<DeleteResult<Returning, Type>>
|
|
109
|
+
// Deletes the rows its selection finds and answers with those it deleted; it never throws for missing rows.
|
|
110
|
+
deleteWhere<Returning extends 'none' | 'minimal' | 'count' = 'none'>(
|
|
111
|
+
params: RemovedResourceOptions & WhereScope & { transaction?: ManagedTransaction; returning?: Returning; chunkSize?: number },
|
|
100
112
|
context?: Context
|
|
101
|
-
): Promise<Returning
|
|
113
|
+
): Promise<DeleteResult<Returning, Type>>
|
|
102
114
|
// Locks the visible rows among ids (FOR UPDATE, id order) until the transaction ends.
|
|
103
115
|
lockByIds<Format extends ResourceFormat = DefaultFormat>(params: {
|
|
104
116
|
ids: DirectResourceId[]
|
|
@@ -107,6 +119,15 @@ export interface DataAccessMethods<
|
|
|
107
119
|
format?: Format
|
|
108
120
|
queryParams?: { fields?: Record<string, string> }
|
|
109
121
|
}, context?: Context): Promise<RecordsResult<Format, Fields, Type>>
|
|
122
|
+
// Locks the visible rows the filters select (FOR UPDATE; id order, on MySQL the
|
|
123
|
+
// order of the forced index, which starts with the ownership or a filtered column),
|
|
124
|
+
// waiting for other transactions; throws RestApiRowLimitError past 1000 rows.
|
|
125
|
+
lockWhere<Format extends ResourceFormat = DefaultFormat>(params: {
|
|
126
|
+
transaction: ManagedTransaction | Knex.Transaction
|
|
127
|
+
filters?: Record<string, unknown>
|
|
128
|
+
format?: Format
|
|
129
|
+
queryParams?: { fields?: Record<string, string> }
|
|
130
|
+
}, context?: Context): Promise<RecordsResult<Format, Fields, Type>>
|
|
110
131
|
// Takes up to limit (default 1, at most 1000) matching rows no other transaction holds.
|
|
111
132
|
claim<Format extends ResourceFormat = DefaultFormat>(params: {
|
|
112
133
|
transaction: ManagedTransaction | Knex.Transaction
|
package/types/errors.d.ts
CHANGED
|
@@ -118,11 +118,12 @@ export class RestApiTemporalDataError extends RestApiError {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export class RestApiBulkWriteUnsupportedError extends RestApiError {
|
|
121
|
-
constructor(options?: { resourceType?: string; operation?: string; reason?: string })
|
|
121
|
+
constructor(options?: { resourceType?: string; operation?: string; reason?: string; hooks?: string[] })
|
|
122
122
|
code: 'REST_API_BULK_WRITE_UNSUPPORTED'
|
|
123
123
|
type: 'rest_api_bulk_write_unsupported'
|
|
124
124
|
statusCode: 422
|
|
125
|
-
|
|
125
|
+
/** `hooks` names the per-record hooks that caused the refusal; HTTP responses leave details out. */
|
|
126
|
+
details: { resourceType?: string; operation?: string; reason?: string; hooks?: string[] }
|
|
126
127
|
toJSON(): SerializedError & Pick<RestApiBulkWriteUnsupportedError, 'code' | 'type' | 'statusCode' | 'details'>
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -154,9 +155,9 @@ export class RestApiScopedQueryReadOnlyError extends RestApiError {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export class RestApiRowLimitError extends RestApiError {
|
|
157
|
-
constructor(options?: { resourceType?: string; rowLimit?: number })
|
|
158
|
+
constructor(options?: { resourceType?: string; rowLimit?: number; operation?: 'queryAll' | 'lockWhere' })
|
|
158
159
|
code: 'REST_API_ROW_LIMIT'
|
|
159
160
|
type: 'rest_api_row_limit'
|
|
160
|
-
details: { resourceType?: string; rowLimit?: number }
|
|
161
|
+
details: { resourceType?: string; rowLimit?: number; operation: 'queryAll' | 'lockWhere' }
|
|
161
162
|
toJSON(): SerializedError & Pick<RestApiRowLimitError, 'code' | 'type' | 'details'>
|
|
162
163
|
}
|
package/types/hook-context.d.ts
CHANGED
|
@@ -189,6 +189,8 @@ export type ManyRowHookContext<Context extends object = Record<string, unknown>,
|
|
|
189
189
|
readonly transaction: ManagedTransaction
|
|
190
190
|
// JSON:API documents of the rows, in input order (creates and patches).
|
|
191
191
|
readonly inputRecords?: Array<{ data: { id?: InputResourceId; attributes?: Record<string, unknown>; relationships?: Record<string, unknown> } }>
|
|
192
|
+
// Patches: fields some row sets with a knex.raw value (those values are not in inputRecords).
|
|
193
|
+
readonly rawFields?: string[]
|
|
192
194
|
readonly ids?: Array<string | undefined>
|
|
193
195
|
}
|
|
194
196
|
|