json-rest-api 1.0.24 → 1.0.26

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 (84) hide show
  1. package/README.md +2 -1
  2. package/docs/API.md +12 -9
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
  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_Row_Policies.md +605 -0
  10. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  11. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  12. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  13. package/docs/GUIDE/index.md +4 -1
  14. package/fixing_plan.md +112 -0
  15. package/index.js +2 -0
  16. package/package.json +1 -1
  17. package/plugins/core/bulk-operations-plugin.js +5 -4
  18. package/plugins/core/connectors/express-plugin.js +6 -1
  19. package/plugins/core/connectors/fastify-plugin.js +5 -1
  20. package/plugins/core/connectors/lib/connector-core.js +17 -11
  21. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  22. package/plugins/core/file-handling-plugin.js +126 -96
  23. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  24. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +36 -26
  25. package/plugins/core/lib/anyapi/schema-utils.js +1 -0
  26. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
  27. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
  28. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  29. package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
  30. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  31. package/plugins/core/lib/querying/storage-adapter-utils.js +8 -8
  32. package/plugins/core/lib/querying/url-helpers.js +9 -21
  33. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  34. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  35. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  36. package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
  37. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  38. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  39. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  40. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
  41. package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
  42. package/plugins/core/lib/storage/storage-adapter.js +74 -13
  43. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
  44. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  45. package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
  46. package/plugins/core/rest-api-knex-plugin.js +70 -60
  47. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
  48. package/plugins/core/rest-api-plugin-methods/common.js +85 -63
  49. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
  50. package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
  51. package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
  52. package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
  53. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
  54. package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
  55. package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
  56. package/plugins/core/rest-api-plugin-methods/post.js +4 -7
  57. package/plugins/core/rest-api-plugin-methods/put.js +4 -7
  58. package/plugins/core/rest-api-plugin.js +6 -36
  59. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  60. package/plugins/storage/local-storage.js +44 -19
  61. package/plugins/storage/s3-storage.js +21 -55
  62. package/tests/README.md +1 -1
  63. package/tests/anyapi-cursor-pagination.test.js +7 -1
  64. package/tests/anyapi-custom-idproperty.test.js +163 -0
  65. package/tests/fastify-plugin.test.js +28 -0
  66. package/tests/field-setters.test.js +10 -10
  67. package/tests/file-handling.test.js +118 -0
  68. package/tests/fixtures/api-configs.js +383 -4
  69. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  70. package/tests/http-connectors-parity.test.js +165 -5
  71. package/tests/local-storage.test.js +96 -0
  72. package/tests/pagination-cursor-multifield.test.js +121 -0
  73. package/tests/pagination-enhanced.test.js +66 -15
  74. package/tests/post-commit-hooks.test.js +120 -0
  75. package/tests/public-exports.test.js +15 -0
  76. package/tests/put-create-parity.test.js +216 -0
  77. package/tests/relationship-endpoints.test.js +259 -0
  78. package/tests/return-record-settings.test.js +184 -0
  79. package/tests/row-policy.test.js +489 -0
  80. package/tests/s3-storage.test.js +38 -0
  81. package/tests/searchschema-merge.test.js +17 -0
  82. package/tests/socketio.test.js +62 -0
  83. package/tests/storage-mapping.test.js +1 -0
  84. package/tests/test-development-guide.md +5 -5
@@ -54,6 +54,7 @@ import {
54
54
  parseSortEntry
55
55
  } from './lib/querying/query-field-sort-helpers.js'
56
56
  import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
57
+ import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
57
58
 
58
59
  const DEFAULT_TENANT = 'default'
59
60
  const LINKS_TABLE = 'any_links'
@@ -122,6 +123,14 @@ export const RestApiAnyapiKnexPlugin = {
122
123
  if (!descriptor) {
123
124
  throw new Error(`Descriptor not found for resource '${scopeName}'`)
124
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'
125
134
  return descriptor
126
135
  }
127
136
 
@@ -275,34 +284,6 @@ export const RestApiAnyapiKnexPlugin = {
275
284
  return descriptors
276
285
  }
277
286
 
278
- const buildQueryString = (queryParams = {}) => {
279
- const buildParts = (prefix, value) => {
280
- const parts = []
281
- if (Array.isArray(value)) {
282
- if (value.length === 0) return parts
283
- parts.push(`${prefix}=${value.map((item) => encodeURIComponent(item)).join(',')}`)
284
- return parts
285
- }
286
- if (value && typeof value === 'object') {
287
- for (const [key, subValue] of Object.entries(value)) {
288
- if (subValue === undefined || subValue === null) continue
289
- const newPrefix = `${prefix}[${key}]`
290
- parts.push(...buildParts(newPrefix, subValue))
291
- }
292
- return parts
293
- }
294
- if (value === undefined || value === null) return parts
295
- parts.push(`${prefix}=${encodeURIComponent(value)}`)
296
- return parts
297
- }
298
-
299
- const parts = []
300
- for (const [key, value] of Object.entries(queryParams)) {
301
- parts.push(...buildParts(key, value))
302
- }
303
- return parts.length > 0 ? `?${parts.join('&')}` : ''
304
- }
305
-
306
287
  const applyPaginationToQuery = ({
307
288
  query,
308
289
  scope,
@@ -379,13 +360,16 @@ export const RestApiAnyapiKnexPlugin = {
379
360
 
380
361
  predicateChains.forEach((chain) => {
381
362
  const chainParts = chain.map(({ descriptor, operator, value }) => {
363
+ const normalizedValue = adapter?.translateFilterValue
364
+ ? adapter.translateFilterValue(descriptor.field, value)
365
+ : value
382
366
  if (descriptor.queryFieldRuntime) {
383
- predicateBindings.push(...(descriptor.queryFieldRuntime.bindings || []), value)
367
+ predicateBindings.push(...(descriptor.queryFieldRuntime.bindings || []), normalizedValue)
384
368
  return `(${descriptor.queryFieldRuntime.sql}) ${operator} ?`
385
369
  }
386
370
 
387
371
  const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
388
- predicateBindings.push(value)
372
+ predicateBindings.push(normalizedValue)
389
373
  return `${columnRef} ${operator} ?`
390
374
  })
391
375
 
@@ -686,7 +670,88 @@ export const RestApiAnyapiKnexPlugin = {
686
670
  return results
687
671
  }
688
672
 
689
- 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 }) => {
690
755
  const relArray = Array.isArray(relData) ? relData : []
691
756
  if (relArray.length === 0) return
692
757
  const targetScope = api.resources[relInfo.target]
@@ -694,13 +759,17 @@ export const RestApiAnyapiKnexPlugin = {
694
759
  throw new Error(`Target resource '${relInfo.target}' not found`)
695
760
  }
696
761
  for (const identifier of relArray) {
697
- if (!identifier?.id) {
762
+ if (identifier?.id === undefined || identifier.id === null) {
698
763
  throw new Error('Relationship data requires resource identifier with id')
699
764
  }
700
765
  if (identifier.type && identifier.type !== relInfo.target) {
701
766
  throw new Error(`Relationship expects type '${relInfo.target}' but received '${identifier.type}'`)
702
767
  }
703
- 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 })
704
773
  }
705
774
  }
706
775
 
@@ -759,7 +828,7 @@ export const RestApiAnyapiKnexPlugin = {
759
828
  }
760
829
  const db = context.transaction || context.db || api.knex.instance
761
830
  const leftId = normalizeId(context.id)
762
- await ensureTargetsExist({ relInfo: info.relInfo, relData, transaction: context.transaction })
831
+ await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
763
832
  await attachLinks({
764
833
  descriptor: info.descriptor,
765
834
  relInfo: info.relInfo,
@@ -777,7 +846,7 @@ export const RestApiAnyapiKnexPlugin = {
777
846
  }
778
847
  const db = context.transaction || context.db || api.knex.instance
779
848
  const leftId = normalizeId(context.id)
780
- await ensureTargetsExist({ relInfo: info.relInfo, relData, transaction: context.transaction })
849
+ await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
781
850
  if (isUpdate) {
782
851
  await syncLinks({
783
852
  descriptor: info.descriptor,
@@ -824,7 +893,7 @@ export const RestApiAnyapiKnexPlugin = {
824
893
  }
825
894
  const db = context.transaction || context.db || api.knex.instance
826
895
  const leftId = normalizeId(context.id)
827
- return listLinks({
896
+ const identifiers = await listLinks({
828
897
  descriptor: info.descriptor,
829
898
  relInfo: info.relInfo,
830
899
  relationshipKey: info.relationshipKey,
@@ -832,6 +901,10 @@ export const RestApiAnyapiKnexPlugin = {
832
901
  leftId,
833
902
  db,
834
903
  })
904
+ return filterVisibleIdentifiers({
905
+ identifiers,
906
+ context
907
+ })
835
908
  },
836
909
  fetchManyToManyRows: async ({ scopeName, relName, parentIds, context }) => {
837
910
  const info = await getManyToManyInfo(scopeName, relName)
@@ -1029,11 +1102,19 @@ export const RestApiAnyapiKnexPlugin = {
1029
1102
  row[canonical.resourceColumn] = descriptor.resource
1030
1103
  row[canonical.tenantColumn] = descriptor.tenant
1031
1104
 
1032
- const updateRow = Object.fromEntries(
1105
+ const writeRow = Object.fromEntries(
1033
1106
  Object.entries(row).filter(([, value]) => value !== undefined)
1034
1107
  )
1035
1108
 
1036
- 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) {
1037
1118
  return 0
1038
1119
  }
1039
1120
 
@@ -1041,7 +1122,7 @@ export const RestApiAnyapiKnexPlugin = {
1041
1122
  .where(logicalIdColumn, id)
1042
1123
  .where(canonical.resourceColumn, descriptor.resource)
1043
1124
  .where(canonical.tenantColumn, descriptor.tenant)
1044
- .update(updateRow)
1125
+ .update(writeRow)
1045
1126
 
1046
1127
  return result
1047
1128
  }
@@ -1096,7 +1177,14 @@ export const RestApiAnyapiKnexPlugin = {
1096
1177
  return result
1097
1178
  }
1098
1179
 
1099
- 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
+ }) => {
1100
1188
  const storageAdapter = getScopeStorageAdapter(scopeName)
1101
1189
  if (storageAdapter) {
1102
1190
  context.storageAdapter = storageAdapter
@@ -1115,33 +1203,40 @@ export const RestApiAnyapiKnexPlugin = {
1115
1203
  if (typeof applyQueryFilters === 'function') {
1116
1204
  const scopedQueryState = await applyQueryFilters({
1117
1205
  query,
1118
- filters: context.queryParams?.filters,
1206
+ filters,
1119
1207
  tableName: canonical.tableName,
1120
1208
  db: context.db,
1121
1209
  scopeName,
1210
+ queryPurpose,
1122
1211
  storageAdapter,
1123
1212
  isAnyApi: true
1124
1213
  })
1125
1214
  query = unwrapQueryBuilderState(scopedQueryState, query)
1126
1215
  } else if (runHooks) {
1216
+ const previousKnexQuery = context.knexQuery
1127
1217
  context.knexQuery = {
1128
1218
  query,
1129
- filters: context.queryParams?.filters,
1219
+ filters,
1130
1220
  schemaInfo: context.schemaInfo,
1131
1221
  scopeName,
1132
1222
  tableName: canonical.tableName,
1133
1223
  db: context.db,
1224
+ queryPurpose,
1134
1225
  isAnyApi: true,
1135
1226
  adapter: storageAdapter,
1136
1227
  storageAdapter,
1137
1228
  }
1138
1229
 
1139
- await applyBuiltInAnyApiQueryFilters(context)
1140
-
1141
- await runHooks('knexQueryFiltering')
1142
-
1143
- if (context.knexQuery) {
1144
- 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
+ }
1145
1240
  }
1146
1241
  }
1147
1242
 
@@ -1226,34 +1321,6 @@ export const RestApiAnyapiKnexPlugin = {
1226
1321
  }
1227
1322
  }
1228
1323
 
1229
- const applyTargetScopeFilters = async ({ query, resourceName, tableName, context, filters }) => {
1230
- const targetScope = api.resources?.[resourceName]
1231
- if (!targetScope?.applyQueryFilters) {
1232
- return { query }
1233
- }
1234
-
1235
- const storageAdapter = getScopeStorageAdapter(resourceName)
1236
- const queryState = await targetScope.applyQueryFilters({
1237
- query,
1238
- filters,
1239
- schemaInfo: targetScope.vars?.schemaInfo,
1240
- scopeName: resourceName,
1241
- tableName,
1242
- db: context.db || context.transaction || api.knex.instance,
1243
- isAnyApi: true,
1244
- storageAdapter
1245
- }, {
1246
- ...context,
1247
- scopeName: resourceName,
1248
- schemaInfo: targetScope.vars?.schemaInfo,
1249
- storageAdapter
1250
- })
1251
-
1252
- return {
1253
- query: unwrapQueryBuilderState(queryState, query)
1254
- }
1255
- }
1256
-
1257
1324
  const buildIncludedResource = ({ row, descriptor, context, fieldSelectionInfo = null }) => {
1258
1325
  const translated = translateCanonicalRecordFromStorage(
1259
1326
  row,
@@ -1319,6 +1386,13 @@ export const RestApiAnyapiKnexPlugin = {
1319
1386
  context
1320
1387
  })
1321
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
1322
1396
  const rows = await query
1323
1397
 
1324
1398
  const newResources = []
@@ -1388,6 +1462,13 @@ export const RestApiAnyapiKnexPlugin = {
1388
1462
  context
1389
1463
  })
1390
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
1391
1472
  const rows = await query
1392
1473
 
1393
1474
  for (const row of rows) {
@@ -1587,6 +1668,13 @@ export const RestApiAnyapiKnexPlugin = {
1587
1668
  context
1588
1669
  })
1589
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
1590
1678
  const rows = await query
1591
1679
 
1592
1680
  const newResources = []
@@ -1676,7 +1764,8 @@ export const RestApiAnyapiKnexPlugin = {
1676
1764
  query,
1677
1765
  resourceName: targetDescriptor.resource,
1678
1766
  tableName: targetDescriptor.canonical.tableName,
1679
- context
1767
+ context,
1768
+ queryPurpose: 'relationship-identifiers'
1680
1769
  })
1681
1770
  query = scopedQueryState.query
1682
1771
  const rows = await query
@@ -1722,7 +1811,8 @@ export const RestApiAnyapiKnexPlugin = {
1722
1811
  query,
1723
1812
  resourceName: targetDescriptor.resource,
1724
1813
  tableName: targetDescriptor.canonical.tableName,
1725
- context
1814
+ context,
1815
+ queryPurpose: 'relationship-identifiers'
1726
1816
  })
1727
1817
  query = scopedQueryState.query
1728
1818
  const rows = await query
@@ -1756,13 +1846,22 @@ export const RestApiAnyapiKnexPlugin = {
1756
1846
  for (const [relName] of manyEntries) {
1757
1847
  const info = await getManyToManyInfo(descriptor.resource, relName)
1758
1848
  if (!info) continue
1759
- const rows = await fetchLinksForParents({
1849
+ let rows = await fetchLinksForParents({
1760
1850
  descriptor: info.descriptor,
1761
1851
  relationshipKey: info.relationshipKey,
1762
1852
  parentIds,
1763
1853
  db,
1764
1854
  })
1765
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
+
1766
1865
  const grouped = rows.reduce((acc, row) => {
1767
1866
  const key = String(row.parentId)
1768
1867
  acc[key] = acc[key] || []
@@ -1943,6 +2042,7 @@ export const RestApiAnyapiKnexPlugin = {
1943
2042
  scope.vars?.idProperty ||
1944
2043
  vars.idProperty ||
1945
2044
  'id'
2045
+ descriptor.idProperty = idProperty
1946
2046
 
1947
2047
  scope.vars.schemaInfo = {
1948
2048
  ...existingInfo,
@@ -2050,6 +2150,7 @@ export const RestApiAnyapiKnexPlugin = {
2050
2150
  scopeName,
2051
2151
  tableName: tableNameForHooks,
2052
2152
  db,
2153
+ queryPurpose: 'collection',
2053
2154
  isAnyApi: true,
2054
2155
  adapter,
2055
2156
  storageAdapter,
@@ -2185,7 +2286,8 @@ export const RestApiAnyapiKnexPlugin = {
2185
2286
  }
2186
2287
 
2187
2288
  context.returnMeta = context.returnMeta || {}
2188
- context.returnMeta.queryString = buildQueryString(queryParams)
2289
+ const queryString = serializeJsonApiQuery(queryParams)
2290
+ context.returnMeta.queryString = queryString ? `?${queryString}` : ''
2189
2291
  delete context.returnMeta.paginationMeta
2190
2292
  delete context.returnMeta.paginationLinks
2191
2293
 
@@ -2215,7 +2317,10 @@ export const RestApiAnyapiKnexPlugin = {
2215
2317
  cursorRecords,
2216
2318
  paginationInfo.pageSize,
2217
2319
  hasMore,
2218
- cursorFields
2320
+ cursorFields,
2321
+ {
2322
+ schemaInfo
2323
+ }
2219
2324
  )
2220
2325
  context.returnMeta.paginationMeta = paginationMeta
2221
2326
  const urlPrefix = getUrlPrefix(context, scope)
@@ -2226,7 +2331,10 @@ export const RestApiAnyapiKnexPlugin = {
2226
2331
  cursorRecords,
2227
2332
  paginationInfo.pageSize,
2228
2333
  hasMore,
2229
- cursorFields
2334
+ cursorFields,
2335
+ {
2336
+ schemaInfo
2337
+ }
2230
2338
  )
2231
2339
  }
2232
2340
 
@@ -2272,27 +2380,30 @@ export const RestApiAnyapiKnexPlugin = {
2272
2380
  const schemaInfo = scope?.vars?.schemaInfo
2273
2381
  const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias
2274
2382
 
2275
- const hookParams = {
2276
- context: {
2277
- knexQuery: {
2278
- query: adapter.query,
2279
- filters: context.queryParams?.filters,
2280
- schemaInfo,
2281
- scopeName,
2282
- tableName: tableNameForHooks,
2283
- db,
2284
- isAnyApi: true,
2285
- adapter,
2286
- storageAdapter,
2287
- },
2288
- },
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
+ }
2289
2397
  }
2290
2398
 
2399
+ const hookParams = { context: countContext }
2291
2400
  await polymorphicFiltersHook(hookParams, queryHookDependencies)
2292
2401
  await crossTableFiltersHook(hookParams, queryHookDependencies)
2293
2402
  await basicFiltersHook(hookParams, queryHookDependencies)
2403
+ await api.runHooks('knexQueryFiltering', countContext)
2294
2404
 
2295
- const [{ count }] = await adapter.query.count({ count: '*' })
2405
+ const countQuery = countContext.knexQuery?.query || adapter.query
2406
+ const [{ count }] = await countQuery.count({ count: '*' })
2296
2407
  return Number(count)
2297
2408
  }
2298
2409
 
@@ -2328,6 +2439,7 @@ export const RestApiAnyapiKnexPlugin = {
2328
2439
  schema,
2329
2440
  relationships,
2330
2441
  canonicalFieldMap: canonicalFieldsMap,
2442
+ idProperty,
2331
2443
  })
2332
2444
 
2333
2445
  const updatedStored = scopeOptionsRegistry.get(scopeName) || {}
@@ -2398,6 +2510,7 @@ export const RestApiAnyapiKnexPlugin = {
2398
2510
  canonicalFieldMap: scopeOptions?.canonicalFieldsMap ||
2399
2511
  scope?.scopeOptions?.canonicalFieldsMap ||
2400
2512
  null,
2513
+ idProperty,
2401
2514
  })
2402
2515
  if (scope) {
2403
2516
  scope.scopeOptions = {