json-rest-api 1.0.13 → 1.0.15

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 (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +13 -0
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +9 -5
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,31 +1,31 @@
1
- import { requirePackage } from 'hooked-api';
2
- import { createSchema } from 'json-rest-schema';
3
- import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js';
4
- import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js';
5
- import { getForeignKeyFields } from './lib/querying-writing/field-utils.js';
6
- import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js';
7
- import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js';
8
- import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js';
9
- import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js';
10
- import { processIncludes } from './lib/querying/knex-process-includes.js';
11
- import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js';
1
+ import { requirePackage } from 'hooked-api'
2
+ import { createSchema } from 'json-rest-schema'
3
+ import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js'
4
+ import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js'
5
+ import { getForeignKeyFields } from './lib/querying-writing/field-utils.js'
6
+ import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js'
7
+ import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
8
+ import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
9
+ import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
10
+ import { processIncludes } from './lib/querying/knex-process-includes.js'
11
+ import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js'
12
12
  import {
13
13
  polymorphicFiltersHook,
14
14
  crossTableFiltersHook,
15
15
  basicFiltersHook
16
- } from './lib/querying/knex-query-helpers.js';
17
- import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js';
18
- import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js';
19
- import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js';
20
- import {
21
- calculatePaginationMeta,
16
+ } from './lib/querying/knex-query-helpers.js'
17
+ import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js'
18
+ import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js'
19
+ import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js'
20
+ import {
21
+ calculatePaginationMeta,
22
22
  generatePaginationLinks,
23
23
  generateCursorPaginationLinks,
24
24
  buildCursorMeta,
25
25
  parseCursor
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';
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'
29
29
 
30
30
  /**
31
31
  * Strips non-database fields (computed and virtual) from attributes before database operations
@@ -34,184 +34,179 @@ import { createStorageAdapter } from './lib/storage/storage-adapter.js';
34
34
  * @returns {Object} Attributes with computed and virtual fields removed
35
35
  */
36
36
  const stripNonDatabaseFields = (attributes, schemaInfo) => {
37
- if (!attributes || !schemaInfo) return attributes || {};
38
-
39
- const { computed = {}, schemaStructure = {} } = schemaInfo;
37
+ if (!attributes || !schemaInfo) return attributes || {}
38
+
39
+ const { computed = {}, schemaStructure = {} } = schemaInfo
40
40
  return Object.entries(attributes)
41
41
  .filter(([key]) => {
42
42
  // Remove computed fields
43
- if (key in computed) return false;
43
+ if (key in computed) return false
44
44
  // Remove fields marked as virtual in the schema
45
- const fieldDef = schemaStructure[key];
45
+ const fieldDef = schemaStructure[key]
46
46
  if (fieldDef && fieldDef.virtual === true) {
47
- return false;
47
+ return false
48
48
  }
49
- return true;
49
+ return true
50
50
  })
51
- .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {});
52
- };
53
-
51
+ .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
52
+ }
54
53
 
55
54
  export const RestApiKnexPlugin = {
56
55
  name: 'rest-api-knex',
57
56
  dependencies: ['rest-api'],
58
57
 
59
- async install({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
58
+ async install ({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
60
59
  // Try to import knex dynamically
61
- let knexLib;
60
+ let knexLib
62
61
  try {
63
- knexLib = await import('knex');
62
+ knexLib = await import('knex')
64
63
  } catch (e) {
65
- requirePackage('knex', 'rest-api-knex',
66
- 'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.');
64
+ requirePackage('knex', 'rest-api-knex',
65
+ 'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
67
66
  }
68
-
67
+
69
68
  // Get Knex instance from plugin options
70
- const knexOptions = pluginOptions || {};
71
- const knex = knexOptions.knex;
72
-
69
+ const knexOptions = pluginOptions || {}
70
+ const knex = knexOptions.knex
71
+
73
72
  // Expose Knex instance and helpers in a structured way
74
73
  api.knex = {
75
74
  instance: knex,
76
75
  helpers: {}
77
- };
76
+ }
78
77
 
79
- const storageAdapters = new Map();
78
+ const storageAdapters = new Map()
80
79
 
81
80
  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;
81
+ if (!scopeName) return null
82
+ const resource = api.resources?.[scopeName] || scopes?.[scopeName]
83
+ const schemaInfo = resource?.vars?.schemaInfo
84
+ if (!schemaInfo) return null
86
85
 
87
- const cached = storageAdapters.get(scopeName);
86
+ const cached = storageAdapters.get(scopeName)
88
87
  if (cached && cached.schemaInfo === schemaInfo) {
89
- return cached.adapter;
88
+ return cached.adapter
90
89
  }
91
90
 
92
- const adapter = createStorageAdapter({ knex, schemaInfo });
93
- storageAdapters.set(scopeName, { adapter, schemaInfo });
91
+ const adapter = createStorageAdapter({ knex, schemaInfo })
92
+ storageAdapters.set(scopeName, { adapter, schemaInfo })
94
93
  if (resource?.vars) {
95
- resource.vars.storageAdapter = adapter;
94
+ resource.vars.storageAdapter = adapter
96
95
  }
97
- return adapter;
98
- };
96
+ return adapter
97
+ }
99
98
 
100
- api.knex.helpers.getStorageAdapter = getScopeStorageAdapter;
101
- helpers.getStorageAdapter = getScopeStorageAdapter;
99
+ api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
100
+ helpers.getStorageAdapter = getScopeStorageAdapter
102
101
 
103
102
  const createSelectTranslator = (adapter) => {
104
- if (!adapter) return null;
103
+ if (!adapter) return null
105
104
  return (field, alias) => {
106
105
  if (field === '*') {
107
- return alias ? `${alias}.*` : '*';
106
+ return alias ? `${alias}.*` : '*'
108
107
  }
109
- const translated = adapter.translateColumn(field);
110
- const result = translated || field;
108
+ const translated = adapter.translateColumn(field)
109
+ const result = translated || field
111
110
  if (!alias) {
112
- return result;
111
+ return result
113
112
  }
114
113
  if (result.includes('.')) {
115
- return result;
114
+ return result
116
115
  }
117
- return `${alias}.${result}`;
118
- };
119
- };
116
+ return `${alias}.${result}`
117
+ }
118
+ }
120
119
 
121
120
  // Check database capabilities
122
- const hasWindowFunctions = await supportsWindowFunctions(knex);
123
- const dbInfo = await getDatabaseInfo(knex);
124
-
121
+ const hasWindowFunctions = await supportsWindowFunctions(knex)
122
+ const dbInfo = await getDatabaseInfo(knex)
123
+
125
124
  // Store capabilities in API instance for access throughout
126
125
  api.knex.capabilities = {
127
126
  windowFunctions: hasWindowFunctions,
128
127
  dbInfo
129
- };
130
-
131
- log.info(`Database capabilities detected:`, {
128
+ }
129
+
130
+ log.info('Database capabilities detected:', {
132
131
  database: dbInfo.client,
133
132
  version: dbInfo.version,
134
133
  windowFunctions: hasWindowFunctions
135
- });
134
+ })
136
135
 
137
136
  // Cross-table search functions are now imported directly and used with full signatures
138
-
137
+
139
138
  /* ╔═════════════════════════════════════════════════════════════════════╗
140
139
  * ║ MAIN QUERY FILTERING HOOK ║
141
140
  * ║ This is the heart of the filtering system. It processes searchSchema ║
142
141
  * ║ filters and builds SQL WHERE conditions with proper JOINs ║
143
142
  * ╚═════════════════════════════════════════════════════════════════════╝ */
144
-
143
+
145
144
  // Register the three separate filter hooks
146
145
  // Dependencies object for the hooks
147
- const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter };
148
-
146
+ const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
147
+
149
148
  // Register in specific order: polymorphic → cross-table → basic
150
149
  // This ensures proper field qualification when JOINs are present
151
-
150
+
152
151
  // 1. Polymorphic filters (adds JOINs for polymorphic relationships)
153
- addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
152
+ addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
154
153
  async (hookParams) => polymorphicFiltersHook(hookParams, polymorphicFiltersHookParams)
155
- );
156
-
154
+ )
155
+
157
156
  // 2. Cross-table filters (adds JOINs for cross-table fields)
158
- addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
157
+ addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
159
158
  async (hookParams) => crossTableFiltersHook(hookParams, polymorphicFiltersHookParams)
160
- );
161
-
159
+ )
160
+
162
161
  // 3. Basic filters (processes simple main table filters)
163
- addHook('knexQueryFiltering', 'basicFiltersHook', {},
162
+ addHook('knexQueryFiltering', 'basicFiltersHook', {},
164
163
  async (hookParams) => basicFiltersHook(hookParams, polymorphicFiltersHookParams)
165
- );
166
-
167
- // 3. Basic filters (processes simple main table filters)
168
- addHook('release', 'releaseHook', {},
169
- async ({ api }) => api.knex.instance.destroy()
170
- );
164
+ )
171
165
 
166
+ // 3. Basic filters (processes simple main table filters)
167
+ addHook('release', 'releaseHook', {},
168
+ async ({ api }) => api.knex.instance.destroy()
169
+ )
172
170
 
173
171
  // Helper scope method to get all schema-related information
174
172
  addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
175
173
  // Create a filtered schema that excludes virtual fields
176
- const schemaStructure = vars.schemaInfo.schemaStructure || {};
177
- const filteredSchema = {};
178
-
174
+ const schemaStructure = vars.schemaInfo.schemaStructure || {}
175
+ const filteredSchema = {}
176
+
179
177
  // Copy only non-virtual fields to the filtered schema
180
178
  Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
181
179
  if (!fieldDef.virtual) {
182
- filteredSchema[fieldName] = fieldDef;
180
+ filteredSchema[fieldName] = fieldDef
183
181
  }
184
- });
185
-
182
+ })
183
+
186
184
  // Create schema object from filtered fields
187
- const tableSchemaInstance = createSchema(filteredSchema);
188
-
185
+ const tableSchemaInstance = createSchema(filteredSchema)
186
+
189
187
  await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
190
188
  })
191
189
 
192
-
193
- // Helper scope method to alter existing fields in a table
194
- addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
190
+ // Helper scope method to alter existing fields in a table
191
+ addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
195
192
  // Validate required parameters
196
- if (!params.fields || typeof params.fields !== 'object') {
197
- throw new Error('fields parameter is required for alterKnexFields');
198
- }
193
+ if (!params.fields || typeof params.fields !== 'object') {
194
+ throw new Error('fields parameter is required for alterKnexFields')
195
+ }
199
196
 
200
- await alterKnexFields(
201
- api.knex.instance,
202
- vars.schemaInfo.tableName,
203
- params.fields,
204
- params.options // Pass through any additional options
205
- );
206
- });
197
+ await alterKnexFields(
198
+ api.knex.instance,
199
+ vars.schemaInfo.tableName,
200
+ params.fields,
201
+ params.options // Pass through any additional options
202
+ )
203
+ })
207
204
 
208
-
209
205
  // Helper scope method to add a field to an existing table
210
206
  addScopeMethod('addKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
211
-
212
207
  // Create schema object from filtered fields
213
- const partialTableSchema = createSchema(params.fields);
214
-
208
+ const partialTableSchema = createSchema(params.fields)
209
+
215
210
  await addKnexFields(api.knex.instance, vars.schemaInfo.tableName, partialTableSchema)
216
211
  })
217
212
 
@@ -226,7 +221,7 @@ export const RestApiKnexPlugin = {
226
221
 
227
222
  /**
228
223
  * Checks if a resource exists in the database
229
- *
224
+ *
230
225
  * @param {Object} params - The parameters object
231
226
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
232
227
  * @param {Object} params.context - The context object containing request-specific data
@@ -238,32 +233,32 @@ export const RestApiKnexPlugin = {
238
233
  * @returns {Promise<boolean>} True if the resource exists, false otherwise
239
234
  */
240
235
  helpers.dataExists = async ({ scopeName, context }) => {
241
- const storageAdapter = getScopeStorageAdapter(scopeName);
236
+ const storageAdapter = getScopeStorageAdapter(scopeName)
242
237
  if (storageAdapter) {
243
- context.storageAdapter = storageAdapter;
238
+ context.storageAdapter = storageAdapter
244
239
  }
245
240
 
246
- const id = context.id;
241
+ const id = context.id
242
+
243
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
244
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
245
+ const db = context.db || api.knex.instance
246
+
247
+ log.debug(`[Knex] EXISTS ${tableName}/${id}`)
248
+
249
+ const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
247
250
 
248
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
249
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
250
- const db = context.db || api.knex.instance;
251
-
252
- log.debug(`[Knex] EXISTS ${tableName}/${id}`);
253
-
254
- const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id';
255
-
256
251
  const record = await db(tableName)
257
252
  .where(idProperty, id)
258
253
  .select(selectClause)
259
- .first();
260
-
261
- return !!record;
262
- };
254
+ .first()
255
+
256
+ return !!record
257
+ }
263
258
 
264
259
  /**
265
260
  * Retrieves a single resource by ID with support for sparse fieldsets and includes
266
- *
261
+ *
267
262
  * @param {Object} params - The parameters object
268
263
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
269
264
  * @param {Object} params.context - The context object containing request-specific data
@@ -281,89 +276,89 @@ export const RestApiKnexPlugin = {
281
276
  * @throws {RestApiResourceError} When the resource is not found
282
277
  */
283
278
  helpers.dataGet = async ({ scopeName, context, runHooks }) => {
284
- const storageAdapter = getScopeStorageAdapter(scopeName);
279
+ const storageAdapter = getScopeStorageAdapter(scopeName)
285
280
  if (storageAdapter) {
286
- context.storageAdapter = storageAdapter;
281
+ context.storageAdapter = storageAdapter
287
282
  }
288
- const scope = api.resources[scopeName];
283
+ const scope = api.resources[scopeName]
289
284
  if (!scope) {
290
- log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) });
291
- throw new Error(`Scope '${scopeName}' not found in api.resources`);
285
+ log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) })
286
+ throw new Error(`Scope '${scopeName}' not found in api.resources`)
292
287
  }
293
288
  if (!scope.scopeName && !scope.name) {
294
- log.debug('[DATA-GET] Scope structure:', {
289
+ log.debug('[DATA-GET] Scope structure:', {
295
290
  scopeKeys: Object.keys(scope),
296
291
  scopeName,
297
292
  hasVars: !!scope.vars,
298
293
  varKeys: scope.vars ? Object.keys(scope.vars) : []
299
- });
294
+ })
300
295
  }
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;
305
-
306
- log.debug(`[Knex] GET ${tableName}/${id}`);
307
-
296
+ const id = context.id
297
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
298
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
299
+ const db = context.db || api.knex.instance
300
+
301
+ log.debug(`[Knex] GET ${tableName}/${id}`)
302
+
308
303
  // Build field selection for sparse fieldsets
309
304
  // This determines which fields to SELECT from database
310
305
  // and tracks dependencies needed for computed fields
311
306
  const fieldSelectionInfo = await buildFieldSelection(
312
307
  scope,
313
308
  { context }
314
- );
315
-
309
+ )
310
+
316
311
  // Store dependency info in context for enrichAttributes
317
312
  // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
318
313
  // The REST API plugin will use this to remove 'cost' from response if not requested
319
- context.computedDependencies = fieldSelectionInfo.computedDependencies;
320
-
314
+ context.computedDependencies = fieldSelectionInfo.computedDependencies
315
+
321
316
  // Build query - no filtering hooks for single records
322
317
  // Permission checks will handle access control
323
- let query = db(tableName).where(idProperty, id);
324
-
318
+ let query = db(tableName).where(idProperty, id)
319
+
325
320
  // Apply field selection
326
- const selectTranslator = createSelectTranslator(storageAdapter);
321
+ const selectTranslator = createSelectTranslator(storageAdapter)
327
322
  query = buildQuerySelection(
328
323
  query,
329
324
  tableName,
330
325
  fieldSelectionInfo.fieldsToSelect,
331
326
  false,
332
- selectTranslator ? { translateColumn: selectTranslator } : undefined,
333
- );
334
-
335
- const record = await query.first();
336
-
327
+ selectTranslator ? { translateColumn: selectTranslator } : undefined
328
+ )
329
+
330
+ const record = await query.first()
331
+
337
332
  if (!record) {
338
333
  throw new RestApiResourceError(
339
- `Resource not found`,
340
- {
334
+ 'Resource not found',
335
+ {
341
336
  subtype: ERROR_SUBTYPES.NOT_FOUND,
342
337
  resourceType: scopeName,
343
338
  resourceId: id
344
339
  }
345
- );
340
+ )
346
341
  }
347
-
342
+
348
343
  // Load relationship identifiers for all hasMany relationships
349
- const records = [record]; // Wrap in array for processing
350
- await loadRelationshipIdentifiers(records, scopeName, scopes, db);
351
-
344
+ const records = [record] // Wrap in array for processing
345
+ await loadRelationshipIdentifiers(records, scopeName, scopes, db)
346
+
352
347
  // Process includes
353
348
  const included = await processIncludes(scope, records, {
354
349
  log,
355
350
  scopes,
356
351
  knex,
357
352
  context
358
- });
359
-
353
+ })
354
+
360
355
  // Build and return response
361
- return buildJsonApiResponse(scope, records, included, true, scopeName, context);
362
- };
356
+ return buildJsonApiResponse(scope, records, included, true, scopeName, context)
357
+ }
363
358
 
364
359
  /**
365
360
  * Retrieves a single resource by ID with minimal processing (no includes or sparse fieldsets)
366
- *
361
+ *
367
362
  * @param {Object} params - The parameters object
368
363
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
369
364
  * @param {Object} params.context - The context object containing request-specific data
@@ -375,41 +370,41 @@ export const RestApiKnexPlugin = {
375
370
  * @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
376
371
  */
377
372
  helpers.dataGetMinimal = async ({ scopeName, context }) => {
378
- const storageAdapter = getScopeStorageAdapter(scopeName);
373
+ const storageAdapter = getScopeStorageAdapter(scopeName)
379
374
  if (storageAdapter) {
380
- context.storageAdapter = storageAdapter;
375
+ context.storageAdapter = storageAdapter
381
376
  }
382
- const scope = api.resources[scopeName];
383
- const id = context.id;
384
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
385
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
386
- const db = context.db || api.knex.instance;
387
-
388
- log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`);
389
-
377
+ const scope = api.resources[scopeName]
378
+ const id = context.id
379
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
380
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
381
+ const db = context.db || api.knex.instance
382
+
383
+ log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
384
+
390
385
  // Build query - no filtering hooks for single records
391
386
  // Permission checks will handle access control
392
- let query = db(tableName).where(idProperty, id);
393
-
387
+ let query = db(tableName).where(idProperty, id)
388
+
394
389
  // Add alias if idProperty is not 'id'
395
390
  if (idProperty !== 'id') {
396
- query = query.select('*', `${idProperty} as id`);
391
+ query = query.select('*', `${idProperty} as id`)
397
392
  }
398
-
393
+
399
394
  // Execute query
400
- const record = await query.first();
401
-
395
+ const record = await query.first()
396
+
402
397
  if (!record) {
403
- return null;
398
+ return null
404
399
  }
405
-
400
+
406
401
  // Transform to JSON:API format with belongsTo relationships
407
- return toJsonApiRecordWithBelongsTo(scope, record, scopeName);
408
- };
402
+ return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
403
+ }
409
404
 
410
405
  /**
411
406
  * Queries resources with support for filtering, sorting, pagination, sparse fieldsets, and includes
412
- *
407
+ *
413
408
  * @param {Object} params - The parameters object
414
409
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
415
410
  * @param {Object} params.context - The context object containing request-specific data
@@ -436,61 +431,61 @@ export const RestApiKnexPlugin = {
436
431
  * @returns {Promise<Object>} JSON:API formatted response with data array, optional included resources, and pagination meta/links
437
432
  */
438
433
  helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
439
- const storageAdapter = getScopeStorageAdapter(scopeName);
434
+ const storageAdapter = getScopeStorageAdapter(scopeName)
440
435
  if (storageAdapter) {
441
- context.storageAdapter = storageAdapter;
436
+ context.storageAdapter = storageAdapter
442
437
  }
443
- const scope = api.resources[scopeName];
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 });
451
- log.debug(`[Knex] QUERY ${tableName}`, queryParams);
452
-
438
+ const scope = api.resources[scopeName]
439
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
440
+ const schemaInfo = context.schemaInfo
441
+ const queryParams = context.queryParams
442
+ const db = context.db || api.knex.instance
443
+ const sortableFields = context.sortableFields
444
+
445
+ log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
446
+ log.debug(`[Knex] QUERY ${tableName}`, queryParams)
447
+
453
448
  // Build field selection for sparse fieldsets
454
449
  // This determines which fields to SELECT from database
455
450
  // and tracks dependencies needed for computed fields
456
451
  const fieldSelectionInfo = await buildFieldSelection(
457
452
  scope,
458
453
  { context }
459
- );
460
-
454
+ )
455
+
461
456
  // Store dependency info in context for enrichAttributes
462
457
  // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
463
458
  // The REST API plugin will use this to remove 'cost' from response if not requested
464
- context.computedDependencies = fieldSelectionInfo.computedDependencies;
465
-
459
+ context.computedDependencies = fieldSelectionInfo.computedDependencies
460
+
466
461
  // Start building query with table prefix (for JOIN support)
467
- let query = db(tableName);
462
+ let query = db(tableName)
468
463
 
469
- const selectTranslator = createSelectTranslator(storageAdapter);
464
+ const selectTranslator = createSelectTranslator(storageAdapter)
470
465
  query = buildQuerySelection(
471
466
  query,
472
467
  tableName,
473
468
  fieldSelectionInfo.fieldsToSelect,
474
469
  true,
475
- selectTranslator ? { translateColumn: selectTranslator } : undefined,
476
- );
477
-
470
+ selectTranslator ? { translateColumn: selectTranslator } : undefined
471
+ )
472
+
478
473
  /* ═══════════════════════════════════════════════════════════════════
479
474
  * FILTERING HOOKS
480
475
  * This is where the magic happens. The knexQueryFiltering hook is called
481
476
  * to apply all filter conditions. The searchSchemaFilter hook (registered
482
477
  * above) will process the searchSchema and apply filters with JOINs.
483
- *
478
+ *
484
479
  * IMPORTANT: Each hook should wrap its conditions in query.where(function() {...})
485
480
  * to ensure proper grouping and prevent accidental filter bypass.
486
481
  * ═══════════════════════════════════════════════════════════════════ */
487
-
488
- log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName });
489
-
490
- log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' });
491
-
492
- log.trace('[DATA-QUERY] Storing query data in context before calling runHooks');
493
-
482
+
483
+ log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
484
+
485
+ log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' })
486
+
487
+ log.trace('[DATA-QUERY] Storing query data in context before calling runHooks')
488
+
494
489
  // YOU ARE HERE: pass 'sesrchSchemaInstance' in the context below
495
490
 
496
491
  // Store the query data in context where hooks can access it
@@ -504,63 +499,63 @@ export const RestApiKnexPlugin = {
504
499
  tableName,
505
500
  db,
506
501
  adapter: storageAdapter,
507
- };
508
-
509
- log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters });
502
+ }
503
+
504
+ log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
510
505
  }
511
-
512
- await runHooks('knexQueryFiltering');
513
-
506
+
507
+ await runHooks('knexQueryFiltering')
508
+
514
509
  // Clean up after hook execution
515
510
  if (context && context.knexQuery) {
516
- delete context.knexQuery;
511
+ delete context.knexQuery
517
512
  }
518
-
519
- log.trace('[DATA-QUERY] Finished knexQueryFiltering hook');
520
-
513
+
514
+ log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
515
+
521
516
  // Apply sorting directly (no hooks)
522
517
  if (queryParams.sort && queryParams.sort.length > 0) {
523
518
  queryParams.sort.forEach(sortField => {
524
- const desc = sortField.startsWith('-');
525
- const field = desc ? sortField.substring(1) : sortField;
526
-
519
+ const desc = sortField.startsWith('-')
520
+ const field = desc ? sortField.substring(1) : sortField
521
+
527
522
  // Check if field is sortable
528
523
  if (sortableFields && sortableFields.length > 0 && !sortableFields.includes(field)) {
529
- log.warn(`Ignoring non-sortable field: ${field}`);
530
- return; // Skip non-sortable fields
524
+ log.warn(`Ignoring non-sortable field: ${field}`)
525
+ return // Skip non-sortable fields
531
526
  }
532
-
527
+
533
528
  // Map relationship names to actual database columns for sorting
534
529
  // Check if this is a relationship field that needs to be mapped
535
- let dbField = field;
536
- const searchField = schemaInfo.searchSchemaStructure?.[field];
530
+ let dbField = field
531
+ const searchField = schemaInfo.searchSchemaStructure?.[field]
537
532
  if (searchField?.actualField) {
538
533
  // This is a relationship field, use the actual database column
539
- dbField = searchField.actualField;
534
+ dbField = searchField.actualField
540
535
  }
541
536
  const translatedField = storageAdapter?.translateColumn
542
537
  ? storageAdapter.translateColumn(dbField)
543
- : dbField;
538
+ : dbField
544
539
 
545
- query.orderBy(translatedField, desc ? 'desc' : 'asc');
546
- });
540
+ query.orderBy(translatedField, desc ? 'desc' : 'asc')
541
+ })
547
542
  }
548
-
543
+
549
544
  // Apply pagination
550
545
  // Check if page object has any actual pagination parameters
551
- const hasPageParams = queryParams.page &&
552
- (queryParams.page.size !== undefined ||
553
- queryParams.page.number !== undefined ||
554
- queryParams.page.after !== undefined ||
555
- queryParams.page.before !== undefined);
556
-
546
+ const hasPageParams = queryParams.page &&
547
+ (queryParams.page.size !== undefined ||
548
+ queryParams.page.number !== undefined ||
549
+ queryParams.page.after !== undefined ||
550
+ queryParams.page.before !== undefined)
551
+
557
552
  if (hasPageParams) {
558
- const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT;
553
+ const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
559
554
  const pageSize = Math.min(
560
555
  requestedSize,
561
556
  scope.vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
562
- );
563
-
557
+ )
558
+
564
559
  // Validate page size
565
560
  if (requestedSize <= 0) {
566
561
  throw new RestApiValidationError(
@@ -573,28 +568,28 @@ export const RestApiKnexPlugin = {
573
568
  message: 'Page size must be a positive number'
574
569
  }]
575
570
  }
576
- );
571
+ )
577
572
  }
578
-
573
+
579
574
  // Offset-based pagination
580
575
  if (queryParams.page.number !== undefined) {
581
- const pageNumber = queryParams.page.number || 1;
576
+ const pageNumber = queryParams.page.number || 1
582
577
  query
583
578
  .limit(pageSize)
584
- .offset((pageNumber - 1) * pageSize);
579
+ .offset((pageNumber - 1) * pageSize)
585
580
  }
586
581
  // Cursor-based pagination
587
582
  else if (queryParams.page.after || queryParams.page.before) {
588
583
  // Fetch one extra record to determine if there are more
589
- query.limit(pageSize + 1);
590
-
584
+ query.limit(pageSize + 1)
585
+
591
586
  // Get the configured ID property
592
- const idProperty = context.schemaInfo.idProperty || 'id';
593
-
587
+ const idProperty = context.schemaInfo.idProperty || 'id'
588
+
594
589
  if (queryParams.page.after) {
595
- let cursorData;
590
+ let cursorData
596
591
  try {
597
- cursorData = parseCursor(queryParams.page.after);
592
+ cursorData = parseCursor(queryParams.page.after)
598
593
  } catch (error) {
599
594
  throw new RestApiValidationError(
600
595
  'Invalid cursor format in page[after] parameter',
@@ -606,64 +601,63 @@ export const RestApiKnexPlugin = {
606
601
  message: 'The cursor value is not valid'
607
602
  }]
608
603
  }
609
- );
604
+ )
610
605
  }
611
-
606
+
612
607
  // Build sort fields array with directions
613
- const sortFields = [];
608
+ const sortFields = []
614
609
  if (queryParams.sort && queryParams.sort.length > 0) {
615
610
  queryParams.sort.forEach(sortField => {
616
- const desc = sortField.startsWith('-');
617
- const field = desc ? sortField.substring(1) : sortField;
618
- sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' });
619
- });
611
+ const desc = sortField.startsWith('-')
612
+ const field = desc ? sortField.substring(1) : sortField
613
+ sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
614
+ })
620
615
  } else if (scope.vars.defaultSort) {
621
616
  sortFields.push({
622
617
  field: scope.vars.defaultSort.field || idProperty,
623
618
  direction: scope.vars.defaultSort.direction || 'ASC'
624
- });
619
+ })
625
620
  } else {
626
- sortFields.push({ field: idProperty, direction: 'ASC' });
621
+ sortFields.push({ field: idProperty, direction: 'ASC' })
627
622
  }
628
-
623
+
629
624
  // Build compound WHERE clause for multi-field cursor
630
- query.where(function() {
625
+ query.where(function () {
631
626
  sortFields.forEach((sortInfo, index) => {
632
- const { field, direction } = sortInfo;
633
- const qualifiedField = `${tableName}.${field}`;
634
- const cursorValue = cursorData[field];
635
-
627
+ const { field, direction } = sortInfo
628
+ const qualifiedField = `${tableName}.${field}`
629
+ const cursorValue = cursorData[field]
630
+
636
631
  if (cursorValue === undefined) {
637
- log.warn(`Cursor missing value for sort field: ${field}`);
638
- return;
632
+ log.warn(`Cursor missing value for sort field: ${field}`)
633
+ return
639
634
  }
640
-
635
+
641
636
  // Build condition for this level and all previous levels
642
- this.orWhere(function() {
637
+ this.orWhere(function () {
643
638
  // All previous fields must be equal
644
639
  for (let i = 0; i < index; i++) {
645
- const prevField = sortFields[i].field;
646
- const prevQualifiedField = `${tableName}.${prevField}`;
647
- const prevValue = cursorData[prevField];
640
+ const prevField = sortFields[i].field
641
+ const prevQualifiedField = `${tableName}.${prevField}`
642
+ const prevValue = cursorData[prevField]
648
643
  if (prevValue !== undefined) {
649
- this.where(prevQualifiedField, '=', prevValue);
644
+ this.where(prevQualifiedField, '=', prevValue)
650
645
  }
651
646
  }
652
-
647
+
653
648
  // Current field must be greater/less than cursor value
654
649
  if (direction === 'DESC') {
655
- this.where(qualifiedField, '<', cursorValue);
650
+ this.where(qualifiedField, '<', cursorValue)
656
651
  } else {
657
- this.where(qualifiedField, '>', cursorValue);
652
+ this.where(qualifiedField, '>', cursorValue)
658
653
  }
659
- });
660
- });
661
- });
662
-
654
+ })
655
+ })
656
+ })
663
657
  } else if (queryParams.page.before) {
664
- let cursorData;
658
+ let cursorData
665
659
  try {
666
- cursorData = parseCursor(queryParams.page.before);
660
+ cursorData = parseCursor(queryParams.page.before)
667
661
  } catch (error) {
668
662
  throw new RestApiValidationError(
669
663
  'Invalid cursor format in page[before] parameter',
@@ -675,82 +669,82 @@ export const RestApiKnexPlugin = {
675
669
  message: 'The cursor value is not valid'
676
670
  }]
677
671
  }
678
- );
672
+ )
679
673
  }
680
-
674
+
681
675
  // Build sort fields array with directions
682
- const sortFields = [];
676
+ const sortFields = []
683
677
  if (queryParams.sort && queryParams.sort.length > 0) {
684
678
  queryParams.sort.forEach(sortField => {
685
- const desc = sortField.startsWith('-');
686
- const field = desc ? sortField.substring(1) : sortField;
687
- sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' });
688
- });
679
+ const desc = sortField.startsWith('-')
680
+ const field = desc ? sortField.substring(1) : sortField
681
+ sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
682
+ })
689
683
  } else if (scope.vars.defaultSort) {
690
684
  sortFields.push({
691
685
  field: scope.vars.defaultSort.field || idProperty,
692
686
  direction: scope.vars.defaultSort.direction || 'ASC'
693
- });
687
+ })
694
688
  } else {
695
- sortFields.push({ field: idProperty, direction: 'ASC' });
689
+ sortFields.push({ field: idProperty, direction: 'ASC' })
696
690
  }
697
-
691
+
698
692
  // Build compound WHERE clause for multi-field cursor (reversed for before)
699
- query.where(function() {
693
+ query.where(function () {
700
694
  sortFields.forEach((sortInfo, index) => {
701
- const { field, direction } = sortInfo;
702
- const qualifiedField = `${tableName}.${field}`;
703
- const cursorValue = cursorData[field];
704
-
695
+ const { field, direction } = sortInfo
696
+ const qualifiedField = `${tableName}.${field}`
697
+ const cursorValue = cursorData[field]
698
+
705
699
  if (cursorValue === undefined) {
706
- log.warn(`Cursor missing value for sort field: ${field}`);
707
- return;
700
+ log.warn(`Cursor missing value for sort field: ${field}`)
701
+ return
708
702
  }
709
-
703
+
710
704
  // Build condition for this level and all previous levels
711
- this.orWhere(function() {
705
+ this.orWhere(function () {
712
706
  // All previous fields must be equal
713
707
  for (let i = 0; i < index; i++) {
714
- const prevField = sortFields[i].field;
715
- const prevQualifiedField = `${tableName}.${prevField}`;
716
- const prevValue = cursorData[prevField];
708
+ const prevField = sortFields[i].field
709
+ const prevQualifiedField = `${tableName}.${prevField}`
710
+ const prevValue = cursorData[prevField]
717
711
  if (prevValue !== undefined) {
718
- this.where(prevQualifiedField, '=', prevValue);
712
+ this.where(prevQualifiedField, '=', prevValue)
719
713
  }
720
714
  }
721
-
715
+
722
716
  // Current field must be less/greater than cursor value (reversed)
723
717
  if (direction === 'DESC') {
724
- this.where(qualifiedField, '>', cursorValue);
718
+ this.where(qualifiedField, '>', cursorValue)
725
719
  } else {
726
- this.where(qualifiedField, '<', cursorValue);
720
+ this.where(qualifiedField, '<', cursorValue)
727
721
  }
728
- });
729
- });
730
- });
722
+ })
723
+ })
724
+ })
731
725
  }
732
726
  }
733
727
  // Default pagination if only size is specified - treat as cursor-based for "load more"
734
728
  else {
735
729
  // Fetch one extra to detect hasMore
736
- query.limit(pageSize + 1);
730
+ query.limit(pageSize + 1)
737
731
  }
738
732
  } else {
739
733
  // No pagination params provided - apply default limit
740
- const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT;
741
- query.limit(defaultLimit);
734
+ const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
735
+ query.limit(defaultLimit)
742
736
  }
743
-
737
+
744
738
  // Execute query
745
- const records = await query;
746
-
739
+ const records = await query
740
+
747
741
  // Store query string for response building
748
- const queryParts = [];
742
+ const queryParts = []
749
743
  Object.entries(queryParams).forEach(([key, value]) => {
750
744
  if (Array.isArray(value)) {
751
745
  // Handle arrays (like sort, include)
752
746
  if (value.length > 0) {
753
- queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`);
747
+ queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`)
754
748
  }
755
749
  } else if (typeof value === 'object' && value !== null) {
756
750
  // Handle nested objects (like filters, fields, page)
@@ -760,33 +754,33 @@ export const RestApiKnexPlugin = {
760
754
  // Handle deeply nested objects (like filters[country][code])
761
755
  Object.entries(subValue).forEach(([subSubKey, subSubValue]) => {
762
756
  if (subSubValue !== undefined && subSubValue !== null) {
763
- queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`);
757
+ queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`)
764
758
  }
765
- });
759
+ })
766
760
  } else {
767
- queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`);
761
+ queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`)
768
762
  }
769
763
  }
770
- });
764
+ })
771
765
  } else if (value !== undefined && value !== null) {
772
- queryParts.push(`${key}=${encodeURIComponent(value)}`);
766
+ queryParts.push(`${key}=${encodeURIComponent(value)}`)
773
767
  }
774
- });
775
-
768
+ })
769
+
776
770
  // Initialize returnMeta namespace for thread-safe metadata
777
- context.returnMeta = context.returnMeta || {};
778
- context.returnMeta.queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
771
+ context.returnMeta = context.returnMeta || {}
772
+ context.returnMeta.queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
779
773
 
780
774
  // Execute count query for pagination if offset-based pagination is used
781
775
  if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
782
- const page = parseInt(queryParams.page?.number) || 1;
783
- const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT;
784
-
776
+ const page = parseInt(queryParams.page?.number) || 1
777
+ const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
778
+
785
779
  // Only execute count query if enabled
786
780
  if (scope.vars.enablePaginationCounts) {
787
781
  // Build count query with same filters as main query
788
- const countQuery = db(tableName);
789
-
782
+ const countQuery = db(tableName)
783
+
790
784
  // Apply filters through hooks (same as main query)
791
785
  if (queryParams.filters && Object.keys(queryParams.filters).length > 0) {
792
786
  // Store query data in context for hooks
@@ -798,67 +792,67 @@ export const RestApiKnexPlugin = {
798
792
  tableName,
799
793
  schemaInfo,
800
794
  db
801
- };
795
+ }
802
796
  }
803
-
797
+
804
798
  // Run the same filtering hooks
805
- await runHooks('knexQueryFiltering');
806
-
799
+ await runHooks('knexQueryFiltering')
800
+
807
801
  // Clean up
808
802
  if (context && context.knexQuery) {
809
- delete context.knexQuery;
803
+ delete context.knexQuery
810
804
  }
811
805
  }
812
-
806
+
813
807
  // Get total count
814
- const countResult = await countQuery.count('* as total').first();
815
- const total = parseInt(countResult.total);
816
-
808
+ const countResult = await countQuery.count('* as total').first()
809
+ const total = parseInt(countResult.total)
810
+
817
811
  // Calculate pagination metadata with total
818
- context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize);
812
+ context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
819
813
  } else {
820
814
  // Without count, we can still provide basic pagination info
821
815
  context.returnMeta.paginationMeta = {
822
816
  page,
823
817
  pageSize
824
818
  // No total, pageCount, or hasMore when counts are disabled
825
- };
819
+ }
826
820
  }
827
-
828
- // Generate links
829
- const urlPrefix = getUrlPrefix(context, scope);
821
+
822
+ // Generate links
823
+ const urlPrefix = getUrlPrefix(context, scope)
830
824
  context.returnMeta.paginationLinks = generatePaginationLinks(
831
825
  urlPrefix,
832
826
  scopeName,
833
827
  queryParams,
834
828
  context.returnMeta.paginationMeta
835
- );
829
+ )
836
830
  }
837
-
831
+
838
832
  // Handle cursor-based pagination meta
839
833
  // Generate cursor metadata when using cursor parameters OR when only size is specified (no page number)
840
- if (queryParams.page?.after || queryParams.page?.before ||
834
+ if (queryParams.page?.after || queryParams.page?.before ||
841
835
  (queryParams.page?.size && queryParams.page?.number === undefined)) {
842
- const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT;
843
-
836
+ const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
837
+
844
838
  // Check if there are more records
845
839
  // We fetched pageSize + 1 records to detect if there are more
846
- const hasMore = records.length > pageSize;
847
-
840
+ const hasMore = records.length > pageSize
841
+
848
842
  // Remove the extra record if present
849
843
  if (hasMore) {
850
- records.pop();
844
+ records.pop()
851
845
  }
852
-
846
+
853
847
  // Determine sort fields for cursor
854
- const idProperty = context.schemaInfo.idProperty || 'id';
855
- let sortFields = [idProperty];
848
+ const idProperty = context.schemaInfo.idProperty || 'id'
849
+ let sortFields = [idProperty]
856
850
  if (queryParams.sort && queryParams.sort.length > 0) {
857
- sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s);
851
+ sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s)
858
852
  }
859
-
860
- context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields);
861
- const urlPrefix = getUrlPrefix(context, scope);
853
+
854
+ context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields)
855
+ const urlPrefix = getUrlPrefix(context, scope)
862
856
  context.returnMeta.paginationLinks = generateCursorPaginationLinks(
863
857
  urlPrefix,
864
858
  scopeName,
@@ -867,12 +861,12 @@ export const RestApiKnexPlugin = {
867
861
  pageSize,
868
862
  hasMore,
869
863
  sortFields
870
- );
864
+ )
871
865
  }
872
-
866
+
873
867
  // Load relationship identifiers for all hasMany relationships
874
- await loadRelationshipIdentifiers(records, scopeName, scopes, db);
875
-
868
+ await loadRelationshipIdentifiers(records, scopeName, scopes, db)
869
+
876
870
  // Process includes
877
871
  const included = await processIncludes(scope, records, {
878
872
  log,
@@ -880,16 +874,15 @@ export const RestApiKnexPlugin = {
880
874
  knex,
881
875
  context,
882
876
  api
883
- });
884
-
877
+ })
885
878
 
886
879
  // Build and return response
887
- return buildJsonApiResponse(scope, records, included, false, scopeName, context);
888
- };
889
-
880
+ return buildJsonApiResponse(scope, records, included, false, scopeName, context)
881
+ }
882
+
890
883
  /**
891
884
  * Creates a new resource in the database
892
- *
885
+ *
893
886
  * @param {Object} params - The parameters object
894
887
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
895
888
  * @param {Object} params.context - The context object containing request-specific data
@@ -904,35 +897,35 @@ export const RestApiKnexPlugin = {
904
897
  * @returns {Promise<string|number>} The ID of the newly created resource
905
898
  */
906
899
  helpers.dataPost = async ({ scopeName, context }) => {
907
- const storageAdapter = getScopeStorageAdapter(scopeName);
900
+ const storageAdapter = getScopeStorageAdapter(scopeName)
908
901
  if (storageAdapter) {
909
- context.storageAdapter = storageAdapter;
902
+ context.storageAdapter = storageAdapter
910
903
  }
911
- const scope = api.resources[scopeName];
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;
916
-
917
- log.debug(`[Knex] POST ${tableName}`, inputRecord);
918
-
904
+ const scope = api.resources[scopeName]
905
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
906
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
907
+ const db = context.db || api.knex.instance
908
+ const inputRecord = context.inputRecord
909
+
910
+ log.debug(`[Knex] POST ${tableName}`, inputRecord)
911
+
919
912
  // Extract attributes from JSON:API format
920
- const attributes = inputRecord.data.attributes;
921
-
913
+ const attributes = inputRecord.data.attributes
914
+
922
915
  // Strip non-database fields (computed and virtual) before insert
923
- const dbAttributes = stripNonDatabaseFields(attributes, context.schemaInfo);
924
-
916
+ const dbAttributes = stripNonDatabaseFields(attributes, context.schemaInfo)
917
+
925
918
  // Insert and get the new ID
926
- const result = await db(tableName).insert(dbAttributes).returning(idProperty);
927
-
919
+ const result = await db(tableName).insert(dbAttributes).returning(idProperty)
920
+
928
921
  // Extract the ID value (SQLite returns array of objects)
929
- const id = result[0]?.[idProperty] || result[0];
922
+ const id = result[0]?.[idProperty] || result[0]
930
923
  return id
931
- };
932
-
924
+ }
925
+
933
926
  /**
934
927
  * Replaces an entire resource (PUT operation) or creates it with a specific ID
935
- *
928
+ *
936
929
  * @param {Object} params - The parameters object
937
930
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
938
931
  * @param {Object} params.context - The context object containing request-specific data
@@ -951,31 +944,31 @@ export const RestApiKnexPlugin = {
951
944
  * @throws {RestApiResourceError} When updating and the resource is not found
952
945
  */
953
946
  helpers.dataPut = async ({ scopeName, context }) => {
954
- const storageAdapter = getScopeStorageAdapter(scopeName);
947
+ const storageAdapter = getScopeStorageAdapter(scopeName)
955
948
  if (storageAdapter) {
956
- context.storageAdapter = storageAdapter;
949
+ context.storageAdapter = storageAdapter
957
950
  }
958
- const scope = api.resources[scopeName];
959
- const id = context.id;
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;
965
-
966
- log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`);
967
-
951
+ const scope = api.resources[scopeName]
952
+ const id = context.id
953
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
954
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
955
+ const db = context.db || api.knex.instance
956
+ const inputRecord = context.inputRecord
957
+ const isCreate = context.isCreate
958
+
959
+ log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
960
+
968
961
  // Extract attributes and process relationships using helper
969
- const attributes = inputRecord.data.attributes || {};
970
- const foreignKeyUpdates = processBelongsToRelationships(scope, { context });
971
- const mergedAttributes = { ...attributes, ...foreignKeyUpdates };
972
-
962
+ const attributes = inputRecord.data.attributes || {}
963
+ const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
964
+ const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
965
+
973
966
  // Strip non-database fields (computed and virtual) before database operation
974
- const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo);
975
-
967
+ const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
968
+
976
969
  // Map 'id' to actual idProperty if needed (for PUT with specific ID)
977
970
  if (idProperty !== 'id' && inputRecord.data.id) {
978
- finalAttributes[idProperty] = inputRecord.data.id;
971
+ finalAttributes[idProperty] = inputRecord.data.id
979
972
  }
980
973
 
981
974
  if (isCreate) {
@@ -983,43 +976,41 @@ export const RestApiKnexPlugin = {
983
976
  const recordData = {
984
977
  ...finalAttributes,
985
978
  [idProperty]: id
986
- };
987
-
988
- await db(tableName).insert(recordData);
979
+ }
980
+
981
+ await db(tableName).insert(recordData)
989
982
  } else {
990
983
  // Update mode - check if record exists first
991
984
  const exists = await db(tableName)
992
985
  .where(idProperty, id)
993
- .first();
994
-
986
+ .first()
987
+
995
988
  if (!exists) {
996
989
  throw new RestApiResourceError(
997
- `Resource not found`,
998
- {
990
+ 'Resource not found',
991
+ {
999
992
  subtype: ERROR_SUBTYPES.NOT_FOUND,
1000
993
  resourceType: scopeName,
1001
994
  resourceId: id
1002
995
  }
1003
- );
996
+ )
1004
997
  }
1005
-
998
+
1006
999
  // Remove the idProperty from attributes to prevent updating the primary key
1007
- delete finalAttributes[idProperty];
1008
-
1000
+ delete finalAttributes[idProperty]
1001
+
1009
1002
  // Update the record (replace all fields)
1010
1003
  if (Object.keys(finalAttributes).length > 0) {
1011
1004
  await db(tableName)
1012
1005
  .where(idProperty, id)
1013
- .update(finalAttributes);
1006
+ .update(finalAttributes)
1014
1007
  }
1015
1008
  }
1016
-
1017
- return
1018
- };
1019
-
1009
+ }
1010
+
1020
1011
  /**
1021
1012
  * Partially updates a resource (PATCH operation)
1022
- *
1013
+ *
1023
1014
  * @param {Object} params - The parameters object
1024
1015
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
1025
1016
  * @param {Object} params.context - The context object containing request-specific data
@@ -1037,61 +1028,59 @@ export const RestApiKnexPlugin = {
1037
1028
  * @throws {RestApiResourceError} When the resource is not found
1038
1029
  */
1039
1030
  helpers.dataPatch = async ({ scopeName, context }) => {
1040
- const storageAdapter = getScopeStorageAdapter(scopeName);
1031
+ const storageAdapter = getScopeStorageAdapter(scopeName)
1041
1032
  if (storageAdapter) {
1042
- context.storageAdapter = storageAdapter;
1033
+ context.storageAdapter = storageAdapter
1043
1034
  }
1044
- const scope = api.resources[scopeName];
1045
- const id = context.id;
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;
1050
-
1051
- log.debug(`[Knex] PATCH ${tableName}/${id}`);
1052
-
1035
+ const scope = api.resources[scopeName]
1036
+ const id = context.id
1037
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
1038
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
1039
+ const db = context.db || api.knex.instance
1040
+ const inputRecord = context.inputRecord
1041
+
1042
+ log.debug(`[Knex] PATCH ${tableName}/${id}`)
1043
+
1053
1044
  // Check if record exists
1054
1045
  const exists = await db(tableName)
1055
1046
  .where(idProperty, id)
1056
- .first();
1057
-
1047
+ .first()
1048
+
1058
1049
  if (!exists) {
1059
1050
  throw new RestApiResourceError(
1060
- `Resource not found`,
1061
- {
1051
+ 'Resource not found',
1052
+ {
1062
1053
  subtype: ERROR_SUBTYPES.NOT_FOUND,
1063
1054
  resourceType: scopeName,
1064
1055
  resourceId: id
1065
1056
  }
1066
- );
1057
+ )
1067
1058
  }
1068
-
1059
+
1069
1060
  // Extract attributes and process relationships using helper
1070
- const attributes = inputRecord.data.attributes || {};
1071
- const foreignKeyUpdates = processBelongsToRelationships(scope, { context });
1072
- const mergedAttributes = { ...attributes, ...foreignKeyUpdates };
1073
-
1061
+ const attributes = inputRecord.data.attributes || {}
1062
+ const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
1063
+ const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
1064
+
1074
1065
  // Strip non-database fields (computed and virtual) before database operation
1075
- const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo);
1066
+ const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
1067
+
1068
+ log.debug('[Knex] PATCH finalAttributes:', finalAttributes)
1076
1069
 
1077
- log.debug(`[Knex] PATCH finalAttributes:`, finalAttributes);
1078
-
1079
1070
  // Remove the idProperty from attributes to prevent updating the primary key
1080
- delete finalAttributes[idProperty];
1081
-
1071
+ delete finalAttributes[idProperty]
1072
+
1082
1073
  // Update only if there are changes
1083
1074
  if (Object.keys(finalAttributes).length > 0) {
1084
1075
  await db(tableName)
1085
1076
  .where(idProperty, id)
1086
- .update(finalAttributes);
1077
+ .update(finalAttributes)
1087
1078
  }
1088
-
1089
- return
1090
- };
1091
-
1079
+ }
1080
+
1092
1081
  /**
1093
1082
  * Deletes a resource from the database
1094
- *
1083
+ *
1095
1084
  * @param {Object} params - The parameters object
1096
1085
  * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
1097
1086
  * @param {Object} params.context - The context object containing request-specific data
@@ -1104,43 +1093,43 @@ export const RestApiKnexPlugin = {
1104
1093
  * @throws {RestApiResourceError} When the resource is not found
1105
1094
  */
1106
1095
  helpers.dataDelete = async ({ scopeName, context }) => {
1107
- const storageAdapter = getScopeStorageAdapter(scopeName);
1096
+ const storageAdapter = getScopeStorageAdapter(scopeName)
1108
1097
  if (storageAdapter) {
1109
- context.storageAdapter = storageAdapter;
1098
+ context.storageAdapter = storageAdapter
1110
1099
  }
1111
- const scope = api.resources[scopeName];
1112
- const id = context.id;
1113
-
1114
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
1115
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
1116
- const db = context.db || api.knex.instance;
1117
-
1118
- log.debug(`[Knex] DELETE ${tableName}/${id}`);
1119
-
1100
+ const scope = api.resources[scopeName]
1101
+ const id = context.id
1102
+
1103
+ const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
1104
+ const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
1105
+ const db = context.db || api.knex.instance
1106
+
1107
+ log.debug(`[Knex] DELETE ${tableName}/${id}`)
1108
+
1120
1109
  // Check if record exists
1121
1110
  const exists = await db(tableName)
1122
1111
  .where(idProperty, id)
1123
- .first();
1124
-
1112
+ .first()
1113
+
1125
1114
  if (!exists) {
1126
1115
  throw new RestApiResourceError(
1127
- `Resource not found`,
1128
- {
1116
+ 'Resource not found',
1117
+ {
1129
1118
  subtype: ERROR_SUBTYPES.NOT_FOUND,
1130
1119
  resourceType: scopeName,
1131
1120
  resourceId: id
1132
1121
  }
1133
- );
1122
+ )
1134
1123
  }
1135
-
1124
+
1136
1125
  // Delete the record
1137
1126
  await db(tableName)
1138
1127
  .where(idProperty, id)
1139
- .delete();
1140
-
1141
- return { success: true };
1142
- };
1143
-
1144
- log.info('RestApiKnexPlugin installed - basic CRUD operations ready');
1128
+ .delete()
1129
+
1130
+ return { success: true }
1131
+ }
1132
+
1133
+ log.info('RestApiKnexPlugin installed - basic CRUD operations ready')
1145
1134
  }
1146
1135
  }