json-rest-api 1.0.10 → 1.0.11

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 (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +5 -9
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -1,6 +1,6 @@
1
1
  import { requirePackage } from 'hooked-api';
2
- import { createSchema, createKnexTable } from 'json-rest-schema';
3
- import { createCrossTableSearchHelpers } from './lib/querying/knex-cross-table-search.js';
2
+ import { createSchema } from 'json-rest-schema';
3
+ import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js';
4
4
  import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js';
5
5
  import { getForeignKeyFields } from './lib/querying-writing/field-utils.js';
6
6
  import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js';
@@ -24,6 +24,8 @@ import {
24
24
  buildCursorMeta,
25
25
  parseCursor
26
26
  } from './lib/querying/knex-pagination-helpers.js';
27
+ import { getUrlPrefix } from './lib/querying/url-helpers.js';
28
+ import { createStorageAdapter } from './lib/storage/storage-adapter.js';
27
29
 
28
30
  /**
29
31
  * Strips non-database fields (computed and virtual) from attributes before database operations
@@ -55,7 +57,6 @@ export const RestApiKnexPlugin = {
55
57
  dependencies: ['rest-api'],
56
58
 
57
59
  async install({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
58
-
59
60
  // Try to import knex dynamically
60
61
  let knexLib;
61
62
  try {
@@ -75,6 +76,48 @@ export const RestApiKnexPlugin = {
75
76
  helpers: {}
76
77
  };
77
78
 
79
+ const storageAdapters = new Map();
80
+
81
+ const getScopeStorageAdapter = (scopeName) => {
82
+ if (!scopeName) return null;
83
+ const resource = api.resources?.[scopeName] || scopes?.[scopeName];
84
+ const schemaInfo = resource?.vars?.schemaInfo;
85
+ if (!schemaInfo) return null;
86
+
87
+ const cached = storageAdapters.get(scopeName);
88
+ if (cached && cached.schemaInfo === schemaInfo) {
89
+ return cached.adapter;
90
+ }
91
+
92
+ const adapter = createStorageAdapter({ knex, schemaInfo });
93
+ storageAdapters.set(scopeName, { adapter, schemaInfo });
94
+ if (resource?.vars) {
95
+ resource.vars.storageAdapter = adapter;
96
+ }
97
+ return adapter;
98
+ };
99
+
100
+ api.knex.helpers.getStorageAdapter = getScopeStorageAdapter;
101
+ helpers.getStorageAdapter = getScopeStorageAdapter;
102
+
103
+ const createSelectTranslator = (adapter) => {
104
+ if (!adapter) return null;
105
+ return (field, alias) => {
106
+ if (field === '*') {
107
+ return alias ? `${alias}.*` : '*';
108
+ }
109
+ const translated = adapter.translateColumn(field);
110
+ const result = translated || field;
111
+ if (!alias) {
112
+ return result;
113
+ }
114
+ if (result.includes('.')) {
115
+ return result;
116
+ }
117
+ return `${alias}.${result}`;
118
+ };
119
+ };
120
+
78
121
  // Check database capabilities
79
122
  const hasWindowFunctions = await supportsWindowFunctions(knex);
80
123
  const dbInfo = await getDatabaseInfo(knex);
@@ -91,8 +134,7 @@ export const RestApiKnexPlugin = {
91
134
  windowFunctions: hasWindowFunctions
92
135
  });
93
136
 
94
- // Initialize cross-table search helpers
95
- const crossTableSearchHelpers = createCrossTableSearchHelpers(scopes, log);
137
+ // Cross-table search functions are now imported directly and used with full signatures
96
138
 
97
139
  /* ╔═════════════════════════════════════════════════════════════════════╗
98
140
  * ║ MAIN QUERY FILTERING HOOK ║
@@ -102,7 +144,7 @@ export const RestApiKnexPlugin = {
102
144
 
103
145
  // Register the three separate filter hooks
104
146
  // Dependencies object for the hooks
105
- const polymorphicFiltersHookParams = { log, scopes, knex, crossTableSearchHelpers };
147
+ const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter };
106
148
 
107
149
  // Register in specific order: polymorphic → cross-table → basic
108
150
  // This ensures proper field qualification when JOINs are present
@@ -121,32 +163,61 @@ export const RestApiKnexPlugin = {
121
163
  addHook('knexQueryFiltering', 'basicFiltersHook', {},
122
164
  async (hookParams) => basicFiltersHook(hookParams, polymorphicFiltersHookParams)
123
165
  );
124
-
125
- // Expose helpers under api.knex.helpers
126
- api.knex.helpers.crossTableSearch = crossTableSearchHelpers;
166
+
167
+ // 3. Basic filters (processes simple main table filters)
168
+ addHook('release', 'releaseHook', {},
169
+ async ({ api }) => api.knex.instance.destroy()
170
+ );
171
+
127
172
 
128
173
  // Helper scope method to get all schema-related information
129
- addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
130
- // Create a filtered schema that excludes virtual fields
131
- const schemaStructure = vars.schemaInfo.schemaStructure || {};
132
- const filteredSchema = {};
133
-
134
- // Copy only non-virtual fields to the filtered schema
135
- Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
136
- if (!fieldDef.virtual) {
137
- filteredSchema[fieldName] = fieldDef;
138
- }
139
- });
140
-
141
- // Create schema object from filtered fields
142
- const tableSchema = createSchema(filteredSchema);
143
-
144
- await createKnexTable(api.knex.instance, vars.schemaInfo.tableName, tableSchema, vars.schemaInfo.idProperty)
145
- })
174
+ addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
175
+ // Create a filtered schema that excludes virtual fields
176
+ const schemaStructure = vars.schemaInfo.schemaStructure || {};
177
+ const filteredSchema = {};
178
+
179
+ // Copy only non-virtual fields to the filtered schema
180
+ Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
181
+ if (!fieldDef.virtual) {
182
+ filteredSchema[fieldName] = fieldDef;
183
+ }
184
+ });
185
+
186
+ // Create schema object from filtered fields
187
+ const tableSchemaInstance = createSchema(filteredSchema);
188
+
189
+ await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
190
+ })
191
+
192
+
193
+ // Helper scope method to alter existing fields in a table
194
+ addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
195
+ // Validate required parameters
196
+ if (!params.fields || typeof params.fields !== 'object') {
197
+ throw new Error('fields parameter is required for alterKnexFields');
198
+ }
199
+
200
+ await alterKnexFields(
201
+ api.knex.instance,
202
+ vars.schemaInfo.tableName,
203
+ params.fields,
204
+ params.options // Pass through any additional options
205
+ );
206
+ });
207
+
146
208
 
147
- helpers.newTransaction = async () => {
148
- return knex.transaction()
149
- }
209
+ // Helper scope method to add a field to an existing table
210
+ addScopeMethod('addKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
211
+
212
+ // Create schema object from filtered fields
213
+ const partialTableSchema = createSchema(params.fields);
214
+
215
+ await addKnexFields(api.knex.instance, vars.schemaInfo.tableName, partialTableSchema)
216
+ })
217
+
218
+ helpers.newTransaction = async () => {
219
+ return knex.transaction()
220
+ }
150
221
 
151
222
  /* ╔═════════════════════════════════════════════════════════════════════╗
152
223
  * ║ DATA OPERATION METHODS ║
@@ -167,16 +238,19 @@ export const RestApiKnexPlugin = {
167
238
  * @returns {Promise<boolean>} True if the resource exists, false otherwise
168
239
  */
169
240
  helpers.dataExists = async ({ scopeName, context }) => {
241
+ const storageAdapter = getScopeStorageAdapter(scopeName);
242
+ if (storageAdapter) {
243
+ context.storageAdapter = storageAdapter;
244
+ }
245
+
170
246
  const id = context.id;
171
- const scope = api.resources[scopeName];
172
247
 
173
- const tableName = context.schemaInfo.tableName
174
- const idProperty = context.schemaInfo.idProperty
175
- const db = context.db;
248
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
249
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
250
+ const db = context.db || api.knex.instance;
176
251
 
177
252
  log.debug(`[Knex] EXISTS ${tableName}/${id}`);
178
253
 
179
- // Select with alias if idProperty is not 'id'
180
254
  const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id';
181
255
 
182
256
  const record = await db(tableName)
@@ -197,7 +271,7 @@ export const RestApiKnexPlugin = {
197
271
  * @param {Object} params.context.schemaInfo - Schema information for the resource
198
272
  * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
199
273
  * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
200
- * @param {Object} params.context.schemaInfo.schema - The full schema definition for the resource
274
+ * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
201
275
  * @param {Object} params.context.db - Database connection (knex instance or transaction)
202
276
  * @param {Object} [params.context.queryParams] - Query parameters for sparse fieldsets and includes
203
277
  * @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
@@ -207,6 +281,10 @@ export const RestApiKnexPlugin = {
207
281
  * @throws {RestApiResourceError} When the resource is not found
208
282
  */
209
283
  helpers.dataGet = async ({ scopeName, context, runHooks }) => {
284
+ const storageAdapter = getScopeStorageAdapter(scopeName);
285
+ if (storageAdapter) {
286
+ context.storageAdapter = storageAdapter;
287
+ }
210
288
  const scope = api.resources[scopeName];
211
289
  if (!scope) {
212
290
  log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) });
@@ -220,11 +298,10 @@ export const RestApiKnexPlugin = {
220
298
  varKeys: scope.vars ? Object.keys(scope.vars) : []
221
299
  });
222
300
  }
223
- const id = context.id;
224
- const tableName = context.schemaInfo.tableName;
225
- const idProperty = context.schemaInfo.idProperty
226
- const schema = context.schemaInfo.schema;
227
- const db = context.db;
301
+ const id = context.id;
302
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
303
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
304
+ const db = context.db || api.knex.instance;
228
305
 
229
306
  log.debug(`[Knex] GET ${tableName}/${id}`);
230
307
 
@@ -246,7 +323,14 @@ export const RestApiKnexPlugin = {
246
323
  let query = db(tableName).where(idProperty, id);
247
324
 
248
325
  // Apply field selection
249
- query = buildQuerySelection(query, tableName, fieldSelectionInfo.fieldsToSelect, false);
326
+ const selectTranslator = createSelectTranslator(storageAdapter);
327
+ query = buildQuerySelection(
328
+ query,
329
+ tableName,
330
+ fieldSelectionInfo.fieldsToSelect,
331
+ false,
332
+ selectTranslator ? { translateColumn: selectTranslator } : undefined,
333
+ );
250
334
 
251
335
  const record = await query.first();
252
336
 
@@ -291,11 +375,15 @@ export const RestApiKnexPlugin = {
291
375
  * @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
292
376
  */
293
377
  helpers.dataGetMinimal = async ({ scopeName, context }) => {
378
+ const storageAdapter = getScopeStorageAdapter(scopeName);
379
+ if (storageAdapter) {
380
+ context.storageAdapter = storageAdapter;
381
+ }
294
382
  const scope = api.resources[scopeName];
295
383
  const id = context.id;
296
- const tableName = context.schemaInfo.tableName;
297
- const idProperty = context.schemaInfo.idProperty;
298
- const db = context.db;
384
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
385
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
386
+ const db = context.db || api.knex.instance;
299
387
 
300
388
  log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`);
301
389
 
@@ -327,8 +415,8 @@ export const RestApiKnexPlugin = {
327
415
  * @param {Object} params.context - The context object containing request-specific data
328
416
  * @param {Object} params.context.schemaInfo - Schema information for the resource
329
417
  * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
330
- * @param {Object} params.context.schemaInfo.schema - The full schema definition for the resource
331
- * @param {Object} params.context.schemaInfo.searchSchema - Search schema for filtering capabilities
418
+ * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
419
+ * @param {Object} params.context.schemaInfo.searchSchemaInstance - Search schema for filtering capabilities
332
420
  * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
333
421
  * @param {Object} params.context.queryParams - Query parameters object
334
422
  * @param {Object} [params.context.queryParams.filters] - Filter conditions
@@ -347,17 +435,19 @@ export const RestApiKnexPlugin = {
347
435
  * @param {Function} params.runHooks - Function to run hooks (e.g., 'knexQueryFiltering')
348
436
  * @returns {Promise<Object>} JSON:API formatted response with data array, optional included resources, and pagination meta/links
349
437
  */
350
- helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
438
+ helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
439
+ const storageAdapter = getScopeStorageAdapter(scopeName);
440
+ if (storageAdapter) {
441
+ context.storageAdapter = storageAdapter;
442
+ }
351
443
  const scope = api.resources[scopeName];
352
- const tableName = context.schemaInfo.tableName;
353
- const schema = context.schemaInfo.schema;
354
- const searchSchema = context.schemaInfo.searchSchema;
355
- const queryParams = context.queryParams
356
- const db = context.db;
357
- const sortableFields = context.sortableFields
358
- const idProperty = context.schemaInfo.idProperty
359
-
360
- log.trace('[DATA-QUERY] Starting dataQuery', { scopeName, hasSearchSchema: !!searchSchema });
444
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
445
+ const schemaInfo = context.schemaInfo;
446
+ const queryParams = context.queryParams;
447
+ const db = context.db || api.knex.instance;
448
+ const sortableFields = context.sortableFields;
449
+
450
+ log.trace('[DATA-QUERY] Starting dataQuery', { scopeName });
361
451
  log.debug(`[Knex] QUERY ${tableName}`, queryParams);
362
452
 
363
453
  // Build field selection for sparse fieldsets
@@ -376,7 +466,14 @@ export const RestApiKnexPlugin = {
376
466
  // Start building query with table prefix (for JOIN support)
377
467
  let query = db(tableName);
378
468
 
379
- query = buildQuerySelection(query, tableName, fieldSelectionInfo.fieldsToSelect, true);
469
+ const selectTranslator = createSelectTranslator(storageAdapter);
470
+ query = buildQuerySelection(
471
+ query,
472
+ tableName,
473
+ fieldSelectionInfo.fieldsToSelect,
474
+ true,
475
+ selectTranslator ? { translateColumn: selectTranslator } : undefined,
476
+ );
380
477
 
381
478
  /* ═══════════════════════════════════════════════════════════════════
382
479
  * FILTERING HOOKS
@@ -394,10 +491,20 @@ export const RestApiKnexPlugin = {
394
491
 
395
492
  log.trace('[DATA-QUERY] Storing query data in context before calling runHooks');
396
493
 
494
+ // YOU ARE HERE: pass 'sesrchSchemaInstance' in the context below
495
+
397
496
  // Store the query data in context where hooks can access it
398
497
  // This is the proper way to share data between methods and hooks
399
498
  if (context) {
400
- context.knexQuery = { query, filters: queryParams.filters, searchSchema, scopeName, tableName, db };
499
+ context.knexQuery = {
500
+ query,
501
+ filters: queryParams.filters,
502
+ schemaInfo,
503
+ scopeName,
504
+ tableName,
505
+ db,
506
+ adapter: storageAdapter,
507
+ };
401
508
 
402
509
  log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters });
403
510
  }
@@ -423,7 +530,19 @@ export const RestApiKnexPlugin = {
423
530
  return; // Skip non-sortable fields
424
531
  }
425
532
 
426
- query.orderBy(field, desc ? 'desc' : 'asc');
533
+ // Map relationship names to actual database columns for sorting
534
+ // Check if this is a relationship field that needs to be mapped
535
+ let dbField = field;
536
+ const searchField = schemaInfo.searchSchemaStructure?.[field];
537
+ if (searchField?.actualField) {
538
+ // This is a relationship field, use the actual database column
539
+ dbField = searchField.actualField;
540
+ }
541
+ const translatedField = storageAdapter?.translateColumn
542
+ ? storageAdapter.translateColumn(dbField)
543
+ : dbField;
544
+
545
+ query.orderBy(translatedField, desc ? 'desc' : 'asc');
427
546
  });
428
547
  }
429
548
 
@@ -672,7 +791,14 @@ export const RestApiKnexPlugin = {
672
791
  if (queryParams.filters && Object.keys(queryParams.filters).length > 0) {
673
792
  // Store query data in context for hooks
674
793
  if (context) {
675
- context.knexQuery = { query: countQuery, filters: queryParams.filters, searchSchema, scopeName, tableName, db };
794
+ context.knexQuery = {
795
+ query: countQuery,
796
+ filters: queryParams.filters,
797
+ scopeName,
798
+ tableName,
799
+ schemaInfo,
800
+ db
801
+ };
676
802
  }
677
803
 
678
804
  // Run the same filtering hooks
@@ -700,7 +826,7 @@ export const RestApiKnexPlugin = {
700
826
  }
701
827
 
702
828
  // Generate links
703
- const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
829
+ const urlPrefix = getUrlPrefix(context, scope);
704
830
  context.returnMeta.paginationLinks = generatePaginationLinks(
705
831
  urlPrefix,
706
832
  scopeName,
@@ -732,7 +858,7 @@ export const RestApiKnexPlugin = {
732
858
  }
733
859
 
734
860
  context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields);
735
- const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
861
+ const urlPrefix = getUrlPrefix(context, scope);
736
862
  context.returnMeta.paginationLinks = generateCursorPaginationLinks(
737
863
  urlPrefix,
738
864
  scopeName,
@@ -770,7 +896,7 @@ export const RestApiKnexPlugin = {
770
896
  * @param {Object} params.context.schemaInfo - Schema information for the resource
771
897
  * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
772
898
  * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
773
- * @param {Object} params.context.schemaInfo.schema - The full schema definition for the resource
899
+ * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
774
900
  * @param {Object} params.context.db - Database connection (knex instance or transaction)
775
901
  * @param {Object} params.context.inputRecord - JSON:API formatted input record
776
902
  * @param {Object} params.context.inputRecord.data - The resource data
@@ -778,12 +904,15 @@ export const RestApiKnexPlugin = {
778
904
  * @returns {Promise<string|number>} The ID of the newly created resource
779
905
  */
780
906
  helpers.dataPost = async ({ scopeName, context }) => {
907
+ const storageAdapter = getScopeStorageAdapter(scopeName);
908
+ if (storageAdapter) {
909
+ context.storageAdapter = storageAdapter;
910
+ }
781
911
  const scope = api.resources[scopeName];
782
- const tableName = context.schemaInfo.tableName;
783
- const idProperty = context.schemaInfo.idProperty
784
- const schema = context.schemaInfo.schema;
785
- const db = context.db;
786
- const inputRecord = context.inputRecord
912
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
913
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
914
+ const db = context.db || api.knex.instance;
915
+ const inputRecord = context.inputRecord;
787
916
 
788
917
  log.debug(`[Knex] POST ${tableName}`, inputRecord);
789
918
 
@@ -811,7 +940,7 @@ export const RestApiKnexPlugin = {
811
940
  * @param {Object} params.context.schemaInfo - Schema information for the resource
812
941
  * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
813
942
  * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
814
- * @param {Object} params.context.schemaInfo.schema - The full schema definition for the resource
943
+ * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
815
944
  * @param {Object} params.context.db - Database connection (knex instance or transaction)
816
945
  * @param {Object} params.context.inputRecord - JSON:API formatted input record
817
946
  * @param {Object} params.context.inputRecord.data - The resource data
@@ -822,14 +951,17 @@ export const RestApiKnexPlugin = {
822
951
  * @throws {RestApiResourceError} When updating and the resource is not found
823
952
  */
824
953
  helpers.dataPut = async ({ scopeName, context }) => {
954
+ const storageAdapter = getScopeStorageAdapter(scopeName);
955
+ if (storageAdapter) {
956
+ context.storageAdapter = storageAdapter;
957
+ }
825
958
  const scope = api.resources[scopeName];
826
959
  const id = context.id;
827
- const tableName = context.schemaInfo.tableName;
828
- const idProperty = context.schemaInfo.idProperty
829
- const schema = context.schemaInfo.schema;
830
- const db = context.db;
831
- const inputRecord = context.inputRecord
832
- const isCreate = context.isCreate
960
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
961
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
962
+ const db = context.db || api.knex.instance;
963
+ const inputRecord = context.inputRecord;
964
+ const isCreate = context.isCreate;
833
965
 
834
966
  log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`);
835
967
 
@@ -895,7 +1027,7 @@ export const RestApiKnexPlugin = {
895
1027
  * @param {Object} params.context.schemaInfo - Schema information for the resource
896
1028
  * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
897
1029
  * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
898
- * @param {Object} params.context.schemaInfo.schema - The full schema definition for the resource
1030
+ * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
899
1031
  * @param {Object} params.context.db - Database connection (knex instance or transaction)
900
1032
  * @param {Object} params.context.inputRecord - JSON:API formatted input record with partial updates
901
1033
  * @param {Object} params.context.inputRecord.data - The resource data
@@ -904,14 +1036,17 @@ export const RestApiKnexPlugin = {
904
1036
  * @returns {Promise<void>} Resolves when the update is complete
905
1037
  * @throws {RestApiResourceError} When the resource is not found
906
1038
  */
907
- helpers.dataPatch = async ({ scopeName, context }) => {
1039
+ helpers.dataPatch = async ({ scopeName, context }) => {
1040
+ const storageAdapter = getScopeStorageAdapter(scopeName);
1041
+ if (storageAdapter) {
1042
+ context.storageAdapter = storageAdapter;
1043
+ }
908
1044
  const scope = api.resources[scopeName];
909
1045
  const id = context.id;
910
- const tableName = context.schemaInfo.tableName;
911
- const idProperty = context.schemaInfo.idProperty
912
- const schema = context.schemaInfo.schema;
913
- const db = context.db;
914
- const inputRecord = context.inputRecord
1046
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
1047
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
1048
+ const db = context.db || api.knex.instance;
1049
+ const inputRecord = context.inputRecord;
915
1050
 
916
1051
  log.debug(`[Knex] PATCH ${tableName}/${id}`);
917
1052
 
@@ -938,7 +1073,7 @@ export const RestApiKnexPlugin = {
938
1073
 
939
1074
  // Strip non-database fields (computed and virtual) before database operation
940
1075
  const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo);
941
-
1076
+
942
1077
  log.debug(`[Knex] PATCH finalAttributes:`, finalAttributes);
943
1078
 
944
1079
  // Remove the idProperty from attributes to prevent updating the primary key
@@ -969,12 +1104,16 @@ export const RestApiKnexPlugin = {
969
1104
  * @throws {RestApiResourceError} When the resource is not found
970
1105
  */
971
1106
  helpers.dataDelete = async ({ scopeName, context }) => {
1107
+ const storageAdapter = getScopeStorageAdapter(scopeName);
1108
+ if (storageAdapter) {
1109
+ context.storageAdapter = storageAdapter;
1110
+ }
972
1111
  const scope = api.resources[scopeName];
973
1112
  const id = context.id;
974
1113
 
975
- const tableName = context.schemaInfo.tableName;
976
- const idProperty = context.schemaInfo.idProperty
977
- const db = context.db;
1114
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
1115
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
1116
+ const db = context.db || api.knex.instance;
978
1117
 
979
1118
  log.debug(`[Knex] DELETE ${tableName}/${id}`);
980
1119
 
@@ -1004,4 +1143,4 @@ export const RestApiKnexPlugin = {
1004
1143
 
1005
1144
  log.info('RestApiKnexPlugin installed - basic CRUD operations ready');
1006
1145
  }
1007
- }
1146
+ }