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
@@ -1,4 +1,4 @@
1
- import { RestApiValidationError, RestApiResourceError } from '../../lib/rest-api-errors.js'
1
+ import { RestApiValidationError } from '../../lib/rest-api-errors.js'
2
2
 
3
3
  export const BulkOperationsPlugin = {
4
4
  name: 'bulk-operations',
@@ -353,41 +353,33 @@ export const BulkOperationsPlugin = {
353
353
  // Create route handlers
354
354
  const createBulkRouteHandler = (method) => {
355
355
  return async ({ context, body, query }) => {
356
- try {
357
- // Parse query params for atomic mode override
358
- const atomic = query?.atomic !== undefined
359
- ? query.atomic === 'true'
360
- : defaultAtomic
361
-
362
- let params
363
- if (method === 'bulkPost') {
364
- // For bulk create, expect array in data field (JSON:API style)
365
- params = {
366
- inputRecords: body?.data || body,
367
- atomic
368
- }
369
- } else if (method === 'bulkPatch') {
370
- // For bulk update, expect operations array
371
- params = {
372
- operations: body?.operations || body,
373
- atomic
374
- }
375
- } else if (method === 'bulkDelete') {
376
- // For bulk delete, expect IDs array
377
- params = {
378
- ids: body?.data || body?.ids || body,
379
- atomic
380
- }
356
+ // Parse query params for atomic mode override
357
+ const atomic = query?.atomic !== undefined
358
+ ? query.atomic === 'true'
359
+ : defaultAtomic
360
+
361
+ let params
362
+ if (method === 'bulkPost') {
363
+ // For bulk create, expect array in data field (JSON:API style)
364
+ params = {
365
+ inputRecords: body?.data || body,
366
+ atomic
367
+ }
368
+ } else if (method === 'bulkPatch') {
369
+ // For bulk update, expect operations array
370
+ params = {
371
+ operations: body?.operations || body,
372
+ atomic
373
+ }
374
+ } else if (method === 'bulkDelete') {
375
+ // For bulk delete, expect IDs array
376
+ params = {
377
+ ids: body?.data || body?.ids || body,
378
+ atomic
381
379
  }
382
-
383
- // Call the scope method
384
- const result = await api.scopes[scopeName][method](params)
385
-
386
- return result
387
- } catch (error) {
388
- // Let transport plugin handle error mapping
389
- throw error
390
380
  }
381
+
382
+ return await api.scopes[scopeName][method](params)
391
383
  }
392
384
  }
393
385
 
@@ -15,7 +15,6 @@
15
15
  */
16
16
 
17
17
  import { requirePackage } from 'hooked-api'
18
- import { parseJsonApiQuery } from '../lib/querying-writing/connectors-query-parser.js'
19
18
  import { createContext } from './lib/request-helpers.js'
20
19
  import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
21
20
  import {
@@ -337,11 +336,6 @@ export const ExpressPlugin = {
337
336
  router[method.toLowerCase()](path, ...beforeMiddleware, expressHandler)
338
337
 
339
338
  // Debug logging for route registration
340
-
341
- // Check if the route was actually added
342
- if (router.stack) {
343
- const lastRoute = router.stack[router.stack.length - 1]
344
- }
345
339
  }
346
340
  } catch (routeError) {
347
341
  log.error('[EXPRESS DEBUG] Error creating route:', {
@@ -101,14 +101,14 @@ export function createBusboyDetector (options = {}) {
101
101
  const chunks = []
102
102
  let size = 0
103
103
 
104
- const filePromise = new Promise((fileResolve, fileReject) => {
104
+ const filePromise = new Promise((resolve, reject) => {
105
105
  file.on('data', (chunk) => {
106
106
  chunks.push(chunk)
107
107
  size += chunk.length
108
108
  })
109
109
 
110
110
  file.on('limit', () => {
111
- fileReject(new Error(`File size limit exceeded for field '${fieldname}'`))
111
+ reject(new Error(`File size limit exceeded for field '${fieldname}'`))
112
112
  })
113
113
 
114
114
  file.on('end', () => {
@@ -119,10 +119,10 @@ export function createBusboyDetector (options = {}) {
119
119
  size,
120
120
  data: Buffer.concat(chunks)
121
121
  }
122
- fileResolve()
122
+ resolve()
123
123
  })
124
124
 
125
- file.on('error', fileReject)
125
+ file.on('error', reject)
126
126
  })
127
127
 
128
128
  filePromises.push(filePromise)
@@ -18,6 +18,7 @@ export const DEFAULT_CANONICAL_CONFIG = {
18
18
  scopeName: 'anyapi_records',
19
19
  tableName: 'any_records',
20
20
  type: 'anyapi_records',
21
+ logicalIdColumn: 'logical_id',
21
22
  tenantColumn: 'tenant_id',
22
23
  resourceColumn: 'resource',
23
24
  relationshipColumn: 'relationship',
@@ -34,20 +35,37 @@ const ensureTable = async (knex, tableName, tableBuilder) => {
34
35
  }
35
36
  }
36
37
 
37
- const ensureIndex = async (knex, tableName, columns, indexName) => {
38
+ const ensureColumn = async (knex, tableName, columnName, columnBuilder) => {
39
+ const exists = await knex.schema.hasTable(tableName)
40
+ if (!exists) return
41
+
42
+ const hasColumn = await knex.schema.hasColumn(tableName, columnName)
43
+ if (!hasColumn) {
44
+ await knex.schema.table(tableName, columnBuilder)
45
+ }
46
+ }
47
+
48
+ const ensureUniqueIndex = async (knex, tableName, columns, indexName) => {
38
49
  const exists = await knex.schema.hasTable(tableName)
39
50
  if (!exists) return
40
51
  const hasIndex = await knex.schema.withSchema('main').hasIndex?.(tableName, indexName)
41
52
  if (!hasIndex) {
42
- await knex.schema.table(tableName, (table) => {
43
- table.index(columns, indexName)
44
- })
53
+ try {
54
+ await knex.schema.table(tableName, (table) => {
55
+ table.unique(columns, indexName)
56
+ })
57
+ } catch (error) {
58
+ if (!String(error?.message || '').includes('already exists')) {
59
+ throw error
60
+ }
61
+ }
45
62
  }
46
63
  }
47
64
 
48
65
  export const ensureAnyApiSchema = async (knex) => {
49
66
  await ensureTable(knex, 'any_records', (table) => {
50
67
  table.increments('id').primary()
68
+ table.string('logical_id')
51
69
  table.string('tenant_id').notNullable()
52
70
  table.string('resource').notNullable()
53
71
 
@@ -77,9 +95,30 @@ export const ensureAnyApiSchema = async (knex) => {
77
95
  table.dateTime('deleted_at')
78
96
 
79
97
  table.index(['tenant_id', 'resource'])
98
+ table.unique(['tenant_id', 'resource', 'logical_id'], 'any_records_tenant_resource_logical_id_unique')
80
99
  table.index(['resource'])
81
100
  })
82
101
 
102
+ await ensureColumn(knex, 'any_records', 'logical_id', (table) => {
103
+ table.string('logical_id')
104
+ })
105
+ await ensureUniqueIndex(
106
+ knex,
107
+ 'any_records',
108
+ ['tenant_id', 'resource', 'logical_id'],
109
+ 'any_records_tenant_resource_logical_id_unique'
110
+ )
111
+
112
+ const rowsMissingLogicalId = await knex('any_records')
113
+ .select('id')
114
+ .whereNull('logical_id')
115
+
116
+ for (const row of rowsMissingLogicalId) {
117
+ await knex('any_records')
118
+ .where({ id: row.id })
119
+ .update({ logical_id: String(row.id) })
120
+ }
121
+
83
122
  await ensureTable(knex, 'any_links', (table) => {
84
123
  table.increments('id').primary()
85
124
  table.string('tenant_id').notNullable()
@@ -22,7 +22,10 @@ export const findSchemaFieldByAlias = (descriptor, alias) => {
22
22
  export const resolveFieldInfo = (descriptor, field) => {
23
23
  if (!descriptor) return null
24
24
  if (field === 'id') {
25
- return { column: 'id', definition: { type: 'id' } }
25
+ return {
26
+ column: descriptor?.canonical?.logicalIdColumn || 'logical_id',
27
+ definition: { type: 'id' }
28
+ }
26
29
  }
27
30
 
28
31
  const directField = descriptor.fields?.[field]
@@ -763,7 +763,7 @@ function deriveSqliteColumnNumbers (dataType, args = '') {
763
763
  async function introspectSqliteTableSnapshot (knex, { tableName, idColumn }) {
764
764
  const quotedTableName = quoteSqliteIdentifier(tableName)
765
765
  const tableRows = normalizeRows(await knex.raw(
766
- `SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
766
+ 'SELECT name, sql FROM sqlite_master WHERE type = \'table\' AND name = ? LIMIT 1',
767
767
  [tableName]
768
768
  ))
769
769
  const firstTableRow = Array.isArray(tableRows) ? tableRows[0] : null
@@ -103,13 +103,14 @@ function buildSetColumnType (values = []) {
103
103
  return `set(${normalized.map((value) => quoteJsString(value)).join(', ')})`
104
104
  }
105
105
 
106
- function createTableBuilderContext (schemaStructure = {}, idColumn = 'id') {
106
+ function createTableBuilderContext (schemaStructure = {}, idColumn = 'id', storage = undefined) {
107
107
  return {
108
108
  schemaStructure,
109
109
  idProperty: idColumn,
110
110
  storageInfo: buildStorageInfo({
111
111
  schemaStructure,
112
- idProperty: idColumn
112
+ idProperty: idColumn,
113
+ storage
113
114
  })
114
115
  }
115
116
  }
@@ -400,7 +401,8 @@ function buildImplicitIdDefinition (idColumn) {
400
401
  function resolveTableSchemaContext (schemaLike, options = {}) {
401
402
  const schemaStructure = schemaLike?.structure || schemaLike || {}
402
403
  const configuredIdColumn = options.idProperty || schemaStructure.id?.storage?.column || 'id'
403
- const tableContext = createTableBuilderContext(schemaStructure, configuredIdColumn)
404
+ const storage = options.storage ?? schemaLike?.storage
405
+ const tableContext = createTableBuilderContext(schemaStructure, configuredIdColumn, storage)
404
406
 
405
407
  let resourceIdField = null
406
408
  for (const [fieldName, definition] of Object.entries(schemaStructure)) {
@@ -888,6 +890,7 @@ export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, op
888
890
  const dialect = detectKnexDialect(knex)
889
891
  const tableSchemaContext = resolveTableSchemaContext(tableSchemaInstance, {
890
892
  idProperty: schemaInfo.idProperty,
893
+ storage: schemaInfo.storage,
891
894
  tableName
892
895
  })
893
896
 
@@ -918,6 +921,7 @@ export async function addKnexFields (knex, tableName, schema, options = {}) {
918
921
  assertNoTopLevelTableMetadata(schema, 'addKnexFields')
919
922
  const dialect = detectKnexDialect(knex)
920
923
  const tableSchemaContext = resolveTableSchemaContext(schema, {
924
+ storage: options.storage,
921
925
  ...options,
922
926
  tableName
923
927
  })
@@ -936,6 +940,7 @@ export async function alterKnexFields (knex, tableName, fields, options = {}) {
936
940
  assertNoTopLevelTableMetadata(fields, 'alterKnexFields')
937
941
  const dialect = detectKnexDialect(knex)
938
942
  const tableSchemaContext = resolveTableSchemaContext(fields, {
943
+ storage: options.storage,
939
944
  ...options,
940
945
  tableName
941
946
  })
@@ -961,6 +966,7 @@ export function generateKnexMigration (tableName, schema, options = {}) {
961
966
  const { autoIncrement = true, timestamps = false, dialect = '' } = options
962
967
  const tableSchemaContext = resolveTableSchemaContext(schema, {
963
968
  ...options,
969
+ storage: options.storage,
964
970
  tableName
965
971
  })
966
972
 
@@ -1028,6 +1034,7 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
1028
1034
  const resolvedDialect = normalizeText(dialect || currentSnapshot?.dialect).toLowerCase()
1029
1035
  const tableSchemaContext = resolveTableSchemaContext(schema, {
1030
1036
  ...options,
1037
+ storage: options.storage,
1031
1038
  tableName
1032
1039
  })
1033
1040
  const desiredColumns = buildDesiredColumnsMap(tableSchemaContext, { autoIncrement })
@@ -208,7 +208,6 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
208
208
  let foundRelationship = null
209
209
  let relationshipType = null
210
210
  let relationshipField = null
211
- let relationshipDef = null
212
211
 
213
212
  if (currentRelationships) {
214
213
  for (const [relName, relDef] of Object.entries(currentRelationships)) {
@@ -222,7 +221,6 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
222
221
  foundRelationship = targetScope
223
222
  relationshipType = 'hasManyPolymorphic'
224
223
  relationshipField = { typeField, idField, via: relDef.via }
225
- relationshipDef = relDef
226
224
  log.trace('[BUILD-JOIN] Found polymorphic hasMany relationship:', {
227
225
  relName, targetScope, via: relDef.via, typeField, idField
228
226
  })
@@ -238,7 +236,6 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
238
236
  foreignKey: relDef.foreignKey,
239
237
  otherKey: relDef.otherKey
240
238
  }
241
- relationshipDef = relDef
242
239
  log.trace('[BUILD-JOIN] Found many-to-many relationship:', {
243
240
  relName,
244
241
  targetScope,
@@ -256,7 +253,6 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
256
253
  log.error('[BUILD-JOIN] Missing foreignKey in hasMany relationship:', { relName, currentScope })
257
254
  throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${currentScope}'`)
258
255
  }
259
- relationshipDef = relDef
260
256
  log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField })
261
257
  break
262
258
  }
@@ -269,7 +265,6 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
269
265
  foundRelationship = targetScope
270
266
  relationshipType = 'belongsTo'
271
267
  relationshipField = fieldName
272
- relationshipDef = fieldDef
273
268
  log.trace('[BUILD-JOIN] Found belongsTo relationship:', { fieldName, targetScope })
274
269
  break
275
270
  }
@@ -282,9 +277,7 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
282
277
  )
283
278
  }
284
279
 
285
- const sourceSchema = scopes[currentScope].vars.schemaInfo.schemaInstance
286
280
  const sourceTableName = scopes[currentScope].vars.schemaInfo.tableName
287
- const targetSchemaInstance = scopes[targetScope].vars.schemaInfo.schemaInstance
288
281
  const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
289
282
 
290
283
  if (relationshipType === 'manyToMany') {
@@ -439,7 +439,6 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
439
439
  return jsonApiRecord
440
440
  })
441
441
 
442
- const urlPrefix = context.urlPrefix || scope.vars.transport?.mountPath || ''
443
442
  const normalizedData = isSingle ? processedRecords[0] : processedRecords
444
443
 
445
444
  if (normalizedData) {
@@ -191,7 +191,7 @@ export const polymorphicFiltersHook = async (hookParams, dependencies) => {
191
191
  // Step 2: Build polymorphic JOINs
192
192
  log.trace('[POLYMORPHIC-SEARCH] Building JOINs for polymorphic searches')
193
193
 
194
- for (const [filterKey, searchInfo] of polymorphicSearches) {
194
+ for (const [, searchInfo] of polymorphicSearches) {
195
195
  const { fieldDef, polymorphicField } = searchInfo
196
196
 
197
197
  // Get the relationship definition
@@ -485,7 +485,6 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
485
485
  }
486
486
 
487
487
  const schemaInfo = scopes[scopeName].vars.schemaInfo
488
- const tableName = schemaInfo.tableName
489
488
  const tableAlias = adapterUtils.defaultAliasForScope(scopeName)
490
489
  const aliasScopeMap = new Map()
491
490
  aliasScopeMap.set(tableAlias, scopeName)
@@ -730,7 +729,7 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
730
729
  fieldDef.applyFilter.call(this, this, filterValue)
731
730
  break
732
731
 
733
- default:
732
+ default: {
734
733
  const targetField = fieldDef.actualField || filterKey
735
734
  const columnRef = resolveFieldColumn(targetField)
736
735
  const operator = resolveSearchOperator(fieldDef)
@@ -738,6 +737,7 @@ export const crossTableFiltersHook = async (hookParams, dependencies) => {
738
737
  applyWhereForOperator({ builder: this, columnRef, operator, value: normalized, knex })
739
738
  break
740
739
  }
740
+ }
741
741
  }
742
742
  })
743
743
  }
@@ -891,8 +891,6 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
891
891
  const scopeName = hookParams.context?.knexQuery?.scopeName
892
892
  const filters = hookParams.context?.knexQuery?.filters
893
893
  const query = hookParams.context?.knexQuery?.query
894
- const db = hookParams.context?.knexQuery?.db || knex
895
-
896
894
  if (!scopeName || !filters || !scopes[scopeName]) {
897
895
  return
898
896
  }
@@ -993,7 +991,7 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
993
991
  fieldDef.applyFilter.call(this, this, filterValue)
994
992
  break
995
993
 
996
- default:
994
+ default: {
997
995
  // Standard filtering
998
996
  const actualField = fieldDef.actualField || filterKey
999
997
  // Always qualify field names
@@ -1005,6 +1003,7 @@ export const basicFiltersHook = async (hookParams, dependencies) => {
1005
1003
  applyWhereForOperator({ builder: this, columnRef: dbField, operator, value: normalized, knex })
1006
1004
  break
1007
1005
  }
1006
+ }
1008
1007
  }
1009
1008
  })
1010
1009
  }
@@ -75,7 +75,6 @@
75
75
  */
76
76
 
77
77
  import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
78
- import { getForeignKeyFields } from '../querying-writing/field-utils.js'
79
78
  import { toJsonApiRecord } from './knex-json-api-transformers-querying.js'
80
79
  import { buildWindowedIncludeQuery, applyStandardIncludeConfig, buildOrderByClause } from './knex-window-queries.js'
81
80
  import { unwrapQueryBuilderState } from './query-builder-utils.js'
@@ -491,8 +490,6 @@ export const loadBelongsTo = async (scope, deps) => {
491
490
  // Get target table info
492
491
  const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
493
492
  const targetIdColumn = getIdColumnForScope(scopes, targetScope)
494
- const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
495
-
496
493
  // Build field selection for sparse fieldsets
497
494
  const targetScopeObject = scopes[targetScope]
498
495
  const storageAdapter = targetScopeObject.vars.storageAdapter
@@ -548,9 +545,9 @@ export const loadBelongsTo = async (scope, deps) => {
548
545
 
549
546
  // Create lookup map
550
547
  const targetById = {}
551
- targetRecords.forEach(record => {
552
- targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
553
- })
548
+ targetRecords.forEach(record => {
549
+ targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
550
+ })
554
551
 
555
552
  // Set relationships on original records
556
553
  const targetRecordsToProcess = []
@@ -695,7 +692,7 @@ export const loadHasMany = async (scope, deps) => {
695
692
  const otherKey = relDef.otherKey
696
693
 
697
694
  if (!foreignKey || !otherKey) {
698
- throw new Error(`Missing foreignKey or otherKey in many-to-many relationship '${relName}' for scope '${scopeName}'`)
695
+ throw new Error(`Missing foreignKey or otherKey in many-to-many relationship '${includeName}' for scope '${scopeName}'`)
699
696
  }
700
697
 
701
698
  // For manyToMany, the target scope is the relationship name itself
@@ -737,12 +734,11 @@ export const loadHasMany = async (scope, deps) => {
737
734
  })
738
735
 
739
736
  // Step 3: Build field selection for sparse fieldsets
740
- const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
741
737
  const targetScopeObject = scopes[targetScope]
742
738
  const storageAdapter = targetScopeObject.vars.storageAdapter
743
739
  const targetIdColumn = getIdColumnForScope(scopes, targetScope)
744
- const fieldSelectionInfo = fields?.[targetScope]
745
- ? await buildFieldSelection(targetScopeObject, {
740
+ const fieldSelectionInfo = fields?.[targetScope]
741
+ ? await buildFieldSelection(targetScopeObject, {
746
742
  context: {
747
743
  ...(requestContext || {}),
748
744
  scopeName: targetScope,
@@ -958,7 +954,7 @@ export const loadHasMany = async (scope, deps) => {
958
954
  const foreignKey = relDef.foreignKey
959
955
 
960
956
  if (!foreignKey) {
961
- throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${scopeName}'`)
957
+ throw new Error(`Missing foreignKey in hasMany relationship '${includeName}' for scope '${scopeName}'`)
962
958
  }
963
959
 
964
960
  log.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
@@ -967,7 +963,6 @@ export const loadHasMany = async (scope, deps) => {
967
963
  })
968
964
 
969
965
  // Build field selection for sparse fieldsets
970
- const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
971
966
  const targetScopeObject = scopes[targetScope]
972
967
  const storageAdapter = targetScopeObject.vars.storageAdapter
973
968
  const fieldSelectionInfo = fields?.[targetScope]
@@ -1199,10 +1194,10 @@ export const loadHasOne = async (scope, deps) => {
1199
1194
  const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
1200
1195
  const { context: { scopes, log, knex, capabilities, requestContext } } = deps
1201
1196
 
1202
- log = log?.child ? log.child({ method: 'loadHasOne' }) : console
1197
+ const childLog = log?.child ? log.child({ method: 'loadHasOne' }) : console
1203
1198
 
1204
1199
  if (!records || records.length === 0) {
1205
- log.trace('[INCLUDE] No records to load hasOne for')
1200
+ childLog.trace('[INCLUDE] No records to load hasOne for')
1206
1201
  return
1207
1202
  }
1208
1203
 
@@ -1210,7 +1205,7 @@ export const loadHasOne = async (scope, deps) => {
1210
1205
  const mainIds = [...new Set(records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(Boolean))]
1211
1206
 
1212
1207
  if (mainIds.length === 0) {
1213
- log.trace('[INCLUDE] No parent IDs found, skipping hasOne load')
1208
+ childLog.trace('[INCLUDE] No parent IDs found, skipping hasOne load')
1214
1209
  return
1215
1210
  }
1216
1211
 
@@ -1222,12 +1217,11 @@ export const loadHasOne = async (scope, deps) => {
1222
1217
  throw new Error(`Missing foreignKey in hasOne relationship '${includeName}' for scope '${scopeName}'`)
1223
1218
  }
1224
1219
 
1225
- log.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
1220
+ childLog.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
1226
1221
  whereIn: mainIds
1227
1222
  })
1228
1223
 
1229
1224
  // Build query for hasOne - expects single result per parent
1230
- const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
1231
1225
  const targetScopeObject = scopes[targetScope]
1232
1226
  const storageAdapter = targetScopeObject.vars.storageAdapter
1233
1227
 
@@ -1629,7 +1623,6 @@ export const loadReversePolymorphic = async (scope, deps) => {
1629
1623
  })
1630
1624
 
1631
1625
  // Build field selection for sparse fieldsets
1632
- const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance
1633
1626
  const targetScopeObject = scopes[targetScope]
1634
1627
  const storageAdapter = targetScopeObject.vars.storageAdapter
1635
1628
  const targetIdColumn = getIdColumnForScope(scopes, targetScope)
@@ -1,6 +1,6 @@
1
1
  import { createSchema } from 'json-rest-schema'
2
2
  import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortFieldsByDependencies } from './schema-helpers.js'
3
- import { buildStorageInfo } from '../storage/storage-mapping.js'
3
+ import { buildStorageInfo, normalizeStorageConfig } from '../storage/storage-mapping.js'
4
4
 
5
5
  /**
6
6
  * Compiles and enriches schemas for a resource scope
@@ -155,7 +155,7 @@ export async function compileSchemas (scope, deps) {
155
155
  }
156
156
 
157
157
  // Default enrichment: Add type to belongsTo fields
158
- for (const [fieldName, fieldDef] of Object.entries(enrichedFields)) {
158
+ for (const [, fieldDef] of Object.entries(enrichedFields)) {
159
159
  if (fieldDef.belongsTo && !fieldDef.type) {
160
160
  fieldDef.type = 'id'
161
161
  }
@@ -174,7 +174,7 @@ export async function compileSchemas (scope, deps) {
174
174
 
175
175
  if (isProbablyPivot) {
176
176
  // Make all belongsTo fields searchable for pivot operations
177
- for (const [fieldName, fieldDef] of belongsToFields) {
177
+ for (const [, fieldDef] of belongsToFields) {
178
178
  if (!fieldDef.search) {
179
179
  fieldDef.search = true
180
180
  }
@@ -206,6 +206,7 @@ export async function compileSchemas (scope, deps) {
206
206
  scope.scopeOptions.searchSchema
207
207
  )
208
208
 
209
+ let searchSchemaObject
209
210
  if (rawSearchFields) {
210
211
  // Mark all search fields as indexed for database optimization.
211
212
  // This ensures that any field used for filtering will have a database index created,
@@ -224,9 +225,9 @@ export async function compileSchemas (scope, deps) {
224
225
  await runHooks('searchSchema:enrich', searchSchemaContext)
225
226
 
226
227
  // Create searchSchema object
227
- var searchSchemaObject = createSchema(rawSearchFields)
228
+ searchSchemaObject = createSchema(rawSearchFields)
228
229
  } else {
229
- var searchSchemaObject = createSchema({})
230
+ searchSchemaObject = createSchema({})
230
231
  }
231
232
 
232
233
  // Build schemaRelationships including polymorphic fields from schema
@@ -317,9 +318,11 @@ export async function compileSchemas (scope, deps) {
317
318
  }
318
319
 
319
320
  const idProperty = scope.scopeOptions.idProperty || scope.vars.idProperty || 'id'
321
+ const storage = normalizeStorageConfig(scope.scopeOptions.storage)
320
322
  const storageInfo = buildStorageInfo({
321
323
  schemaStructure: schemaObject.structure,
322
- idProperty
324
+ idProperty,
325
+ storage
323
326
  })
324
327
 
325
328
  // Cache everything
@@ -335,6 +338,7 @@ export async function compileSchemas (scope, deps) {
335
338
  schemaRelationships,
336
339
  tableName: scope.scopeOptions.tableName || scopeName,
337
340
  idProperty,
341
+ storage,
338
342
  storageInfo,
339
343
  indexes: Array.isArray(scope.scopeOptions.indexes) ? [...scope.scopeOptions.indexes] : [],
340
344
  foreignKeys: Array.isArray(scope.scopeOptions.foreignKeys) ? [...scope.scopeOptions.foreignKeys] : [],
@@ -257,7 +257,6 @@ export const buildFieldSelection = async (scope, deps) => {
257
257
  }
258
258
  })
259
259
  }
260
-
261
260
  } else {
262
261
  // No sparse fieldsets - return all visible fields
263
262
  // This is the default behavior when no ?fields parameter is provided
@@ -283,7 +282,7 @@ export const buildFieldSelection = async (scope, deps) => {
283
282
  // So we need to include their dependencies even if normallyHidden
284
283
  // Example: profit_margin depends on 'cost' which is normallyHidden
285
284
  // We fetch 'cost' for calculation but don't return it in response
286
- for (const [fieldName, fieldDef] of Object.entries(computedFields)) {
285
+ for (const [, fieldDef] of Object.entries(computedFields)) {
287
286
  if (fieldDef.dependencies) {
288
287
  for (const dep of fieldDef.dependencies) {
289
288
  const depFieldDef = schemaStructure[dep]