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.
- package/README.md +22 -1
- package/docs/API.md +145 -24
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +83 -4
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +26 -34
- package/plugins/core/connectors/express-plugin.js +0 -6
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/lib/anyapi/schema-utils.js +43 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
- package/plugins/core/lib/dbIntrospection.js +1 -1
- package/plugins/core/lib/dbTablesOperations.js +10 -3
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +0 -1
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +11 -18
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +1 -2
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +22 -2
- package/plugins/core/lib/storage/storage-adapter.js +3 -2
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
- package/plugins/core/rest-api-anyapi-knex-plugin.js +194 -102
- package/plugins/core/rest-api-knex-plugin.js +23 -16
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
- package/plugins/core/rest-api-plugin-methods/common.js +27 -9
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
- package/plugins/core/rest-api-plugin-methods/patch.js +31 -3
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/post.js +8 -0
- package/plugins/core/rest-api-plugin-methods/put.js +31 -3
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +4 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +1 -1
- package/plugins/storage/s3-storage.js +5 -2
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +7 -0
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/db-introspection.test.js +4 -1
- package/tests/db-table-operations.test.js +25 -20
- package/tests/fastify-plugin.test.js +21 -6
- package/tests/fixtures/api-configs.js +133 -0
- package/tests/id-normalization.test.js +444 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +11 -11
- package/tests/storage-mapping.test.js +55 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RestApiValidationError
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
resolve()
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
file.on('error',
|
|
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
|
|
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
|
-
|
|
43
|
-
table
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 [
|
|
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
|
-
|
|
552
|
-
|
|
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 '${
|
|
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
|
-
|
|
745
|
-
|
|
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 '${
|
|
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
|
-
|
|
1197
|
+
const childLog = log?.child ? log.child({ method: 'loadHasOne' }) : console
|
|
1203
1198
|
|
|
1204
1199
|
if (!records || records.length === 0) {
|
|
1205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
228
|
+
searchSchemaObject = createSchema(rawSearchFields)
|
|
228
229
|
} else {
|
|
229
|
-
|
|
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 [
|
|
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]
|