json-rest-api 1.0.23 → 1.0.24

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 (60) hide show
  1. package/README.md +22 -1
  2. package/docs/API.md +145 -24
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +83 -4
  4. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
  5. package/lib/enhanced-logger.js +1 -1
  6. package/package.json +6 -2
  7. package/plugins/core/bulk-operations-plugin.js +26 -34
  8. package/plugins/core/connectors/express-plugin.js +0 -6
  9. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  10. package/plugins/core/lib/anyapi/schema-utils.js +43 -4
  11. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
  12. package/plugins/core/lib/dbIntrospection.js +1 -1
  13. package/plugins/core/lib/dbTablesOperations.js +10 -3
  14. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  15. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +0 -1
  16. package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
  17. package/plugins/core/lib/querying/knex-relationship-includes.js +11 -18
  18. package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
  19. package/plugins/core/lib/querying-writing/knex-field-helpers.js +1 -2
  20. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  21. package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
  22. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  23. package/plugins/core/lib/storage/canonical-storage-mapping.js +22 -2
  24. package/plugins/core/lib/storage/storage-adapter.js +3 -2
  25. package/plugins/core/lib/storage/storage-mapping.js +86 -16
  26. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
  27. package/plugins/core/rest-api-anyapi-knex-plugin.js +194 -102
  28. package/plugins/core/rest-api-knex-plugin.js +23 -16
  29. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
  30. package/plugins/core/rest-api-plugin-methods/common.js +27 -9
  31. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
  32. package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
  33. package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
  34. package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
  35. package/plugins/core/rest-api-plugin-methods/get.js +7 -1
  36. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
  37. package/plugins/core/rest-api-plugin-methods/patch.js +31 -3
  38. package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
  39. package/plugins/core/rest-api-plugin-methods/post.js +8 -0
  40. package/plugins/core/rest-api-plugin-methods/put.js +31 -3
  41. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  42. package/plugins/core/rest-api-plugin.js +4 -0
  43. package/plugins/core/rest-api-positioning-plugin.js +1 -4
  44. package/plugins/core/socketio-plugin.js +0 -1
  45. package/plugins/storage/local-storage.js +1 -1
  46. package/plugins/storage/s3-storage.js +5 -2
  47. package/quickTest.js +1 -0
  48. package/scripts/run-docs.js +80 -84
  49. package/test-debug.js +1 -1
  50. package/tests/README.md +7 -0
  51. package/tests/anyapi-basic.test.js +3 -0
  52. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  53. package/tests/db-introspection.test.js +4 -1
  54. package/tests/db-table-operations.test.js +25 -20
  55. package/tests/fastify-plugin.test.js +21 -6
  56. package/tests/fixtures/api-configs.js +133 -0
  57. package/tests/id-normalization.test.js +444 -0
  58. package/tests/logical-id-write-validation.test.js +151 -0
  59. package/tests/pagination-cursor-multifield.test.js +11 -11
  60. package/tests/storage-mapping.test.js +55 -6
@@ -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 {
@@ -121,46 +125,104 @@ export const RestApiAnyapiKnexPlugin = {
121
125
  return descriptor
122
126
  }
123
127
 
128
+ const decorateResourceLinks = ({ resource, scope, scopeName, context }) => {
129
+ if (!resource || !scope || !scopeName || !context) return
130
+
131
+ resource.links = resource.links || {}
132
+ resource.links.self = buildResourceUrl(context, scope, scopeName, resource.id)
133
+
134
+ if (!resource.relationships || typeof resource.relationships !== 'object') {
135
+ return
136
+ }
137
+
138
+ for (const [relationshipName, relationshipObject] of Object.entries(resource.relationships)) {
139
+ if (!relationshipObject || typeof relationshipObject !== 'object') {
140
+ continue
141
+ }
142
+
143
+ relationshipObject.links = {
144
+ self: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, true),
145
+ related: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, false),
146
+ }
147
+ }
148
+ }
149
+
124
150
  const getAllowedQueryFieldNames = (scopeName) => (
125
151
  Object.keys(api.resources?.[scopeName]?.vars?.queryFields || {})
126
152
  )
127
153
 
154
+ const getLogicalResourceIdColumn = (descriptor) => getCanonicalResourceIdColumn(descriptor)
155
+
156
+ const getLogicalResourceId = (row, descriptor) => getCanonicalResourceId(row, descriptor)
157
+
128
158
  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
159
+ if (!filters || Object.keys(filters).length === 0) return
160
+
161
+ const normalizeValue = (field, value) => {
162
+ if (!adapter?.translateFilterValue) {
163
+ return value
164
+ }
165
+ if (Array.isArray(value)) {
166
+ return value.map((entry) => adapter.translateFilterValue(field, entry))
167
+ }
168
+ return adapter.translateFilterValue(field, value)
134
169
  }
135
170
 
136
- // TODO: Probably delete the rest of the function. searchSchema is always there...!!!
137
- if (!filters || Object.keys(filters).length === 0) return
171
+ for (const [filterKey, rawValue] of Object.entries(filters)) {
172
+ const fieldDef = searchSchema?.[filterKey]
173
+
174
+ if (fieldDef?.polymorphicField) {
175
+ continue
176
+ }
138
177
 
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)
178
+ if (fieldDef?.actualField?.includes('.')) {
179
+ continue
180
+ }
181
+
182
+ if (fieldDef?.oneOf?.some((field) => field.includes('.'))) {
183
+ continue
184
+ }
185
+
186
+ if (fieldDef?.oneOf && Array.isArray(fieldDef.oneOf)) {
187
+ continue
188
+ }
189
+
190
+ if (fieldDef?.applyFilter && typeof fieldDef.applyFilter === 'function') {
191
+ continue
192
+ }
193
+
194
+ const actualField = fieldDef?.actualField || filterKey
195
+ const fieldInfo = ensureFilterableField(descriptor, filterKey)
196
+ const columnRef = adapter ? adapter.translateColumn(actualField) : fieldInfo.column
197
+ const operator = resolveSearchOperator(fieldDef || fieldInfo.definition)
142
198
  const values = normalizeFilterValues(rawValue, fieldInfo.definition, {
143
199
  isRelationship: fieldInfo.isRelationship,
144
200
  })
145
-
146
- const nonNullValues = values.filter((value) => value !== null)
147
- const hasNull = values.length !== nonNullValues.length
201
+ const normalizedValues = values.map((value) => normalizeValue(actualField, value))
202
+ const nonNullValues = normalizedValues.filter((value) => value !== null)
203
+ const hasNull = normalizedValues.length !== nonNullValues.length
148
204
 
149
205
  if (nonNullValues.length === 0 && hasNull) {
150
- query.whereNull(fieldInfo.column)
206
+ query.whereNull(columnRef)
151
207
  continue
152
208
  }
153
209
  if (nonNullValues.length === 1 && !hasNull) {
154
- query.where(fieldInfo.column, nonNullValues[0])
210
+ applyWhereForOperator({
211
+ builder: query,
212
+ columnRef,
213
+ operator,
214
+ value: nonNullValues[0],
215
+ knex,
216
+ })
155
217
  continue
156
218
  }
157
219
  if (nonNullValues.length > 1 && !hasNull) {
158
- query.whereIn(fieldInfo.column, nonNullValues)
220
+ query.whereIn(columnRef, nonNullValues)
159
221
  continue
160
222
  }
161
223
  if (nonNullValues.length > 0 && hasNull) {
162
- query.where(function filterGroup () {
163
- this.whereIn(fieldInfo.column, nonNullValues).orWhereNull(fieldInfo.column)
224
+ query.where(function anyApiNullFilterGroup () {
225
+ this.whereIn(columnRef, nonNullValues).orWhereNull(columnRef)
164
226
  })
165
227
  }
166
228
  }
@@ -891,12 +953,13 @@ export const RestApiAnyapiKnexPlugin = {
891
953
  const descriptor = await getDescriptor(scopeName)
892
954
  const { canonical } = descriptor
893
955
  const id = context.id
956
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
894
957
 
895
958
  const row = await context.db(canonical.tableName)
896
959
  .select('id')
897
960
  .where(canonical.tenantColumn, descriptor.tenant)
898
961
  .where(canonical.resourceColumn, descriptor.resource)
899
- .where('id', id)
962
+ .where(logicalIdColumn, id)
900
963
  .first()
901
964
 
902
965
  return !!row
@@ -909,10 +972,17 @@ export const RestApiAnyapiKnexPlugin = {
909
972
  }
910
973
  const descriptor = await getDescriptor(scopeName)
911
974
  const { canonical } = descriptor
975
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
976
+ const explicitId = context.inputRecord?.data?.id
912
977
  const attributes = context.inputRecord?.data?.attributes || {}
913
978
  const row = storageAdapter?.toStorageRow
914
979
  ? storageAdapter.toStorageRow(attributes)
915
980
  : translateCanonicalAttributesForStorage(attributes, descriptor)
981
+
982
+ if (explicitId !== undefined && explicitId !== null) {
983
+ row[logicalIdColumn] = explicitId
984
+ }
985
+
916
986
  row[canonical.tenantColumn] = descriptor.tenant
917
987
  row[canonical.resourceColumn] = descriptor.resource
918
988
 
@@ -921,10 +991,25 @@ export const RestApiAnyapiKnexPlugin = {
921
991
  .returning('id')
922
992
 
923
993
  const inserted = Array.isArray(result) ? result[0] : result
924
- if (inserted && typeof inserted === 'object' && 'id' in inserted) {
925
- return inserted.id
994
+ const insertedRowId = inserted && typeof inserted === 'object' && 'id' in inserted
995
+ ? inserted.id
996
+ : inserted
997
+
998
+ if (explicitId !== undefined && explicitId !== null) {
999
+ return explicitId
926
1000
  }
927
- return inserted
1001
+
1002
+ if (insertedRowId === undefined || insertedRowId === null) {
1003
+ return insertedRowId
1004
+ }
1005
+
1006
+ const generatedLogicalId = String(insertedRowId)
1007
+
1008
+ await context.db(canonical.tableName)
1009
+ .where({ id: insertedRowId })
1010
+ .update({ [logicalIdColumn]: generatedLogicalId })
1011
+
1012
+ return generatedLogicalId
928
1013
  }
929
1014
 
930
1015
  helpers.dataPut = async ({ scopeName, context }) => {
@@ -935,6 +1020,7 @@ export const RestApiAnyapiKnexPlugin = {
935
1020
  const descriptor = await getDescriptor(scopeName)
936
1021
  const { canonical } = descriptor
937
1022
  const id = context.id
1023
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
938
1024
  const attributes = context.inputRecord?.data?.attributes || {}
939
1025
  const row = storageAdapter?.toStorageRow
940
1026
  ? storageAdapter.toStorageRow(attributes)
@@ -952,7 +1038,7 @@ export const RestApiAnyapiKnexPlugin = {
952
1038
  }
953
1039
 
954
1040
  const result = await context.db(canonical.tableName)
955
- .where('id', id)
1041
+ .where(logicalIdColumn, id)
956
1042
  .where(canonical.resourceColumn, descriptor.resource)
957
1043
  .where(canonical.tenantColumn, descriptor.tenant)
958
1044
  .update(updateRow)
@@ -968,6 +1054,7 @@ export const RestApiAnyapiKnexPlugin = {
968
1054
  const descriptor = await getDescriptor(scopeName)
969
1055
  const { canonical } = descriptor
970
1056
  const id = context.id
1057
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
971
1058
  const attributes = context.inputRecord?.data?.attributes || {}
972
1059
  const row = storageAdapter?.toStorageRow
973
1060
  ? storageAdapter.toStorageRow(attributes)
@@ -982,7 +1069,7 @@ export const RestApiAnyapiKnexPlugin = {
982
1069
  }
983
1070
 
984
1071
  const result = await context.db(canonical.tableName)
985
- .where('id', id)
1072
+ .where(logicalIdColumn, id)
986
1073
  .where(canonical.resourceColumn, descriptor.resource)
987
1074
  .where(canonical.tenantColumn, descriptor.tenant)
988
1075
  .update(updateRow)
@@ -998,9 +1085,10 @@ export const RestApiAnyapiKnexPlugin = {
998
1085
  const descriptor = await getDescriptor(scopeName)
999
1086
  const { canonical } = descriptor
1000
1087
  const id = context.id
1088
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1001
1089
 
1002
1090
  const result = await context.db(canonical.tableName)
1003
- .where('id', id)
1091
+ .where(logicalIdColumn, id)
1004
1092
  .where(canonical.resourceColumn, descriptor.resource)
1005
1093
  .where(canonical.tenantColumn, descriptor.tenant)
1006
1094
  .delete()
@@ -1016,10 +1104,11 @@ export const RestApiAnyapiKnexPlugin = {
1016
1104
  const descriptor = await getDescriptor(scopeName)
1017
1105
  const { canonical } = descriptor
1018
1106
  const id = context.id
1107
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1019
1108
  const scope = api.resources?.[scopeName]
1020
1109
 
1021
1110
  let query = context.db(canonical.tableName)
1022
- .where('id', id)
1111
+ .where(logicalIdColumn, id)
1023
1112
  .where(canonical.resourceColumn, descriptor.resource)
1024
1113
  .where(canonical.tenantColumn, descriptor.tenant)
1025
1114
 
@@ -1047,6 +1136,8 @@ export const RestApiAnyapiKnexPlugin = {
1047
1136
  storageAdapter,
1048
1137
  }
1049
1138
 
1139
+ await applyBuiltInAnyApiQueryFilters(context)
1140
+
1050
1141
  await runHooks('knexQueryFiltering')
1051
1142
 
1052
1143
  if (context.knexQuery) {
@@ -1062,16 +1153,15 @@ export const RestApiAnyapiKnexPlugin = {
1062
1153
  descriptor,
1063
1154
  { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1064
1155
  )
1156
+ const resourceId = getLogicalResourceId(row, descriptor)
1065
1157
  const minimal = {
1066
1158
  type: descriptor.resource,
1067
- id: String(row.id),
1159
+ id: resourceId,
1068
1160
  attributes: translated.attributes,
1069
1161
  relationships: translated.relationships,
1070
1162
  }
1071
1163
  if (scope) {
1072
- minimal.links = {
1073
- self: buildResourceUrl(context, scope, scopeName, minimal.id),
1074
- }
1164
+ decorateResourceLinks({ resource: minimal, scope, scopeName, context })
1075
1165
  }
1076
1166
  return minimal
1077
1167
  }
@@ -1170,9 +1260,10 @@ export const RestApiAnyapiKnexPlugin = {
1170
1260
  descriptor,
1171
1261
  { allowedExtraFields: getAllowedQueryFieldNames(descriptor.resource) }
1172
1262
  )
1263
+ const resourceId = getLogicalResourceId(row, descriptor)
1173
1264
  const includeResource = {
1174
1265
  type: descriptor.resource,
1175
- id: String(row.id),
1266
+ id: resourceId,
1176
1267
  attributes: translated.attributes,
1177
1268
  }
1178
1269
 
@@ -1186,8 +1277,12 @@ export const RestApiAnyapiKnexPlugin = {
1186
1277
 
1187
1278
  const targetScope = api.resources?.[descriptor.resource]
1188
1279
  if (targetScope) {
1189
- includeResource.links = includeResource.links || {}
1190
- includeResource.links.self = buildResourceUrl(context, targetScope, descriptor.resource, includeResource.id)
1280
+ decorateResourceLinks({
1281
+ resource: includeResource,
1282
+ scope: targetScope,
1283
+ scopeName: descriptor.resource,
1284
+ context,
1285
+ })
1191
1286
  }
1192
1287
 
1193
1288
  return includeResource
@@ -1216,7 +1311,7 @@ export const RestApiAnyapiKnexPlugin = {
1216
1311
  let query = db(targetDescriptor.canonical.tableName)
1217
1312
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1218
1313
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1219
- .whereIn('id', ids)
1314
+ .whereIn(getLogicalResourceIdColumn(targetDescriptor), ids)
1220
1315
  const selectionState = await applyIncludeFieldSelection({
1221
1316
  query,
1222
1317
  resourceName: targetDescriptor.resource,
@@ -1228,7 +1323,8 @@ export const RestApiAnyapiKnexPlugin = {
1228
1323
 
1229
1324
  const newResources = []
1230
1325
  for (const row of rows) {
1231
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1326
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1327
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1232
1328
  if (seen.has(includeKey)) continue
1233
1329
  seen.add(includeKey)
1234
1330
  const includeResource = buildIncludedResource({
@@ -1284,7 +1380,7 @@ export const RestApiAnyapiKnexPlugin = {
1284
1380
  let query = db(targetDescriptor.canonical.tableName)
1285
1381
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1286
1382
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1287
- .whereIn('id', [...idSet])
1383
+ .whereIn(getLogicalResourceIdColumn(targetDescriptor), [...idSet])
1288
1384
  const selectionState = await applyIncludeFieldSelection({
1289
1385
  query,
1290
1386
  resourceName: targetDescriptor.resource,
@@ -1295,7 +1391,8 @@ export const RestApiAnyapiKnexPlugin = {
1295
1391
  const rows = await query
1296
1392
 
1297
1393
  for (const row of rows) {
1298
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1394
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1395
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1299
1396
  if (seen.has(includeKey)) continue
1300
1397
  seen.add(includeKey)
1301
1398
  const includeResource = buildIncludedResource({
@@ -1432,7 +1529,8 @@ export const RestApiAnyapiKnexPlugin = {
1432
1529
 
1433
1530
  const newResources = []
1434
1531
  for (const row of rows) {
1435
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1532
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1533
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1436
1534
  if (seen.has(includeKey)) continue
1437
1535
  seen.add(includeKey)
1438
1536
  const includeResource = buildIncludedResource({
@@ -1481,7 +1579,7 @@ export const RestApiAnyapiKnexPlugin = {
1481
1579
  let query = db(targetDescriptor.canonical.tableName)
1482
1580
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1483
1581
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1484
- .whereIn('id', childIds)
1582
+ .whereIn(getLogicalResourceIdColumn(targetDescriptor), childIds)
1485
1583
  const selectionState = await applyIncludeFieldSelection({
1486
1584
  query,
1487
1585
  resourceName: targetDescriptor.resource,
@@ -1493,7 +1591,8 @@ export const RestApiAnyapiKnexPlugin = {
1493
1591
 
1494
1592
  const newResources = []
1495
1593
  for (const row of rows) {
1496
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1594
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1595
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1497
1596
  if (seen.has(includeKey)) continue
1498
1597
  seen.add(includeKey)
1499
1598
  const includeResource = buildIncludedResource({
@@ -1587,7 +1686,7 @@ export const RestApiAnyapiKnexPlugin = {
1587
1686
  if (parentId == null) return acc
1588
1687
  const key = String(parentId)
1589
1688
  acc[key] = acc[key] || []
1590
- acc[key].push({ type: targetDescriptor.resource, id: String(row.id) })
1689
+ acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
1591
1690
  return acc
1592
1691
  }, {})
1593
1692
 
@@ -1633,7 +1732,7 @@ export const RestApiAnyapiKnexPlugin = {
1633
1732
  if (parentId == null) return acc
1634
1733
  const key = String(parentId)
1635
1734
  acc[key] = acc[key] || []
1636
- acc[key].push({ type: targetDescriptor.resource, id: String(row.id) })
1735
+ acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
1637
1736
  return acc
1638
1737
  }, {})
1639
1738
 
@@ -1654,7 +1753,7 @@ export const RestApiAnyapiKnexPlugin = {
1654
1753
  const db = context.db || context.transaction || api.knex.instance
1655
1754
  const parentIds = resources.map((resource) => resource.id)
1656
1755
 
1657
- for (const [relName, relInfo] of manyEntries) {
1756
+ for (const [relName] of manyEntries) {
1658
1757
  const info = await getManyToManyInfo(descriptor.resource, relName)
1659
1758
  if (!info) continue
1660
1759
  const rows = await fetchLinksForParents({
@@ -1687,6 +1786,7 @@ export const RestApiAnyapiKnexPlugin = {
1687
1786
  const descriptor = await getDescriptor(scopeName)
1688
1787
  const { canonical } = descriptor
1689
1788
  const id = context.id
1789
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1690
1790
  const scope = api.resources?.[scopeName]
1691
1791
 
1692
1792
  let fieldSelectionInfo = null
@@ -1698,7 +1798,7 @@ export const RestApiAnyapiKnexPlugin = {
1698
1798
  }
1699
1799
 
1700
1800
  let query = context.db(canonical.tableName)
1701
- .where('id', id)
1801
+ .where(logicalIdColumn, id)
1702
1802
  .where(canonical.resourceColumn, descriptor.resource)
1703
1803
  .where(canonical.tenantColumn, descriptor.tenant)
1704
1804
 
@@ -1726,9 +1826,10 @@ export const RestApiAnyapiKnexPlugin = {
1726
1826
  descriptor,
1727
1827
  { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1728
1828
  )
1829
+ const resourceId = getLogicalResourceId(row, descriptor)
1729
1830
  const data = {
1730
1831
  type: descriptor.resource,
1731
- id: String(row.id),
1832
+ id: resourceId,
1732
1833
  attributes: record.attributes,
1733
1834
  }
1734
1835
 
@@ -1760,24 +1861,20 @@ export const RestApiAnyapiKnexPlugin = {
1760
1861
  }
1761
1862
 
1762
1863
  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
- }
1864
+ decorateResourceLinks({ resource: data, scope, scopeName, context })
1865
+ response.links = { self: data.links.self }
1768
1866
  }
1769
1867
 
1770
1868
  if (included.length > 0) {
1771
1869
  for (const includeResource of included) {
1772
1870
  const targetScope = api.resources?.[includeResource.type]
1773
1871
  if (!targetScope) continue
1774
- includeResource.links = includeResource.links || {}
1775
- includeResource.links.self = buildResourceUrl(
1872
+ decorateResourceLinks({
1873
+ resource: includeResource,
1874
+ scope: targetScope,
1875
+ scopeName: includeResource.type,
1776
1876
  context,
1777
- targetScope,
1778
- includeResource.type,
1779
- includeResource.id
1780
- )
1877
+ })
1781
1878
  }
1782
1879
  }
1783
1880
 
@@ -1894,32 +1991,14 @@ export const RestApiAnyapiKnexPlugin = {
1894
1991
  return { resourceToTableName, tableNameToResource }
1895
1992
  }
1896
1993
 
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 }
1994
+ const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter }
1911
1995
 
1912
- addHook('knexQueryFiltering', 'polymorphicFiltersHook', {}, async (hookParams) =>
1913
- polymorphicFiltersHook(hookParams, queryHookDependencies)
1914
- )
1915
-
1916
- addHook('knexQueryFiltering', 'crossTableFiltersHook', {}, async (hookParams) =>
1917
- crossTableFiltersHook(hookParams, queryHookDependencies)
1918
- )
1919
-
1920
- addHook('knexQueryFiltering', 'basicFiltersHook', {}, async (hookParams) =>
1921
- basicFiltersHook(hookParams, queryHookDependencies)
1922
- )
1996
+ const applyBuiltInAnyApiQueryFilters = async (context) => {
1997
+ const hookParams = { context }
1998
+ await polymorphicFiltersHook(hookParams, queryHookDependencies)
1999
+ await crossTableFiltersHook(hookParams, queryHookDependencies)
2000
+ await basicFiltersHook(hookParams, queryHookDependencies)
2001
+ }
1923
2002
 
1924
2003
  helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
1925
2004
  const storageAdapter = getScopeStorageAdapter(scopeName)
@@ -1976,6 +2055,8 @@ export const RestApiAnyapiKnexPlugin = {
1976
2055
  storageAdapter,
1977
2056
  }
1978
2057
 
2058
+ await applyBuiltInAnyApiQueryFilters(context)
2059
+
1979
2060
  if (runHooks) {
1980
2061
  await runHooks('knexQueryFiltering')
1981
2062
  }
@@ -1984,19 +2065,20 @@ export const RestApiAnyapiKnexPlugin = {
1984
2065
 
1985
2066
  const countQuery = queryBuilder.clone()
1986
2067
 
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() }
2068
+ let selectionState = { query: queryBuilder, queryFieldRuntimeByField: new Map() }
2069
+ if (scope && fieldSelectionInfo) {
2070
+ selectionState = await applyFieldSelectionToQuery({
2071
+ query: queryBuilder,
2072
+ scope,
2073
+ fieldSelectionInfo,
2074
+ tableName: adapter.tableAlias,
2075
+ useTablePrefix: false,
2076
+ storageAdapter,
2077
+ db,
2078
+ context,
2079
+ scopeName
2080
+ })
2081
+ }
2000
2082
 
2001
2083
  const sortDescriptors = applySortingToQuery({
2002
2084
  query: selectionState.query,
@@ -2006,9 +2088,6 @@ export const RestApiAnyapiKnexPlugin = {
2006
2088
  queryFieldRuntimeByField: selectionState.queryFieldRuntimeByField
2007
2089
  })
2008
2090
 
2009
- if (queryParams?.page?.after) {
2010
- }
2011
-
2012
2091
  const paginationInfo = applyPaginationToQuery({
2013
2092
  query: queryBuilder,
2014
2093
  scope,
@@ -2027,7 +2106,7 @@ export const RestApiAnyapiKnexPlugin = {
2027
2106
  const record = {}
2028
2107
  for (const descriptorEntry of cursorDescriptors) {
2029
2108
  if (descriptorEntry.field === 'id') {
2030
- record.id = row.id
2109
+ record.id = getLogicalResourceId(row, descriptor)
2031
2110
  } else if (descriptorEntry.queryFieldRuntime) {
2032
2111
  record[descriptorEntry.field] = row[descriptorEntry.field]
2033
2112
  } else if (descriptorEntry.column) {
@@ -2050,9 +2129,10 @@ export const RestApiAnyapiKnexPlugin = {
2050
2129
  descriptor,
2051
2130
  { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
2052
2131
  )
2132
+ const resourceId = getLogicalResourceId(row, descriptor)
2053
2133
  const resource = {
2054
2134
  type: descriptor.resource,
2055
- id: String(row.id),
2135
+ id: resourceId,
2056
2136
  attributes: translated.attributes,
2057
2137
  }
2058
2138
  if (translated.relationships && Object.keys(translated.relationships).length > 0) {
@@ -2060,8 +2140,7 @@ export const RestApiAnyapiKnexPlugin = {
2060
2140
  }
2061
2141
  const resourceScope = scope
2062
2142
  if (resourceScope) {
2063
- resource.links = resource.links || {}
2064
- resource.links.self = buildResourceUrl(context, resourceScope, scopeName, resource.id)
2143
+ decorateResourceLinks({ resource, scope: resourceScope, scopeName, context })
2065
2144
  }
2066
2145
  if (fieldSelectionInfo?.computedDependencies?.length) {
2067
2146
  resource.__$jsonrestapi_computed_deps$__ = fieldSelectionInfo.computedDependencies
@@ -2092,6 +2171,19 @@ export const RestApiAnyapiKnexPlugin = {
2092
2171
  response.included = included
2093
2172
  }
2094
2173
 
2174
+ if (included.length > 0) {
2175
+ for (const includeResource of included) {
2176
+ const targetScope = api.resources?.[includeResource.type]
2177
+ if (!targetScope) continue
2178
+ decorateResourceLinks({
2179
+ resource: includeResource,
2180
+ scope: targetScope,
2181
+ scopeName: includeResource.type,
2182
+ context,
2183
+ })
2184
+ }
2185
+ }
2186
+
2095
2187
  context.returnMeta = context.returnMeta || {}
2096
2188
  context.returnMeta.queryString = buildQueryString(queryParams)
2097
2189
  delete context.returnMeta.paginationMeta