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
|
@@ -14,6 +14,8 @@ import { translateCanonicalAttributesForStorage, getCanonicalResourceId, getCano
|
|
|
14
14
|
import { unwrapQueryBuilderState, withQueryFilteringContext } from '../querying/query-builder-utils.js'
|
|
15
15
|
import { applyInsertReturning } from '../querying-writing/database-capabilities.js'
|
|
16
16
|
import { serializeJsonApiQuery } from '../querying-writing/connectors-query-parser.js'
|
|
17
|
+
import { deleteFoundRow, foundRow, lockFoundRow, updateFoundRow } from './single-row-writes.js'
|
|
18
|
+
import { whereInIdentifiers } from '../querying/identifier-query.js'
|
|
17
19
|
|
|
18
20
|
/** @param {StorageRow} row @param {CanonicalDescriptor} descriptor */
|
|
19
21
|
function requireCanonicalResourceId (row, descriptor) {
|
|
@@ -91,6 +93,25 @@ export function createCanonicalDataHelpers ({
|
|
|
91
93
|
return descriptors
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
// The canonical row of the record a single write found with its scoped read.
|
|
97
|
+
/** @param {string} scopeName @param {{ db: import('./storage-types.js').StorageDatabase, id: string | number }} context */
|
|
98
|
+
const foundRecord = (scopeName, context) => {
|
|
99
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
100
|
+
if (!storageAdapter) throw new Error(`Storage adapter for resource '${scopeName}' not found`)
|
|
101
|
+
const descriptor = getDescriptor(scopeName)
|
|
102
|
+
const { canonical } = descriptor
|
|
103
|
+
return foundRow({
|
|
104
|
+
byId: () => context.db(canonical.tableName)
|
|
105
|
+
.where(getCanonicalResourceIdColumn(descriptor), context.id)
|
|
106
|
+
.where(canonical.resourceColumn, descriptor.resource)
|
|
107
|
+
.where(canonical.tenantColumn, descriptor.tenant),
|
|
108
|
+
context,
|
|
109
|
+
scopeName,
|
|
110
|
+
storageAdapter,
|
|
111
|
+
tableName: canonical.tableName
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
/** @type {CanonicalDataWriteHelpers['dataExists']} */
|
|
95
116
|
const dataExists = async ({ scopeName, context }) => {
|
|
96
117
|
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
@@ -188,13 +209,7 @@ export function createCanonicalDataHelpers ({
|
|
|
188
209
|
return 1
|
|
189
210
|
}
|
|
190
211
|
|
|
191
|
-
|
|
192
|
-
.where(logicalIdColumn, id)
|
|
193
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
194
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
195
|
-
.update(writeRow)
|
|
196
|
-
|
|
197
|
-
return result
|
|
212
|
+
return updateFoundRow(foundRecord(scopeName, context), { db: context.db, scopeName, id, idColumn: logicalIdColumn, values: writeRow })
|
|
198
213
|
}
|
|
199
214
|
|
|
200
215
|
/** @type {CanonicalDataWriteHelpers['dataPatch']} */
|
|
@@ -204,7 +219,6 @@ export function createCanonicalDataHelpers ({
|
|
|
204
219
|
context.storageAdapter = storageAdapter
|
|
205
220
|
}
|
|
206
221
|
const descriptor = getDescriptor(scopeName)
|
|
207
|
-
const { canonical } = descriptor
|
|
208
222
|
const id = context.id
|
|
209
223
|
const logicalIdColumn = getCanonicalResourceIdColumn(descriptor)
|
|
210
224
|
const attributes = context.inputRecord?.data?.attributes || {}
|
|
@@ -216,17 +230,7 @@ export function createCanonicalDataHelpers ({
|
|
|
216
230
|
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
217
231
|
)
|
|
218
232
|
|
|
219
|
-
|
|
220
|
-
return 0
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const result = await context.db(canonical.tableName)
|
|
224
|
-
.where(logicalIdColumn, id)
|
|
225
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
226
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
227
|
-
.update(updateRow)
|
|
228
|
-
|
|
229
|
-
return result
|
|
233
|
+
return updateFoundRow(foundRecord(scopeName, context), { db: context.db, scopeName, id, idColumn: logicalIdColumn, values: updateRow })
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
/** @type {CanonicalDataWriteHelpers['dataDelete']} */
|
|
@@ -236,23 +240,18 @@ export function createCanonicalDataHelpers ({
|
|
|
236
240
|
context.storageAdapter = storageAdapter
|
|
237
241
|
}
|
|
238
242
|
const descriptor = getDescriptor(scopeName)
|
|
239
|
-
const { canonical } = descriptor
|
|
240
243
|
const id = context.id
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
await linkStore.invalidateDeletedLinkTargets(scopeName, context)
|
|
244
|
-
const result = await context.db(canonical.tableName)
|
|
245
|
-
.where(logicalIdColumn, id)
|
|
246
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
247
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
248
|
-
.delete()
|
|
244
|
+
const row = foundRecord(scopeName, context)
|
|
249
245
|
|
|
246
|
+
const idColumn = getCanonicalResourceIdColumn(descriptor)
|
|
247
|
+
await linkStore.invalidateDeletedLinkTargets(scopeName, context, () => lockFoundRow(row, { scopeName, id, idColumn }))
|
|
248
|
+
const deleted = await deleteFoundRow(row, { scopeName, id })
|
|
250
249
|
await linkStore.deleteResourceLinks({ descriptor, scopeName, id, db: context.db })
|
|
251
250
|
|
|
252
|
-
return
|
|
251
|
+
return deleted
|
|
253
252
|
}
|
|
254
253
|
|
|
255
|
-
// Supplying ids returns minimal records for
|
|
254
|
+
// Supplying ids returns minimal records for all of them, in one statement.
|
|
256
255
|
/** @overload @param {DataMinimalRequest & { ids: Array<string | number> }} request @returns {Promise<DataResource[]>} */
|
|
257
256
|
/** @overload @param {DataMinimalRequest & { ids?: undefined, context: DataReadContext & {id: string | number} }} request @returns {Promise<DataResource | null>} */
|
|
258
257
|
/** @param {DataMinimalRequest & { ids?: Array<string | number> }} request */
|
|
@@ -279,8 +278,7 @@ export function createCanonicalDataHelpers ({
|
|
|
279
278
|
.where(canonical.tenantColumn, descriptor.tenant)
|
|
280
279
|
if (ids === undefined) query.where(logicalIdColumn, id)
|
|
281
280
|
else {
|
|
282
|
-
|
|
283
|
-
query.whereIn(logicalIdColumn, /** @type {import('knex').Knex.Value[]} */ (ids.map(value => storageAdapter.translateFilterValue('id', value))))
|
|
281
|
+
query.modify(whereInIdentifiers, logicalIdColumn, ids.map(value => storageAdapter.translateFilterValue('id', value)))
|
|
284
282
|
.distinct(`${canonical.tableName}.${logicalIdColumn}`)
|
|
285
283
|
}
|
|
286
284
|
|
|
@@ -332,7 +330,7 @@ export function createCanonicalDataHelpers ({
|
|
|
332
330
|
return minimal
|
|
333
331
|
}
|
|
334
332
|
if (ids !== undefined) {
|
|
335
|
-
// Filtering owns predicates; the
|
|
333
|
+
// Filtering owns predicates; the id list owns its identity projection.
|
|
336
334
|
query.clearSelect().distinct(`${canonical.tableName}.${logicalIdColumn}`)
|
|
337
335
|
/** @type {StorageRow[]} */
|
|
338
336
|
const rows = await applyDatabaseReadOptions(storageAdapter.buildBaseQuery({ transaction: context.db }).whereIn(logicalIdColumn, query))
|
|
@@ -343,7 +341,7 @@ export function createCanonicalDataHelpers ({
|
|
|
343
341
|
}
|
|
344
342
|
|
|
345
343
|
/** @type {CanonicalDataReadHelpers['dataGet']} */
|
|
346
|
-
const dataGet = async ({ scopeName, context }) => {
|
|
344
|
+
const dataGet = async ({ scopeName, context, runHooks, scoped = false }) => {
|
|
347
345
|
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
348
346
|
if (!storageAdapter) throw new Error(`Storage adapter for resource '${scopeName}' not found`)
|
|
349
347
|
context.storageAdapter = storageAdapter
|
|
@@ -357,17 +355,40 @@ export function createCanonicalDataHelpers ({
|
|
|
357
355
|
const fieldSelectionInfo = await buildFieldSelection(scope, { context })
|
|
358
356
|
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
359
357
|
|
|
358
|
+
// The preceding minimal read has already applied permission and row filters,
|
|
359
|
+
// unless the read is `scoped`: then it applies them itself, as the only read,
|
|
360
|
+
// and names its columns' table, since a row policy may join tables.
|
|
361
|
+
const column = (/** @type {string} */ name) => scoped ? `${canonical.tableName}.${name}` : name
|
|
360
362
|
let query = context.db(canonical.tableName)
|
|
361
|
-
.where(logicalIdColumn, id)
|
|
362
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
363
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
363
|
+
.where(column(logicalIdColumn), id)
|
|
364
|
+
.where(column(canonical.resourceColumn), descriptor.resource)
|
|
365
|
+
.where(column(canonical.tenantColumn), descriptor.tenant)
|
|
366
|
+
if (scoped) {
|
|
367
|
+
const filterHooks = runHooks
|
|
368
|
+
if (!filterHooks) throw new Error('A scoped read needs runHooks')
|
|
369
|
+
query = (await withQueryFilteringContext(context, {
|
|
370
|
+
query,
|
|
371
|
+
filters: context.queryParams?.filters,
|
|
372
|
+
schemaInfo: context.schemaInfo,
|
|
373
|
+
scopeName,
|
|
374
|
+
tableName: canonical.tableName,
|
|
375
|
+
db: context.db,
|
|
376
|
+
queryPurpose: 'single',
|
|
377
|
+
isAnyApi: true,
|
|
378
|
+
adapter: storageAdapter,
|
|
379
|
+
storageAdapter
|
|
380
|
+
}, async () => {
|
|
381
|
+
await applyBuiltInAnyApiQueryFilters(context)
|
|
382
|
+
await filterHooks('knexQueryFiltering')
|
|
383
|
+
})).query
|
|
384
|
+
}
|
|
364
385
|
|
|
365
386
|
const selectionState = await applyFieldSelectionToQuery({
|
|
366
387
|
query,
|
|
367
388
|
scope,
|
|
368
389
|
fieldSelectionInfo,
|
|
369
390
|
tableName: canonical.tableName,
|
|
370
|
-
useTablePrefix:
|
|
391
|
+
useTablePrefix: scoped,
|
|
371
392
|
storageAdapter,
|
|
372
393
|
db: context.db,
|
|
373
394
|
context,
|
|
@@ -512,6 +533,11 @@ export function createCanonicalDataHelpers ({
|
|
|
512
533
|
let rows = await applyDatabaseReadOptions(queryBuilder)
|
|
513
534
|
let cursorRecords = null
|
|
514
535
|
let hasMore = false
|
|
536
|
+
// Without a count, a numbered page's one extra row answers hasMore.
|
|
537
|
+
if (paginationInfo.mode === 'offset' && !scope.vars.enablePaginationCounts) {
|
|
538
|
+
hasMore = rows.length > paginationInfo.pageSize
|
|
539
|
+
if (hasMore) rows = rows.slice(0, paginationInfo.pageSize)
|
|
540
|
+
}
|
|
515
541
|
|
|
516
542
|
if (paginationInfo.mode === 'cursor') {
|
|
517
543
|
const { pageSize } = paginationInfo
|
|
@@ -615,7 +641,7 @@ export function createCanonicalDataHelpers ({
|
|
|
615
641
|
const total = Number(countResult?.count ?? countResult?.total ?? 0)
|
|
616
642
|
paginationMeta = calculatePaginationMeta(total, page, pageSize)
|
|
617
643
|
} else {
|
|
618
|
-
paginationMeta = { page, pageSize }
|
|
644
|
+
paginationMeta = { page, pageSize, hasMore }
|
|
619
645
|
}
|
|
620
646
|
|
|
621
647
|
context.returnMeta.paginationMeta = paginationMeta
|
|
@@ -699,13 +725,51 @@ export function createCanonicalDataHelpers ({
|
|
|
699
725
|
return runAggregateQuery({ source: filteredState.query, sourceTable: tableAlias, storageAdapter, db, plan })
|
|
700
726
|
}
|
|
701
727
|
|
|
728
|
+
/**
|
|
729
|
+
* The canonical rows of one resource with ownership and row policy applied.
|
|
730
|
+
* Many-row writes and row locks are table-storage features; AnyAPI resources
|
|
731
|
+
* get the read-only builder only. Resolves to `{ query, alias }`.
|
|
732
|
+
* @param {{ scopeName: string, context: DataReadContext, runHooks: (name: 'knexQueryFiltering') => unknown, alias?: string, purpose: string, filters?: Record<string, unknown> }} request
|
|
733
|
+
*/
|
|
734
|
+
const dataScopedQuery = async ({ scopeName, context, runHooks, alias, purpose, filters }) => {
|
|
735
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
736
|
+
if (!storageAdapter) throw new Error(`Storage adapter for resource '${scopeName}' not found`)
|
|
737
|
+
context.storageAdapter = storageAdapter
|
|
738
|
+
const descriptor = getDescriptor(scopeName)
|
|
739
|
+
const db = context.db || context.transaction || api.knex.instance
|
|
740
|
+
const tableAlias = alias || context.schemaInfo.tableName || descriptor.resource
|
|
741
|
+
const filteredState = await withQueryFilteringContext(context, {
|
|
742
|
+
query: storageAdapter.buildBaseQuery({ transaction: db, tableAlias }),
|
|
743
|
+
filters,
|
|
744
|
+
schemaInfo: context.schemaInfo,
|
|
745
|
+
scopeName,
|
|
746
|
+
tableName: tableAlias,
|
|
747
|
+
db,
|
|
748
|
+
queryPurpose: purpose,
|
|
749
|
+
isAnyApi: true,
|
|
750
|
+
adapter: storageAdapter,
|
|
751
|
+
storageAdapter,
|
|
752
|
+
}, async () => {
|
|
753
|
+
await applyBuiltInAnyApiQueryFilters(context)
|
|
754
|
+
await runHooks('knexQueryFiltering')
|
|
755
|
+
})
|
|
756
|
+
// Wrapped: a builder is thenable, so an async function cannot return it bare.
|
|
757
|
+
return { query: filteredState.query, alias: tableAlias }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// A get with no reader of the minimal record reads once, scoped.
|
|
761
|
+
/** @type {NonNullable<CanonicalDataReadHelpers['dataGetScoped']>} */
|
|
762
|
+
const dataGetScoped = ({ scopeName, context, runHooks }) => dataGet({ scopeName, context, runHooks, scoped: true })
|
|
763
|
+
|
|
702
764
|
return {
|
|
765
|
+
dataScopedQuery,
|
|
703
766
|
dataExists,
|
|
704
767
|
dataPost,
|
|
705
768
|
dataPut,
|
|
706
769
|
dataPatch,
|
|
707
770
|
dataDelete,
|
|
708
771
|
dataGet,
|
|
772
|
+
dataGetScoped,
|
|
709
773
|
dataQuery,
|
|
710
774
|
dataAggregate,
|
|
711
775
|
dataGetMinimal
|