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.
Files changed (106) hide show
  1. package/README.md +24 -2
  2. package/docs/API.md +157 -33
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
  10. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  11. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  12. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  13. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  14. package/docs/GUIDE/index.md +4 -1
  15. package/fixing_plan.md +112 -0
  16. package/index.js +2 -0
  17. package/lib/enhanced-logger.js +1 -1
  18. package/package.json +6 -2
  19. package/plugins/core/bulk-operations-plugin.js +31 -38
  20. package/plugins/core/connectors/express-plugin.js +6 -7
  21. package/plugins/core/connectors/fastify-plugin.js +5 -1
  22. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  23. package/plugins/core/connectors/lib/connector-core.js +17 -11
  24. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  25. package/plugins/core/file-handling-plugin.js +126 -96
  26. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  27. package/plugins/core/lib/anyapi/schema-utils.js +44 -4
  28. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
  29. package/plugins/core/lib/dbIntrospection.js +1 -1
  30. package/plugins/core/lib/dbTablesOperations.js +10 -3
  31. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  32. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
  33. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  34. package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
  35. package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
  36. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  37. package/plugins/core/lib/querying/url-helpers.js +9 -21
  38. package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
  39. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  40. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  41. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  42. package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
  43. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  44. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  45. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  46. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  47. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
  48. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  49. package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
  50. package/plugins/core/lib/storage/storage-adapter.js +76 -14
  51. package/plugins/core/lib/storage/storage-mapping.js +86 -16
  52. package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
  53. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  54. package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
  55. package/plugins/core/rest-api-knex-plugin.js +93 -76
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
  57. package/plugins/core/rest-api-plugin-methods/common.js +112 -72
  58. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
  59. package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
  60. package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
  61. package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
  62. package/plugins/core/rest-api-plugin-methods/get.js +7 -1
  63. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
  64. package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
  65. package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
  66. package/plugins/core/rest-api-plugin-methods/post.js +12 -7
  67. package/plugins/core/rest-api-plugin-methods/put.js +35 -10
  68. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  69. package/plugins/core/rest-api-plugin.js +11 -37
  70. package/plugins/core/rest-api-positioning-plugin.js +1 -4
  71. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  72. package/plugins/core/socketio-plugin.js +0 -1
  73. package/plugins/storage/local-storage.js +45 -20
  74. package/plugins/storage/s3-storage.js +23 -54
  75. package/quickTest.js +1 -0
  76. package/scripts/run-docs.js +80 -84
  77. package/test-debug.js +1 -1
  78. package/tests/README.md +8 -1
  79. package/tests/anyapi-basic.test.js +3 -0
  80. package/tests/anyapi-cursor-pagination.test.js +7 -1
  81. package/tests/anyapi-custom-idproperty.test.js +163 -0
  82. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  83. package/tests/db-introspection.test.js +4 -1
  84. package/tests/db-table-operations.test.js +25 -20
  85. package/tests/fastify-plugin.test.js +49 -6
  86. package/tests/field-setters.test.js +10 -10
  87. package/tests/file-handling.test.js +118 -0
  88. package/tests/fixtures/api-configs.js +495 -4
  89. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  90. package/tests/http-connectors-parity.test.js +165 -5
  91. package/tests/id-normalization.test.js +444 -0
  92. package/tests/local-storage.test.js +96 -0
  93. package/tests/logical-id-write-validation.test.js +151 -0
  94. package/tests/pagination-cursor-multifield.test.js +132 -11
  95. package/tests/pagination-enhanced.test.js +66 -15
  96. package/tests/post-commit-hooks.test.js +120 -0
  97. package/tests/public-exports.test.js +15 -0
  98. package/tests/put-create-parity.test.js +216 -0
  99. package/tests/relationship-endpoints.test.js +259 -0
  100. package/tests/return-record-settings.test.js +184 -0
  101. package/tests/row-policy.test.js +433 -0
  102. package/tests/s3-storage.test.js +38 -0
  103. package/tests/searchschema-merge.test.js +17 -0
  104. package/tests/socketio.test.js +62 -0
  105. package/tests/storage-mapping.test.js +56 -6
  106. 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
- // Filtering is handled by the query hooks to keep behavior consistent
130
- // (operators, null handling, multi-field, joins, polymorphic, etc.).
131
- // Avoid applying filters here to prevent duplicate conditions.
132
- if (searchSchema) {
133
- return
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
- // TODO: Probably delete the rest of the function. searchSchema is always there...!!!
137
- if (!filters || Object.keys(filters).length === 0) return
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
- // Fallback: apply minimal safe filtering when no search schema is defined
140
- for (const [field, rawValue] of Object.entries(filters)) {
141
- const fieldInfo = ensureFilterableField(descriptor, field)
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 = values.filter((value) => value !== null)
147
- const hasNull = values.length !== nonNullValues.length
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(fieldInfo.column)
215
+ query.whereNull(columnRef)
151
216
  continue
152
217
  }
153
218
  if (nonNullValues.length === 1 && !hasNull) {
154
- query.where(fieldInfo.column, nonNullValues[0])
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(fieldInfo.column, nonNullValues)
229
+ query.whereIn(columnRef, nonNullValues)
159
230
  continue
160
231
  }
161
232
  if (nonNullValues.length > 0 && hasNull) {
162
- query.where(function filterGroup () {
163
- this.whereIn(fieldInfo.column, nonNullValues).orWhereNull(fieldInfo.column)
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 || []), value)
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(value)
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 ensureTargetsExist = async ({ relInfo, relData, transaction }) => {
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 (!identifier?.id) {
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({ id: identifier.id, transaction, simplified: false })
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, transaction: context.transaction })
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, transaction: context.transaction })
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
- return listLinks({
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('id', id)
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
- if (inserted && typeof inserted === 'object' && 'id' in inserted) {
925
- return inserted.id
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
- return inserted
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 updateRow = Object.fromEntries(
1105
+ const writeRow = Object.fromEntries(
947
1106
  Object.entries(row).filter(([, value]) => value !== undefined)
948
1107
  )
949
1108
 
950
- if (Object.keys(updateRow).length === 0) {
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('id', id)
1122
+ .where(logicalIdColumn, id)
956
1123
  .where(canonical.resourceColumn, descriptor.resource)
957
1124
  .where(canonical.tenantColumn, descriptor.tenant)
958
- .update(updateRow)
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('id', id)
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('id', id)
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 ({ scopeName, context, runHooks, applyQueryFilters }) => {
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('id', id)
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: context.queryParams?.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: context.queryParams?.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
- await runHooks('knexQueryFiltering')
1051
-
1052
- if (context.knexQuery) {
1053
- delete context.knexQuery
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: String(row.id),
1254
+ id: resourceId,
1068
1255
  attributes: translated.attributes,
1069
1256
  relationships: translated.relationships,
1070
1257
  }
1071
1258
  if (scope) {
1072
- minimal.links = {
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: String(row.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
- includeResource.links = includeResource.links || {}
1190
- includeResource.links.self = buildResourceUrl(context, targetScope, descriptor.resource, includeResource.id)
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('id', ids)
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 includeKey = `${targetDescriptor.resource}:${row.id}`
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('id', [...idSet])
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 includeKey = `${targetDescriptor.resource}:${row.id}`
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 includeKey = `${targetDescriptor.resource}:${row.id}`
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('id', childIds)
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 includeKey = `${targetDescriptor.resource}:${row.id}`
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: String(row.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: String(row.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, relInfo] of manyEntries) {
1846
+ for (const [relName] of manyEntries) {
1658
1847
  const info = await getManyToManyInfo(descriptor.resource, relName)
1659
1848
  if (!info) continue
1660
- const rows = await fetchLinksForParents({
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('id', id)
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: String(row.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
- data.links = data.links || {}
1764
- data.links.self = buildResourceUrl(context, scope, scopeName, data.id)
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
- includeResource.links = includeResource.links || {}
1775
- includeResource.links.self = buildResourceUrl(
1971
+ decorateResourceLinks({
1972
+ resource: includeResource,
1973
+ scope: targetScope,
1974
+ scopeName: includeResource.type,
1776
1975
  context,
1777
- targetScope,
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 scopesProxy = new Proxy({}, {
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
- addHook('knexQueryFiltering', 'basicFiltersHook', {}, async (hookParams) =>
1921
- basicFiltersHook(hookParams, queryHookDependencies)
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
- const selectionState = scope && fieldSelectionInfo
1988
- ? await applyFieldSelectionToQuery({
1989
- query: queryBuilder,
1990
- scope,
1991
- fieldSelectionInfo,
1992
- tableName: adapter.tableAlias,
1993
- useTablePrefix: false,
1994
- storageAdapter,
1995
- db,
1996
- context,
1997
- scopeName
1998
- })
1999
- : { query: queryBuilder, queryFieldRuntimeByField: new Map() }
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.id
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: String(row.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.links = resource.links || {}
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
- context.returnMeta.queryString = buildQueryString(queryParams)
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 hookParams = {
2184
- context: {
2185
- knexQuery: {
2186
- query: adapter.query,
2187
- filters: context.queryParams?.filters,
2188
- schemaInfo,
2189
- scopeName,
2190
- tableName: tableNameForHooks,
2191
- db,
2192
- isAnyApi: true,
2193
- adapter,
2194
- storageAdapter,
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 [{ count }] = await adapter.query.count({ count: '*' })
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 = {