json-rest-api 1.0.23 → 1.0.25
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 +24 -2
- package/docs/API.md +157 -33
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +31 -38
- package/plugins/core/connectors/express-plugin.js +6 -7
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/schema-utils.js +44 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
- package/plugins/core/lib/dbIntrospection.js +1 -1
- package/plugins/core/lib/dbTablesOperations.js +10 -3
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
- package/plugins/core/lib/storage/storage-adapter.js +76 -14
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
- package/plugins/core/rest-api-knex-plugin.js +93 -76
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
- package/plugins/core/rest-api-plugin-methods/common.js +112 -72
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
- package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
- package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
- package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
- package/plugins/core/rest-api-plugin-methods/post.js +12 -7
- package/plugins/core/rest-api-plugin-methods/put.js +35 -10
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +11 -37
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +45 -20
- package/plugins/storage/s3-storage.js +23 -54
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +8 -1
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/db-introspection.test.js +4 -1
- package/tests/db-table-operations.test.js +25 -20
- package/tests/fastify-plugin.test.js +49 -6
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +495 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/id-normalization.test.js +444 -0
- package/tests/local-storage.test.js +96 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +132 -11
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +433 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +56 -6
- package/tests/test-development-guide.md +5 -5
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
buildCursorMeta,
|
|
15
15
|
parseCursor,
|
|
16
16
|
} from './lib/querying/knex-pagination-helpers.js'
|
|
17
|
-
import { getUrlPrefix, buildResourceUrl } from './lib/querying/url-helpers.js'
|
|
17
|
+
import { getUrlPrefix, buildResourceUrl, buildRelationshipUrl } from './lib/querying/url-helpers.js'
|
|
18
18
|
import {
|
|
19
19
|
normalizeId,
|
|
20
20
|
resolveFieldInfo,
|
|
@@ -35,12 +35,16 @@ import {
|
|
|
35
35
|
polymorphicFiltersHook,
|
|
36
36
|
crossTableFiltersHook,
|
|
37
37
|
basicFiltersHook,
|
|
38
|
+
resolveSearchOperator,
|
|
39
|
+
applyWhereForOperator,
|
|
38
40
|
} from './lib/querying/knex-query-helpers.js'
|
|
39
41
|
import {
|
|
40
42
|
createStorageAdapter,
|
|
41
43
|
} from './lib/storage/storage-adapter.js'
|
|
42
44
|
import {
|
|
43
45
|
translateCanonicalAttributesForStorage,
|
|
46
|
+
getCanonicalResourceId,
|
|
47
|
+
getCanonicalResourceIdColumn,
|
|
44
48
|
translateCanonicalRecordFromStorage,
|
|
45
49
|
} from './lib/storage/canonical-storage-mapping.js'
|
|
46
50
|
import {
|
|
@@ -50,6 +54,7 @@ import {
|
|
|
50
54
|
parseSortEntry
|
|
51
55
|
} from './lib/querying/query-field-sort-helpers.js'
|
|
52
56
|
import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
|
|
57
|
+
import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
|
|
53
58
|
|
|
54
59
|
const DEFAULT_TENANT = 'default'
|
|
55
60
|
const LINKS_TABLE = 'any_links'
|
|
@@ -118,49 +123,115 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
118
123
|
if (!descriptor) {
|
|
119
124
|
throw new Error(`Descriptor not found for resource '${scopeName}'`)
|
|
120
125
|
}
|
|
126
|
+
const scope = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
127
|
+
const stored = scopeOptionsRegistry.get(scopeName) || {}
|
|
128
|
+
descriptor.idProperty = stored.idProperty ||
|
|
129
|
+
scope?.vars?.schemaInfo?.idProperty ||
|
|
130
|
+
scope?.scopeOptions?.idProperty ||
|
|
131
|
+
descriptor.idProperty ||
|
|
132
|
+
vars.idProperty ||
|
|
133
|
+
'id'
|
|
121
134
|
return descriptor
|
|
122
135
|
}
|
|
123
136
|
|
|
137
|
+
const decorateResourceLinks = ({ resource, scope, scopeName, context }) => {
|
|
138
|
+
if (!resource || !scope || !scopeName || !context) return
|
|
139
|
+
|
|
140
|
+
resource.links = resource.links || {}
|
|
141
|
+
resource.links.self = buildResourceUrl(context, scope, scopeName, resource.id)
|
|
142
|
+
|
|
143
|
+
if (!resource.relationships || typeof resource.relationships !== 'object') {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const [relationshipName, relationshipObject] of Object.entries(resource.relationships)) {
|
|
148
|
+
if (!relationshipObject || typeof relationshipObject !== 'object') {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
relationshipObject.links = {
|
|
153
|
+
self: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, true),
|
|
154
|
+
related: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, false),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
124
159
|
const getAllowedQueryFieldNames = (scopeName) => (
|
|
125
160
|
Object.keys(api.resources?.[scopeName]?.vars?.queryFields || {})
|
|
126
161
|
)
|
|
127
162
|
|
|
163
|
+
const getLogicalResourceIdColumn = (descriptor) => getCanonicalResourceIdColumn(descriptor)
|
|
164
|
+
|
|
165
|
+
const getLogicalResourceId = (row, descriptor) => getCanonicalResourceId(row, descriptor)
|
|
166
|
+
|
|
128
167
|
const applyFiltersToQuery = ({ query, filters, descriptor, searchSchema, adapter }) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
168
|
+
if (!filters || Object.keys(filters).length === 0) return
|
|
169
|
+
|
|
170
|
+
const normalizeValue = (field, value) => {
|
|
171
|
+
if (!adapter?.translateFilterValue) {
|
|
172
|
+
return value
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
return value.map((entry) => adapter.translateFilterValue(field, entry))
|
|
176
|
+
}
|
|
177
|
+
return adapter.translateFilterValue(field, value)
|
|
134
178
|
}
|
|
135
179
|
|
|
136
|
-
|
|
137
|
-
|
|
180
|
+
for (const [filterKey, rawValue] of Object.entries(filters)) {
|
|
181
|
+
const fieldDef = searchSchema?.[filterKey]
|
|
182
|
+
|
|
183
|
+
if (fieldDef?.polymorphicField) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
138
186
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
187
|
+
if (fieldDef?.actualField?.includes('.')) {
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (fieldDef?.oneOf?.some((field) => field.includes('.'))) {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (fieldDef?.oneOf && Array.isArray(fieldDef.oneOf)) {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (fieldDef?.applyFilter && typeof fieldDef.applyFilter === 'function') {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const actualField = fieldDef?.actualField || filterKey
|
|
204
|
+
const fieldInfo = ensureFilterableField(descriptor, filterKey)
|
|
205
|
+
const columnRef = adapter ? adapter.translateColumn(actualField) : fieldInfo.column
|
|
206
|
+
const operator = resolveSearchOperator(fieldDef || fieldInfo.definition)
|
|
142
207
|
const values = normalizeFilterValues(rawValue, fieldInfo.definition, {
|
|
143
208
|
isRelationship: fieldInfo.isRelationship,
|
|
144
209
|
})
|
|
145
|
-
|
|
146
|
-
const nonNullValues =
|
|
147
|
-
const hasNull =
|
|
210
|
+
const normalizedValues = values.map((value) => normalizeValue(actualField, value))
|
|
211
|
+
const nonNullValues = normalizedValues.filter((value) => value !== null)
|
|
212
|
+
const hasNull = normalizedValues.length !== nonNullValues.length
|
|
148
213
|
|
|
149
214
|
if (nonNullValues.length === 0 && hasNull) {
|
|
150
|
-
query.whereNull(
|
|
215
|
+
query.whereNull(columnRef)
|
|
151
216
|
continue
|
|
152
217
|
}
|
|
153
218
|
if (nonNullValues.length === 1 && !hasNull) {
|
|
154
|
-
|
|
219
|
+
applyWhereForOperator({
|
|
220
|
+
builder: query,
|
|
221
|
+
columnRef,
|
|
222
|
+
operator,
|
|
223
|
+
value: nonNullValues[0],
|
|
224
|
+
knex,
|
|
225
|
+
})
|
|
155
226
|
continue
|
|
156
227
|
}
|
|
157
228
|
if (nonNullValues.length > 1 && !hasNull) {
|
|
158
|
-
query.whereIn(
|
|
229
|
+
query.whereIn(columnRef, nonNullValues)
|
|
159
230
|
continue
|
|
160
231
|
}
|
|
161
232
|
if (nonNullValues.length > 0 && hasNull) {
|
|
162
|
-
query.where(function
|
|
163
|
-
this.whereIn(
|
|
233
|
+
query.where(function anyApiNullFilterGroup () {
|
|
234
|
+
this.whereIn(columnRef, nonNullValues).orWhereNull(columnRef)
|
|
164
235
|
})
|
|
165
236
|
}
|
|
166
237
|
}
|
|
@@ -213,34 +284,6 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
213
284
|
return descriptors
|
|
214
285
|
}
|
|
215
286
|
|
|
216
|
-
const buildQueryString = (queryParams = {}) => {
|
|
217
|
-
const buildParts = (prefix, value) => {
|
|
218
|
-
const parts = []
|
|
219
|
-
if (Array.isArray(value)) {
|
|
220
|
-
if (value.length === 0) return parts
|
|
221
|
-
parts.push(`${prefix}=${value.map((item) => encodeURIComponent(item)).join(',')}`)
|
|
222
|
-
return parts
|
|
223
|
-
}
|
|
224
|
-
if (value && typeof value === 'object') {
|
|
225
|
-
for (const [key, subValue] of Object.entries(value)) {
|
|
226
|
-
if (subValue === undefined || subValue === null) continue
|
|
227
|
-
const newPrefix = `${prefix}[${key}]`
|
|
228
|
-
parts.push(...buildParts(newPrefix, subValue))
|
|
229
|
-
}
|
|
230
|
-
return parts
|
|
231
|
-
}
|
|
232
|
-
if (value === undefined || value === null) return parts
|
|
233
|
-
parts.push(`${prefix}=${encodeURIComponent(value)}`)
|
|
234
|
-
return parts
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const parts = []
|
|
238
|
-
for (const [key, value] of Object.entries(queryParams)) {
|
|
239
|
-
parts.push(...buildParts(key, value))
|
|
240
|
-
}
|
|
241
|
-
return parts.length > 0 ? `?${parts.join('&')}` : ''
|
|
242
|
-
}
|
|
243
|
-
|
|
244
287
|
const applyPaginationToQuery = ({
|
|
245
288
|
query,
|
|
246
289
|
scope,
|
|
@@ -317,13 +360,16 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
317
360
|
|
|
318
361
|
predicateChains.forEach((chain) => {
|
|
319
362
|
const chainParts = chain.map(({ descriptor, operator, value }) => {
|
|
363
|
+
const normalizedValue = adapter?.translateFilterValue
|
|
364
|
+
? adapter.translateFilterValue(descriptor.field, value)
|
|
365
|
+
: value
|
|
320
366
|
if (descriptor.queryFieldRuntime) {
|
|
321
|
-
predicateBindings.push(...(descriptor.queryFieldRuntime.bindings || []),
|
|
367
|
+
predicateBindings.push(...(descriptor.queryFieldRuntime.bindings || []), normalizedValue)
|
|
322
368
|
return `(${descriptor.queryFieldRuntime.sql}) ${operator} ?`
|
|
323
369
|
}
|
|
324
370
|
|
|
325
371
|
const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
|
|
326
|
-
predicateBindings.push(
|
|
372
|
+
predicateBindings.push(normalizedValue)
|
|
327
373
|
return `${columnRef} ${operator} ?`
|
|
328
374
|
})
|
|
329
375
|
|
|
@@ -624,7 +670,88 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
624
670
|
return results
|
|
625
671
|
}
|
|
626
672
|
|
|
627
|
-
const
|
|
673
|
+
const applyTargetScopeFilters = async ({
|
|
674
|
+
query,
|
|
675
|
+
resourceName,
|
|
676
|
+
tableName,
|
|
677
|
+
context,
|
|
678
|
+
filters,
|
|
679
|
+
queryPurpose = 'include'
|
|
680
|
+
}) => {
|
|
681
|
+
const targetScope = api.resources?.[resourceName]
|
|
682
|
+
if (!targetScope?.applyQueryFilters) {
|
|
683
|
+
return { query }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const storageAdapter = getScopeStorageAdapter(resourceName)
|
|
687
|
+
const queryState = await targetScope.applyQueryFilters({
|
|
688
|
+
query,
|
|
689
|
+
filters,
|
|
690
|
+
schemaInfo: targetScope.vars?.schemaInfo,
|
|
691
|
+
scopeName: resourceName,
|
|
692
|
+
tableName,
|
|
693
|
+
db: context.db || context.transaction || api.knex.instance,
|
|
694
|
+
isAnyApi: true,
|
|
695
|
+
queryPurpose,
|
|
696
|
+
storageAdapter
|
|
697
|
+
}, {
|
|
698
|
+
...context,
|
|
699
|
+
scopeName: resourceName,
|
|
700
|
+
schemaInfo: targetScope.vars?.schemaInfo,
|
|
701
|
+
storageAdapter
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
query: unwrapQueryBuilderState(queryState, query)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const filterVisibleIdentifiers = async ({ identifiers, context }) => {
|
|
710
|
+
if (!identifiers || identifiers.length === 0) return []
|
|
711
|
+
|
|
712
|
+
const db = context.db || context.transaction || api.knex.instance
|
|
713
|
+
const identifiersByType = new Map()
|
|
714
|
+
|
|
715
|
+
for (const identifier of identifiers) {
|
|
716
|
+
if (!identifier?.type || identifier.id === undefined || identifier.id === null) continue
|
|
717
|
+
if (!identifiersByType.has(identifier.type)) {
|
|
718
|
+
identifiersByType.set(identifier.type, new Set())
|
|
719
|
+
}
|
|
720
|
+
identifiersByType.get(identifier.type).add(normalizeId(identifier.id))
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const visibleKeys = new Set()
|
|
724
|
+
|
|
725
|
+
for (const [resourceName, idSet] of identifiersByType.entries()) {
|
|
726
|
+
const descriptor = await getDescriptor(resourceName)
|
|
727
|
+
const idColumn = getLogicalResourceIdColumn(descriptor)
|
|
728
|
+
let query = db(descriptor.canonical.tableName)
|
|
729
|
+
.where(descriptor.canonical.tenantColumn, descriptor.tenant)
|
|
730
|
+
.where(descriptor.canonical.resourceColumn, descriptor.resource)
|
|
731
|
+
.whereIn(idColumn, [...idSet])
|
|
732
|
+
.select(idColumn)
|
|
733
|
+
|
|
734
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
735
|
+
query,
|
|
736
|
+
resourceName,
|
|
737
|
+
tableName: descriptor.canonical.tableName,
|
|
738
|
+
context,
|
|
739
|
+
queryPurpose: 'relationship-identifiers'
|
|
740
|
+
})
|
|
741
|
+
query = scopedQueryState.query
|
|
742
|
+
|
|
743
|
+
const rows = await query
|
|
744
|
+
for (const row of rows) {
|
|
745
|
+
visibleKeys.add(`${resourceName}:${String(getLogicalResourceId(row, descriptor))}`)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return identifiers.filter((identifier) => (
|
|
750
|
+
visibleKeys.has(`${identifier.type}:${String(identifier.id)}`)
|
|
751
|
+
))
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const ensureTargetsExist = async ({ relInfo, relData, context }) => {
|
|
628
755
|
const relArray = Array.isArray(relData) ? relData : []
|
|
629
756
|
if (relArray.length === 0) return
|
|
630
757
|
const targetScope = api.resources[relInfo.target]
|
|
@@ -632,13 +759,17 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
632
759
|
throw new Error(`Target resource '${relInfo.target}' not found`)
|
|
633
760
|
}
|
|
634
761
|
for (const identifier of relArray) {
|
|
635
|
-
if (
|
|
762
|
+
if (identifier?.id === undefined || identifier.id === null) {
|
|
636
763
|
throw new Error('Relationship data requires resource identifier with id')
|
|
637
764
|
}
|
|
638
765
|
if (identifier.type && identifier.type !== relInfo.target) {
|
|
639
766
|
throw new Error(`Relationship expects type '${relInfo.target}' but received '${identifier.type}'`)
|
|
640
767
|
}
|
|
641
|
-
await targetScope.get({
|
|
768
|
+
await targetScope.get({
|
|
769
|
+
id: identifier.id,
|
|
770
|
+
transaction: context.transaction,
|
|
771
|
+
simplified: false
|
|
772
|
+
}, { ...context })
|
|
642
773
|
}
|
|
643
774
|
}
|
|
644
775
|
|
|
@@ -697,7 +828,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
697
828
|
}
|
|
698
829
|
const db = context.transaction || context.db || api.knex.instance
|
|
699
830
|
const leftId = normalizeId(context.id)
|
|
700
|
-
await ensureTargetsExist({ relInfo: info.relInfo, relData,
|
|
831
|
+
await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
|
|
701
832
|
await attachLinks({
|
|
702
833
|
descriptor: info.descriptor,
|
|
703
834
|
relInfo: info.relInfo,
|
|
@@ -715,7 +846,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
715
846
|
}
|
|
716
847
|
const db = context.transaction || context.db || api.knex.instance
|
|
717
848
|
const leftId = normalizeId(context.id)
|
|
718
|
-
await ensureTargetsExist({ relInfo: info.relInfo, relData,
|
|
849
|
+
await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
|
|
719
850
|
if (isUpdate) {
|
|
720
851
|
await syncLinks({
|
|
721
852
|
descriptor: info.descriptor,
|
|
@@ -762,7 +893,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
762
893
|
}
|
|
763
894
|
const db = context.transaction || context.db || api.knex.instance
|
|
764
895
|
const leftId = normalizeId(context.id)
|
|
765
|
-
|
|
896
|
+
const identifiers = await listLinks({
|
|
766
897
|
descriptor: info.descriptor,
|
|
767
898
|
relInfo: info.relInfo,
|
|
768
899
|
relationshipKey: info.relationshipKey,
|
|
@@ -770,6 +901,10 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
770
901
|
leftId,
|
|
771
902
|
db,
|
|
772
903
|
})
|
|
904
|
+
return filterVisibleIdentifiers({
|
|
905
|
+
identifiers,
|
|
906
|
+
context
|
|
907
|
+
})
|
|
773
908
|
},
|
|
774
909
|
fetchManyToManyRows: async ({ scopeName, relName, parentIds, context }) => {
|
|
775
910
|
const info = await getManyToManyInfo(scopeName, relName)
|
|
@@ -891,12 +1026,13 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
891
1026
|
const descriptor = await getDescriptor(scopeName)
|
|
892
1027
|
const { canonical } = descriptor
|
|
893
1028
|
const id = context.id
|
|
1029
|
+
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
894
1030
|
|
|
895
1031
|
const row = await context.db(canonical.tableName)
|
|
896
1032
|
.select('id')
|
|
897
1033
|
.where(canonical.tenantColumn, descriptor.tenant)
|
|
898
1034
|
.where(canonical.resourceColumn, descriptor.resource)
|
|
899
|
-
.where(
|
|
1035
|
+
.where(logicalIdColumn, id)
|
|
900
1036
|
.first()
|
|
901
1037
|
|
|
902
1038
|
return !!row
|
|
@@ -909,10 +1045,17 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
909
1045
|
}
|
|
910
1046
|
const descriptor = await getDescriptor(scopeName)
|
|
911
1047
|
const { canonical } = descriptor
|
|
1048
|
+
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1049
|
+
const explicitId = context.inputRecord?.data?.id
|
|
912
1050
|
const attributes = context.inputRecord?.data?.attributes || {}
|
|
913
1051
|
const row = storageAdapter?.toStorageRow
|
|
914
1052
|
? storageAdapter.toStorageRow(attributes)
|
|
915
1053
|
: translateCanonicalAttributesForStorage(attributes, descriptor)
|
|
1054
|
+
|
|
1055
|
+
if (explicitId !== undefined && explicitId !== null) {
|
|
1056
|
+
row[logicalIdColumn] = explicitId
|
|
1057
|
+
}
|
|
1058
|
+
|
|
916
1059
|
row[canonical.tenantColumn] = descriptor.tenant
|
|
917
1060
|
row[canonical.resourceColumn] = descriptor.resource
|
|
918
1061
|
|
|
@@ -921,10 +1064,25 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
921
1064
|
.returning('id')
|
|
922
1065
|
|
|
923
1066
|
const inserted = Array.isArray(result) ? result[0] : result
|
|
924
|
-
|
|
925
|
-
|
|
1067
|
+
const insertedRowId = inserted && typeof inserted === 'object' && 'id' in inserted
|
|
1068
|
+
? inserted.id
|
|
1069
|
+
: inserted
|
|
1070
|
+
|
|
1071
|
+
if (explicitId !== undefined && explicitId !== null) {
|
|
1072
|
+
return explicitId
|
|
926
1073
|
}
|
|
927
|
-
|
|
1074
|
+
|
|
1075
|
+
if (insertedRowId === undefined || insertedRowId === null) {
|
|
1076
|
+
return insertedRowId
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const generatedLogicalId = String(insertedRowId)
|
|
1080
|
+
|
|
1081
|
+
await context.db(canonical.tableName)
|
|
1082
|
+
.where({ id: insertedRowId })
|
|
1083
|
+
.update({ [logicalIdColumn]: generatedLogicalId })
|
|
1084
|
+
|
|
1085
|
+
return generatedLogicalId
|
|
928
1086
|
}
|
|
929
1087
|
|
|
930
1088
|
helpers.dataPut = async ({ scopeName, context }) => {
|
|
@@ -935,6 +1093,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
935
1093
|
const descriptor = await getDescriptor(scopeName)
|
|
936
1094
|
const { canonical } = descriptor
|
|
937
1095
|
const id = context.id
|
|
1096
|
+
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
938
1097
|
const attributes = context.inputRecord?.data?.attributes || {}
|
|
939
1098
|
const row = storageAdapter?.toStorageRow
|
|
940
1099
|
? storageAdapter.toStorageRow(attributes)
|
|
@@ -943,19 +1102,27 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
943
1102
|
row[canonical.resourceColumn] = descriptor.resource
|
|
944
1103
|
row[canonical.tenantColumn] = descriptor.tenant
|
|
945
1104
|
|
|
946
|
-
const
|
|
1105
|
+
const writeRow = Object.fromEntries(
|
|
947
1106
|
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
948
1107
|
)
|
|
949
1108
|
|
|
950
|
-
if (
|
|
1109
|
+
if (context.isCreate) {
|
|
1110
|
+
await context.db(canonical.tableName).insert({
|
|
1111
|
+
...writeRow,
|
|
1112
|
+
[logicalIdColumn]: id,
|
|
1113
|
+
})
|
|
1114
|
+
return 1
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (Object.keys(writeRow).length === 0) {
|
|
951
1118
|
return 0
|
|
952
1119
|
}
|
|
953
1120
|
|
|
954
1121
|
const result = await context.db(canonical.tableName)
|
|
955
|
-
.where(
|
|
1122
|
+
.where(logicalIdColumn, id)
|
|
956
1123
|
.where(canonical.resourceColumn, descriptor.resource)
|
|
957
1124
|
.where(canonical.tenantColumn, descriptor.tenant)
|
|
958
|
-
.update(
|
|
1125
|
+
.update(writeRow)
|
|
959
1126
|
|
|
960
1127
|
return result
|
|
961
1128
|
}
|
|
@@ -968,6 +1135,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
968
1135
|
const descriptor = await getDescriptor(scopeName)
|
|
969
1136
|
const { canonical } = descriptor
|
|
970
1137
|
const id = context.id
|
|
1138
|
+
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
971
1139
|
const attributes = context.inputRecord?.data?.attributes || {}
|
|
972
1140
|
const row = storageAdapter?.toStorageRow
|
|
973
1141
|
? storageAdapter.toStorageRow(attributes)
|
|
@@ -982,7 +1150,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
982
1150
|
}
|
|
983
1151
|
|
|
984
1152
|
const result = await context.db(canonical.tableName)
|
|
985
|
-
.where(
|
|
1153
|
+
.where(logicalIdColumn, id)
|
|
986
1154
|
.where(canonical.resourceColumn, descriptor.resource)
|
|
987
1155
|
.where(canonical.tenantColumn, descriptor.tenant)
|
|
988
1156
|
.update(updateRow)
|
|
@@ -998,9 +1166,10 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
998
1166
|
const descriptor = await getDescriptor(scopeName)
|
|
999
1167
|
const { canonical } = descriptor
|
|
1000
1168
|
const id = context.id
|
|
1169
|
+
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1001
1170
|
|
|
1002
1171
|
const result = await context.db(canonical.tableName)
|
|
1003
|
-
.where(
|
|
1172
|
+
.where(logicalIdColumn, id)
|
|
1004
1173
|
.where(canonical.resourceColumn, descriptor.resource)
|
|
1005
1174
|
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1006
1175
|
.delete()
|
|
@@ -1008,7 +1177,14 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1008
1177
|
return result
|
|
1009
1178
|
}
|
|
1010
1179
|
|
|
1011
|
-
helpers.dataGetMinimal = async ({
|
|
1180
|
+
helpers.dataGetMinimal = async ({
|
|
1181
|
+
scopeName,
|
|
1182
|
+
context,
|
|
1183
|
+
runHooks,
|
|
1184
|
+
applyQueryFilters,
|
|
1185
|
+
filters = context.queryParams?.filters,
|
|
1186
|
+
queryPurpose = 'single'
|
|
1187
|
+
}) => {
|
|
1012
1188
|
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1013
1189
|
if (storageAdapter) {
|
|
1014
1190
|
context.storageAdapter = storageAdapter
|
|
@@ -1016,41 +1192,51 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1016
1192
|
const descriptor = await getDescriptor(scopeName)
|
|
1017
1193
|
const { canonical } = descriptor
|
|
1018
1194
|
const id = context.id
|
|
1195
|
+
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1019
1196
|
const scope = api.resources?.[scopeName]
|
|
1020
1197
|
|
|
1021
1198
|
let query = context.db(canonical.tableName)
|
|
1022
|
-
.where(
|
|
1199
|
+
.where(logicalIdColumn, id)
|
|
1023
1200
|
.where(canonical.resourceColumn, descriptor.resource)
|
|
1024
1201
|
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1025
1202
|
|
|
1026
1203
|
if (typeof applyQueryFilters === 'function') {
|
|
1027
1204
|
const scopedQueryState = await applyQueryFilters({
|
|
1028
1205
|
query,
|
|
1029
|
-
filters
|
|
1206
|
+
filters,
|
|
1030
1207
|
tableName: canonical.tableName,
|
|
1031
1208
|
db: context.db,
|
|
1032
1209
|
scopeName,
|
|
1210
|
+
queryPurpose,
|
|
1033
1211
|
storageAdapter,
|
|
1034
1212
|
isAnyApi: true
|
|
1035
1213
|
})
|
|
1036
1214
|
query = unwrapQueryBuilderState(scopedQueryState, query)
|
|
1037
1215
|
} else if (runHooks) {
|
|
1216
|
+
const previousKnexQuery = context.knexQuery
|
|
1038
1217
|
context.knexQuery = {
|
|
1039
1218
|
query,
|
|
1040
|
-
filters
|
|
1219
|
+
filters,
|
|
1041
1220
|
schemaInfo: context.schemaInfo,
|
|
1042
1221
|
scopeName,
|
|
1043
1222
|
tableName: canonical.tableName,
|
|
1044
1223
|
db: context.db,
|
|
1224
|
+
queryPurpose,
|
|
1045
1225
|
isAnyApi: true,
|
|
1046
1226
|
adapter: storageAdapter,
|
|
1047
1227
|
storageAdapter,
|
|
1048
1228
|
}
|
|
1049
1229
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1230
|
+
try {
|
|
1231
|
+
await applyBuiltInAnyApiQueryFilters(context)
|
|
1232
|
+
await runHooks('knexQueryFiltering')
|
|
1233
|
+
query = context.knexQuery?.query || query
|
|
1234
|
+
} finally {
|
|
1235
|
+
if (previousKnexQuery === undefined) {
|
|
1236
|
+
delete context.knexQuery
|
|
1237
|
+
} else {
|
|
1238
|
+
context.knexQuery = previousKnexQuery
|
|
1239
|
+
}
|
|
1054
1240
|
}
|
|
1055
1241
|
}
|
|
1056
1242
|
|
|
@@ -1062,16 +1248,15 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1062
1248
|
descriptor,
|
|
1063
1249
|
{ allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
|
|
1064
1250
|
)
|
|
1251
|
+
const resourceId = getLogicalResourceId(row, descriptor)
|
|
1065
1252
|
const minimal = {
|
|
1066
1253
|
type: descriptor.resource,
|
|
1067
|
-
id:
|
|
1254
|
+
id: resourceId,
|
|
1068
1255
|
attributes: translated.attributes,
|
|
1069
1256
|
relationships: translated.relationships,
|
|
1070
1257
|
}
|
|
1071
1258
|
if (scope) {
|
|
1072
|
-
minimal
|
|
1073
|
-
self: buildResourceUrl(context, scope, scopeName, minimal.id),
|
|
1074
|
-
}
|
|
1259
|
+
decorateResourceLinks({ resource: minimal, scope, scopeName, context })
|
|
1075
1260
|
}
|
|
1076
1261
|
return minimal
|
|
1077
1262
|
}
|
|
@@ -1136,43 +1321,16 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1136
1321
|
}
|
|
1137
1322
|
}
|
|
1138
1323
|
|
|
1139
|
-
const applyTargetScopeFilters = async ({ query, resourceName, tableName, context, filters }) => {
|
|
1140
|
-
const targetScope = api.resources?.[resourceName]
|
|
1141
|
-
if (!targetScope?.applyQueryFilters) {
|
|
1142
|
-
return { query }
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const storageAdapter = getScopeStorageAdapter(resourceName)
|
|
1146
|
-
const queryState = await targetScope.applyQueryFilters({
|
|
1147
|
-
query,
|
|
1148
|
-
filters,
|
|
1149
|
-
schemaInfo: targetScope.vars?.schemaInfo,
|
|
1150
|
-
scopeName: resourceName,
|
|
1151
|
-
tableName,
|
|
1152
|
-
db: context.db || context.transaction || api.knex.instance,
|
|
1153
|
-
isAnyApi: true,
|
|
1154
|
-
storageAdapter
|
|
1155
|
-
}, {
|
|
1156
|
-
...context,
|
|
1157
|
-
scopeName: resourceName,
|
|
1158
|
-
schemaInfo: targetScope.vars?.schemaInfo,
|
|
1159
|
-
storageAdapter
|
|
1160
|
-
})
|
|
1161
|
-
|
|
1162
|
-
return {
|
|
1163
|
-
query: unwrapQueryBuilderState(queryState, query)
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
1324
|
const buildIncludedResource = ({ row, descriptor, context, fieldSelectionInfo = null }) => {
|
|
1168
1325
|
const translated = translateCanonicalRecordFromStorage(
|
|
1169
1326
|
row,
|
|
1170
1327
|
descriptor,
|
|
1171
1328
|
{ allowedExtraFields: getAllowedQueryFieldNames(descriptor.resource) }
|
|
1172
1329
|
)
|
|
1330
|
+
const resourceId = getLogicalResourceId(row, descriptor)
|
|
1173
1331
|
const includeResource = {
|
|
1174
1332
|
type: descriptor.resource,
|
|
1175
|
-
id:
|
|
1333
|
+
id: resourceId,
|
|
1176
1334
|
attributes: translated.attributes,
|
|
1177
1335
|
}
|
|
1178
1336
|
|
|
@@ -1186,8 +1344,12 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1186
1344
|
|
|
1187
1345
|
const targetScope = api.resources?.[descriptor.resource]
|
|
1188
1346
|
if (targetScope) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1347
|
+
decorateResourceLinks({
|
|
1348
|
+
resource: includeResource,
|
|
1349
|
+
scope: targetScope,
|
|
1350
|
+
scopeName: descriptor.resource,
|
|
1351
|
+
context,
|
|
1352
|
+
})
|
|
1191
1353
|
}
|
|
1192
1354
|
|
|
1193
1355
|
return includeResource
|
|
@@ -1216,7 +1378,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1216
1378
|
let query = db(targetDescriptor.canonical.tableName)
|
|
1217
1379
|
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1218
1380
|
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1219
|
-
.whereIn(
|
|
1381
|
+
.whereIn(getLogicalResourceIdColumn(targetDescriptor), ids)
|
|
1220
1382
|
const selectionState = await applyIncludeFieldSelection({
|
|
1221
1383
|
query,
|
|
1222
1384
|
resourceName: targetDescriptor.resource,
|
|
@@ -1224,11 +1386,19 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1224
1386
|
context
|
|
1225
1387
|
})
|
|
1226
1388
|
query = selectionState.query
|
|
1389
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
1390
|
+
query,
|
|
1391
|
+
resourceName: targetDescriptor.resource,
|
|
1392
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
1393
|
+
context
|
|
1394
|
+
})
|
|
1395
|
+
query = scopedQueryState.query
|
|
1227
1396
|
const rows = await query
|
|
1228
1397
|
|
|
1229
1398
|
const newResources = []
|
|
1230
1399
|
for (const row of rows) {
|
|
1231
|
-
const
|
|
1400
|
+
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1401
|
+
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1232
1402
|
if (seen.has(includeKey)) continue
|
|
1233
1403
|
seen.add(includeKey)
|
|
1234
1404
|
const includeResource = buildIncludedResource({
|
|
@@ -1284,7 +1454,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1284
1454
|
let query = db(targetDescriptor.canonical.tableName)
|
|
1285
1455
|
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1286
1456
|
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1287
|
-
.whereIn(
|
|
1457
|
+
.whereIn(getLogicalResourceIdColumn(targetDescriptor), [...idSet])
|
|
1288
1458
|
const selectionState = await applyIncludeFieldSelection({
|
|
1289
1459
|
query,
|
|
1290
1460
|
resourceName: targetDescriptor.resource,
|
|
@@ -1292,10 +1462,18 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1292
1462
|
context
|
|
1293
1463
|
})
|
|
1294
1464
|
query = selectionState.query
|
|
1465
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
1466
|
+
query,
|
|
1467
|
+
resourceName: targetDescriptor.resource,
|
|
1468
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
1469
|
+
context
|
|
1470
|
+
})
|
|
1471
|
+
query = scopedQueryState.query
|
|
1295
1472
|
const rows = await query
|
|
1296
1473
|
|
|
1297
1474
|
for (const row of rows) {
|
|
1298
|
-
const
|
|
1475
|
+
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1476
|
+
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1299
1477
|
if (seen.has(includeKey)) continue
|
|
1300
1478
|
seen.add(includeKey)
|
|
1301
1479
|
const includeResource = buildIncludedResource({
|
|
@@ -1432,7 +1610,8 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1432
1610
|
|
|
1433
1611
|
const newResources = []
|
|
1434
1612
|
for (const row of rows) {
|
|
1435
|
-
const
|
|
1613
|
+
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1614
|
+
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1436
1615
|
if (seen.has(includeKey)) continue
|
|
1437
1616
|
seen.add(includeKey)
|
|
1438
1617
|
const includeResource = buildIncludedResource({
|
|
@@ -1481,7 +1660,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1481
1660
|
let query = db(targetDescriptor.canonical.tableName)
|
|
1482
1661
|
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1483
1662
|
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1484
|
-
.whereIn(
|
|
1663
|
+
.whereIn(getLogicalResourceIdColumn(targetDescriptor), childIds)
|
|
1485
1664
|
const selectionState = await applyIncludeFieldSelection({
|
|
1486
1665
|
query,
|
|
1487
1666
|
resourceName: targetDescriptor.resource,
|
|
@@ -1489,11 +1668,19 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1489
1668
|
context
|
|
1490
1669
|
})
|
|
1491
1670
|
query = selectionState.query
|
|
1671
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
1672
|
+
query,
|
|
1673
|
+
resourceName: targetDescriptor.resource,
|
|
1674
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
1675
|
+
context
|
|
1676
|
+
})
|
|
1677
|
+
query = scopedQueryState.query
|
|
1492
1678
|
const rows = await query
|
|
1493
1679
|
|
|
1494
1680
|
const newResources = []
|
|
1495
1681
|
for (const row of rows) {
|
|
1496
|
-
const
|
|
1682
|
+
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1683
|
+
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1497
1684
|
if (seen.has(includeKey)) continue
|
|
1498
1685
|
seen.add(includeKey)
|
|
1499
1686
|
const includeResource = buildIncludedResource({
|
|
@@ -1577,7 +1764,8 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1577
1764
|
query,
|
|
1578
1765
|
resourceName: targetDescriptor.resource,
|
|
1579
1766
|
tableName: targetDescriptor.canonical.tableName,
|
|
1580
|
-
context
|
|
1767
|
+
context,
|
|
1768
|
+
queryPurpose: 'relationship-identifiers'
|
|
1581
1769
|
})
|
|
1582
1770
|
query = scopedQueryState.query
|
|
1583
1771
|
const rows = await query
|
|
@@ -1587,7 +1775,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1587
1775
|
if (parentId == null) return acc
|
|
1588
1776
|
const key = String(parentId)
|
|
1589
1777
|
acc[key] = acc[key] || []
|
|
1590
|
-
acc[key].push({ type: targetDescriptor.resource, id:
|
|
1778
|
+
acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
|
|
1591
1779
|
return acc
|
|
1592
1780
|
}, {})
|
|
1593
1781
|
|
|
@@ -1623,7 +1811,8 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1623
1811
|
query,
|
|
1624
1812
|
resourceName: targetDescriptor.resource,
|
|
1625
1813
|
tableName: targetDescriptor.canonical.tableName,
|
|
1626
|
-
context
|
|
1814
|
+
context,
|
|
1815
|
+
queryPurpose: 'relationship-identifiers'
|
|
1627
1816
|
})
|
|
1628
1817
|
query = scopedQueryState.query
|
|
1629
1818
|
const rows = await query
|
|
@@ -1633,7 +1822,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1633
1822
|
if (parentId == null) return acc
|
|
1634
1823
|
const key = String(parentId)
|
|
1635
1824
|
acc[key] = acc[key] || []
|
|
1636
|
-
acc[key].push({ type: targetDescriptor.resource, id:
|
|
1825
|
+
acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
|
|
1637
1826
|
return acc
|
|
1638
1827
|
}, {})
|
|
1639
1828
|
|
|
@@ -1654,16 +1843,25 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1654
1843
|
const db = context.db || context.transaction || api.knex.instance
|
|
1655
1844
|
const parentIds = resources.map((resource) => resource.id)
|
|
1656
1845
|
|
|
1657
|
-
for (const [relName
|
|
1846
|
+
for (const [relName] of manyEntries) {
|
|
1658
1847
|
const info = await getManyToManyInfo(descriptor.resource, relName)
|
|
1659
1848
|
if (!info) continue
|
|
1660
|
-
|
|
1849
|
+
let rows = await fetchLinksForParents({
|
|
1661
1850
|
descriptor: info.descriptor,
|
|
1662
1851
|
relationshipKey: info.relationshipKey,
|
|
1663
1852
|
parentIds,
|
|
1664
1853
|
db,
|
|
1665
1854
|
})
|
|
1666
1855
|
|
|
1856
|
+
const visibleIdentifiers = await filterVisibleIdentifiers({
|
|
1857
|
+
identifiers: rows.map((row) => ({ type: row.childType, id: row.childId })),
|
|
1858
|
+
context
|
|
1859
|
+
})
|
|
1860
|
+
const visibleKeys = new Set(
|
|
1861
|
+
visibleIdentifiers.map((identifier) => `${identifier.type}:${String(identifier.id)}`)
|
|
1862
|
+
)
|
|
1863
|
+
rows = rows.filter((row) => visibleKeys.has(`${row.childType}:${String(row.childId)}`))
|
|
1864
|
+
|
|
1667
1865
|
const grouped = rows.reduce((acc, row) => {
|
|
1668
1866
|
const key = String(row.parentId)
|
|
1669
1867
|
acc[key] = acc[key] || []
|
|
@@ -1687,6 +1885,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1687
1885
|
const descriptor = await getDescriptor(scopeName)
|
|
1688
1886
|
const { canonical } = descriptor
|
|
1689
1887
|
const id = context.id
|
|
1888
|
+
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1690
1889
|
const scope = api.resources?.[scopeName]
|
|
1691
1890
|
|
|
1692
1891
|
let fieldSelectionInfo = null
|
|
@@ -1698,7 +1897,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1698
1897
|
}
|
|
1699
1898
|
|
|
1700
1899
|
let query = context.db(canonical.tableName)
|
|
1701
|
-
.where(
|
|
1900
|
+
.where(logicalIdColumn, id)
|
|
1702
1901
|
.where(canonical.resourceColumn, descriptor.resource)
|
|
1703
1902
|
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1704
1903
|
|
|
@@ -1726,9 +1925,10 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1726
1925
|
descriptor,
|
|
1727
1926
|
{ allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
|
|
1728
1927
|
)
|
|
1928
|
+
const resourceId = getLogicalResourceId(row, descriptor)
|
|
1729
1929
|
const data = {
|
|
1730
1930
|
type: descriptor.resource,
|
|
1731
|
-
id:
|
|
1931
|
+
id: resourceId,
|
|
1732
1932
|
attributes: record.attributes,
|
|
1733
1933
|
}
|
|
1734
1934
|
|
|
@@ -1760,24 +1960,20 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1760
1960
|
}
|
|
1761
1961
|
|
|
1762
1962
|
if (scope) {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
response.links = {
|
|
1766
|
-
self: data.links.self,
|
|
1767
|
-
}
|
|
1963
|
+
decorateResourceLinks({ resource: data, scope, scopeName, context })
|
|
1964
|
+
response.links = { self: data.links.self }
|
|
1768
1965
|
}
|
|
1769
1966
|
|
|
1770
1967
|
if (included.length > 0) {
|
|
1771
1968
|
for (const includeResource of included) {
|
|
1772
1969
|
const targetScope = api.resources?.[includeResource.type]
|
|
1773
1970
|
if (!targetScope) continue
|
|
1774
|
-
|
|
1775
|
-
|
|
1971
|
+
decorateResourceLinks({
|
|
1972
|
+
resource: includeResource,
|
|
1973
|
+
scope: targetScope,
|
|
1974
|
+
scopeName: includeResource.type,
|
|
1776
1975
|
context,
|
|
1777
|
-
|
|
1778
|
-
includeResource.type,
|
|
1779
|
-
includeResource.id
|
|
1780
|
-
)
|
|
1976
|
+
})
|
|
1781
1977
|
}
|
|
1782
1978
|
}
|
|
1783
1979
|
|
|
@@ -1846,6 +2042,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1846
2042
|
scope.vars?.idProperty ||
|
|
1847
2043
|
vars.idProperty ||
|
|
1848
2044
|
'id'
|
|
2045
|
+
descriptor.idProperty = idProperty
|
|
1849
2046
|
|
|
1850
2047
|
scope.vars.schemaInfo = {
|
|
1851
2048
|
...existingInfo,
|
|
@@ -1894,32 +2091,14 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1894
2091
|
return { resourceToTableName, tableNameToResource }
|
|
1895
2092
|
}
|
|
1896
2093
|
|
|
1897
|
-
const
|
|
1898
|
-
get (_target, prop) {
|
|
1899
|
-
if (typeof prop !== 'string') {
|
|
1900
|
-
return undefined
|
|
1901
|
-
}
|
|
1902
|
-
const resource = api.resources?.[prop]
|
|
1903
|
-
if (resource?.vars?.schemaInfo) {
|
|
1904
|
-
return resource
|
|
1905
|
-
}
|
|
1906
|
-
return scopes?.[prop]
|
|
1907
|
-
},
|
|
1908
|
-
})
|
|
1909
|
-
|
|
1910
|
-
const queryHookDependencies = { log, scopes: scopesProxy, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
1911
|
-
|
|
1912
|
-
addHook('knexQueryFiltering', 'polymorphicFiltersHook', {}, async (hookParams) =>
|
|
1913
|
-
polymorphicFiltersHook(hookParams, queryHookDependencies)
|
|
1914
|
-
)
|
|
1915
|
-
|
|
1916
|
-
addHook('knexQueryFiltering', 'crossTableFiltersHook', {}, async (hookParams) =>
|
|
1917
|
-
crossTableFiltersHook(hookParams, queryHookDependencies)
|
|
1918
|
-
)
|
|
2094
|
+
const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
1919
2095
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2096
|
+
const applyBuiltInAnyApiQueryFilters = async (context) => {
|
|
2097
|
+
const hookParams = { context }
|
|
2098
|
+
await polymorphicFiltersHook(hookParams, queryHookDependencies)
|
|
2099
|
+
await crossTableFiltersHook(hookParams, queryHookDependencies)
|
|
2100
|
+
await basicFiltersHook(hookParams, queryHookDependencies)
|
|
2101
|
+
}
|
|
1923
2102
|
|
|
1924
2103
|
helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
|
|
1925
2104
|
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
@@ -1971,11 +2150,14 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1971
2150
|
scopeName,
|
|
1972
2151
|
tableName: tableNameForHooks,
|
|
1973
2152
|
db,
|
|
2153
|
+
queryPurpose: 'collection',
|
|
1974
2154
|
isAnyApi: true,
|
|
1975
2155
|
adapter,
|
|
1976
2156
|
storageAdapter,
|
|
1977
2157
|
}
|
|
1978
2158
|
|
|
2159
|
+
await applyBuiltInAnyApiQueryFilters(context)
|
|
2160
|
+
|
|
1979
2161
|
if (runHooks) {
|
|
1980
2162
|
await runHooks('knexQueryFiltering')
|
|
1981
2163
|
}
|
|
@@ -1984,19 +2166,20 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
1984
2166
|
|
|
1985
2167
|
const countQuery = queryBuilder.clone()
|
|
1986
2168
|
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2169
|
+
let selectionState = { query: queryBuilder, queryFieldRuntimeByField: new Map() }
|
|
2170
|
+
if (scope && fieldSelectionInfo) {
|
|
2171
|
+
selectionState = await applyFieldSelectionToQuery({
|
|
2172
|
+
query: queryBuilder,
|
|
2173
|
+
scope,
|
|
2174
|
+
fieldSelectionInfo,
|
|
2175
|
+
tableName: adapter.tableAlias,
|
|
2176
|
+
useTablePrefix: false,
|
|
2177
|
+
storageAdapter,
|
|
2178
|
+
db,
|
|
2179
|
+
context,
|
|
2180
|
+
scopeName
|
|
2181
|
+
})
|
|
2182
|
+
}
|
|
2000
2183
|
|
|
2001
2184
|
const sortDescriptors = applySortingToQuery({
|
|
2002
2185
|
query: selectionState.query,
|
|
@@ -2006,9 +2189,6 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2006
2189
|
queryFieldRuntimeByField: selectionState.queryFieldRuntimeByField
|
|
2007
2190
|
})
|
|
2008
2191
|
|
|
2009
|
-
if (queryParams?.page?.after) {
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
2192
|
const paginationInfo = applyPaginationToQuery({
|
|
2013
2193
|
query: queryBuilder,
|
|
2014
2194
|
scope,
|
|
@@ -2027,7 +2207,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2027
2207
|
const record = {}
|
|
2028
2208
|
for (const descriptorEntry of cursorDescriptors) {
|
|
2029
2209
|
if (descriptorEntry.field === 'id') {
|
|
2030
|
-
record.id = row
|
|
2210
|
+
record.id = getLogicalResourceId(row, descriptor)
|
|
2031
2211
|
} else if (descriptorEntry.queryFieldRuntime) {
|
|
2032
2212
|
record[descriptorEntry.field] = row[descriptorEntry.field]
|
|
2033
2213
|
} else if (descriptorEntry.column) {
|
|
@@ -2050,9 +2230,10 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2050
2230
|
descriptor,
|
|
2051
2231
|
{ allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
|
|
2052
2232
|
)
|
|
2233
|
+
const resourceId = getLogicalResourceId(row, descriptor)
|
|
2053
2234
|
const resource = {
|
|
2054
2235
|
type: descriptor.resource,
|
|
2055
|
-
id:
|
|
2236
|
+
id: resourceId,
|
|
2056
2237
|
attributes: translated.attributes,
|
|
2057
2238
|
}
|
|
2058
2239
|
if (translated.relationships && Object.keys(translated.relationships).length > 0) {
|
|
@@ -2060,8 +2241,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2060
2241
|
}
|
|
2061
2242
|
const resourceScope = scope
|
|
2062
2243
|
if (resourceScope) {
|
|
2063
|
-
resource
|
|
2064
|
-
resource.links.self = buildResourceUrl(context, resourceScope, scopeName, resource.id)
|
|
2244
|
+
decorateResourceLinks({ resource, scope: resourceScope, scopeName, context })
|
|
2065
2245
|
}
|
|
2066
2246
|
if (fieldSelectionInfo?.computedDependencies?.length) {
|
|
2067
2247
|
resource.__$jsonrestapi_computed_deps$__ = fieldSelectionInfo.computedDependencies
|
|
@@ -2092,8 +2272,22 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2092
2272
|
response.included = included
|
|
2093
2273
|
}
|
|
2094
2274
|
|
|
2275
|
+
if (included.length > 0) {
|
|
2276
|
+
for (const includeResource of included) {
|
|
2277
|
+
const targetScope = api.resources?.[includeResource.type]
|
|
2278
|
+
if (!targetScope) continue
|
|
2279
|
+
decorateResourceLinks({
|
|
2280
|
+
resource: includeResource,
|
|
2281
|
+
scope: targetScope,
|
|
2282
|
+
scopeName: includeResource.type,
|
|
2283
|
+
context,
|
|
2284
|
+
})
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2095
2288
|
context.returnMeta = context.returnMeta || {}
|
|
2096
|
-
|
|
2289
|
+
const queryString = serializeJsonApiQuery(queryParams)
|
|
2290
|
+
context.returnMeta.queryString = queryString ? `?${queryString}` : ''
|
|
2097
2291
|
delete context.returnMeta.paginationMeta
|
|
2098
2292
|
delete context.returnMeta.paginationLinks
|
|
2099
2293
|
|
|
@@ -2123,7 +2317,10 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2123
2317
|
cursorRecords,
|
|
2124
2318
|
paginationInfo.pageSize,
|
|
2125
2319
|
hasMore,
|
|
2126
|
-
cursorFields
|
|
2320
|
+
cursorFields,
|
|
2321
|
+
{
|
|
2322
|
+
schemaInfo
|
|
2323
|
+
}
|
|
2127
2324
|
)
|
|
2128
2325
|
context.returnMeta.paginationMeta = paginationMeta
|
|
2129
2326
|
const urlPrefix = getUrlPrefix(context, scope)
|
|
@@ -2134,7 +2331,10 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2134
2331
|
cursorRecords,
|
|
2135
2332
|
paginationInfo.pageSize,
|
|
2136
2333
|
hasMore,
|
|
2137
|
-
cursorFields
|
|
2334
|
+
cursorFields,
|
|
2335
|
+
{
|
|
2336
|
+
schemaInfo
|
|
2337
|
+
}
|
|
2138
2338
|
)
|
|
2139
2339
|
}
|
|
2140
2340
|
|
|
@@ -2180,27 +2380,30 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2180
2380
|
const schemaInfo = scope?.vars?.schemaInfo
|
|
2181
2381
|
const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias
|
|
2182
2382
|
|
|
2183
|
-
const
|
|
2184
|
-
context
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
}
|
|
2383
|
+
const countContext = {
|
|
2384
|
+
...context,
|
|
2385
|
+
knexQuery: {
|
|
2386
|
+
query: adapter.query,
|
|
2387
|
+
filters: context.queryParams?.filters,
|
|
2388
|
+
schemaInfo,
|
|
2389
|
+
scopeName,
|
|
2390
|
+
tableName: tableNameForHooks,
|
|
2391
|
+
db,
|
|
2392
|
+
queryPurpose: 'count',
|
|
2393
|
+
isAnyApi: true,
|
|
2394
|
+
adapter,
|
|
2395
|
+
storageAdapter,
|
|
2396
|
+
}
|
|
2197
2397
|
}
|
|
2198
2398
|
|
|
2399
|
+
const hookParams = { context: countContext }
|
|
2199
2400
|
await polymorphicFiltersHook(hookParams, queryHookDependencies)
|
|
2200
2401
|
await crossTableFiltersHook(hookParams, queryHookDependencies)
|
|
2201
2402
|
await basicFiltersHook(hookParams, queryHookDependencies)
|
|
2403
|
+
await api.runHooks('knexQueryFiltering', countContext)
|
|
2202
2404
|
|
|
2203
|
-
const
|
|
2405
|
+
const countQuery = countContext.knexQuery?.query || adapter.query
|
|
2406
|
+
const [{ count }] = await countQuery.count({ count: '*' })
|
|
2204
2407
|
return Number(count)
|
|
2205
2408
|
}
|
|
2206
2409
|
|
|
@@ -2236,6 +2439,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2236
2439
|
schema,
|
|
2237
2440
|
relationships,
|
|
2238
2441
|
canonicalFieldMap: canonicalFieldsMap,
|
|
2442
|
+
idProperty,
|
|
2239
2443
|
})
|
|
2240
2444
|
|
|
2241
2445
|
const updatedStored = scopeOptionsRegistry.get(scopeName) || {}
|
|
@@ -2306,6 +2510,7 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2306
2510
|
canonicalFieldMap: scopeOptions?.canonicalFieldsMap ||
|
|
2307
2511
|
scope?.scopeOptions?.canonicalFieldsMap ||
|
|
2308
2512
|
null,
|
|
2513
|
+
idProperty,
|
|
2309
2514
|
})
|
|
2310
2515
|
if (scope) {
|
|
2311
2516
|
scope.scopeOptions = {
|