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,9 +1,9 @@
1
- import {
2
- RestApiValidationError,
3
- RestApiPayloadError
4
- } from '../../../lib/rest-api-errors.js';
5
- import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js';
6
- import { createEnhancedLogger } from '../../../lib/enhanced-logger.js';
1
+ import {
2
+ RestApiValidationError,
3
+ RestApiPayloadError
4
+ } from '../../../lib/rest-api-errors.js'
5
+ import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js'
6
+ import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
7
7
 
8
8
  /**
9
9
  * Gets an enhanced logger instance with full error details and stack traces
@@ -11,11 +11,11 @@ import { createEnhancedLogger } from '../../../lib/enhanced-logger.js';
11
11
  * @returns {Object} Enhanced logger instance
12
12
  */
13
13
  export const getEnhancedLogger = (log) => {
14
- return createEnhancedLogger(log, {
15
- logFullErrors: true,
16
- includeStack: true
17
- });
18
- };
14
+ return createEnhancedLogger(log, {
15
+ logFullErrors: true,
16
+ includeStack: true
17
+ })
18
+ }
19
19
 
20
20
  /**
21
21
  * Cascades configuration values through multiple sources with fallback to default
@@ -25,22 +25,22 @@ export const getEnhancedLogger = (log) => {
25
25
  * @returns {*} The first defined value found or the default
26
26
  */
27
27
  export const cascadeConfig = (settingName, sources, defaultValue) =>
28
- sources.find(source => source?.[settingName] !== undefined)?.[settingName] ?? defaultValue;
28
+ sources.find(source => source?.[settingName] !== undefined)?.[settingName] ?? defaultValue
29
29
 
30
30
  /**
31
31
  * Normalizes return record settings to valid values
32
32
  * @param {string|boolean} value - The value to normalize
33
33
  * @returns {string} Normalized value: 'no', 'minimal', or 'full'
34
34
  */
35
- export function normalizeReturnValue(value) {
36
- if (['no', 'minimal', 'full'].includes(value)) return value;
37
- return 'no'; // default
35
+ export function normalizeReturnValue (value) {
36
+ if (['no', 'minimal', 'full'].includes(value)) return value
37
+ return 'no' // default
38
38
  }
39
39
 
40
40
  /**
41
41
  * Sets up common request context for REST API methods
42
42
  * Handles simplified mode, transaction setup, and initial validation
43
- *
43
+ *
44
44
  * @param {Object} params - The method parameters
45
45
  * @param {Object} context - The request context
46
46
  * @param {Object} vars - Plugin variables
@@ -51,47 +51,47 @@ export function normalizeReturnValue(value) {
51
51
  * @param {Object} helpers - Helper functions
52
52
  * @returns {Object} An object containing schema-related shortcuts
53
53
  */
54
- export async function setupCommonRequest({ params, context, vars, scopes, scopeOptions, scopeName, api, helpers }) {
54
+ export async function setupCommonRequest ({ params, context, vars, scopes, scopeOptions, scopeName, api, helpers }) {
55
55
  // Determine which simplified setting to use based on transport
56
- const isTransport = params.isTransport === true;
57
-
56
+ const isTransport = params.isTransport === true
57
+
58
58
  // Use vars which automatically cascade from scope to global
59
- const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi;
60
-
59
+ const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
60
+
61
61
  // Get simplified setting - from params only (per-call override) or use default
62
- context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified;
63
-
62
+ context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
63
+
64
64
  // Special case: if no inputRecord provided, force simplified mode
65
65
  if (!params.inputRecord && context.simplified === false) {
66
- context.simplified = true;
66
+ context.simplified = true
67
67
  }
68
68
 
69
69
  // Params is totally bypassed in simplified mode
70
70
  if (context.simplified) {
71
71
  if (params.inputRecord) {
72
- context.inputRecord = params.inputRecord;
73
- context.params = params;
72
+ context.inputRecord = params.inputRecord
73
+ context.params = params
74
74
  } else {
75
- context.inputRecord = params;
75
+ context.inputRecord = params
76
76
  // Preserve returnFullRecord if specified in params
77
- context.params = params.returnFullRecord ? { returnFullRecord: params.returnFullRecord } : {};
77
+ context.params = params.returnFullRecord ? { returnFullRecord: params.returnFullRecord } : {}
78
78
  }
79
79
  } else {
80
- context.inputRecord = params.inputRecord;
81
- context.params = params;
80
+ context.inputRecord = params.inputRecord
81
+ context.params = params
82
82
  }
83
83
 
84
84
  // Assign common context properties
85
- context.schemaInfo = scopes[scopeName].vars.schemaInfo;
86
-
85
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo
86
+
87
87
  // Use vars which automatically cascade from scope to global
88
- const defaultReturnFullRecord = isTransport ? vars.returnRecordTransport : vars.returnRecordApi;
89
-
88
+ const defaultReturnFullRecord = isTransport ? vars.returnRecordTransport : vars.returnRecordApi
89
+
90
90
  // Get return record setting - from params only (per-call override) or use default
91
- const returnFullRecordRaw = context.params.returnFullRecord !== undefined
92
- ? context.params.returnFullRecord
93
- : defaultReturnFullRecord;
94
-
91
+ const returnFullRecordRaw = context.params.returnFullRecord !== undefined
92
+ ? context.params.returnFullRecord
93
+ : defaultReturnFullRecord
94
+
95
95
  // Normalize return record setting to always be an object with method keys
96
96
  if (typeof returnFullRecordRaw === 'object' && returnFullRecordRaw !== null) {
97
97
  // It's already an object, normalize the values
@@ -99,47 +99,47 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
99
99
  post: normalizeReturnValue(returnFullRecordRaw.post),
100
100
  put: normalizeReturnValue(returnFullRecordRaw.put),
101
101
  patch: normalizeReturnValue(returnFullRecordRaw.patch)
102
- };
102
+ }
103
103
  } else {
104
104
  // It's a single value (string or boolean), apply to all methods
105
- const normalized = normalizeReturnValue(returnFullRecordRaw);
105
+ const normalized = normalizeReturnValue(returnFullRecordRaw)
106
106
  context.returnRecordSetting = {
107
107
  post: normalized,
108
108
  put: normalized,
109
109
  patch: normalized
110
- };
110
+ }
111
111
  }
112
-
112
+
113
113
  // Helper function to normalize return values (same as above)
114
- function normalizeReturnValue(value) {
115
- if (['no', 'minimal', 'full'].includes(value)) return value;
116
- return 'no'; // default
114
+ function normalizeReturnValue (value) {
115
+ if (['no', 'minimal', 'full'].includes(value)) return value
116
+ return 'no' // default
117
117
  }
118
118
 
119
119
  // These only make sense as parameter per query, not in vars etc.
120
- context.queryParams = params.queryParams || {};
121
- context.queryParams.fields = cascadeConfig('fields', [context.queryParams], {});
122
- context.queryParams.include = cascadeConfig('include', [context.queryParams], []);
120
+ context.queryParams = params.queryParams || {}
121
+ context.queryParams.fields = cascadeConfig('fields', [context.queryParams], {})
122
+ context.queryParams.include = cascadeConfig('include', [context.queryParams], [])
123
123
 
124
- context.scopeName = scopeName;
124
+ context.scopeName = scopeName
125
125
 
126
126
  // Transaction handling
127
- context.transaction = params.transaction ||
128
- (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
129
- context.shouldCommit = !params.transaction && !!context.transaction;
130
- context.db = context.transaction || api.knex.instance;
127
+ context.transaction = params.transaction ||
128
+ (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
129
+ context.shouldCommit = !params.transaction && !!context.transaction
130
+ context.db = context.transaction || api.knex.instance
131
131
 
132
132
  // These are just shortcuts used in this function and will be returned
133
- const schema = context.schemaInfo.schemaInstance;
134
- const schemaStructure = context.schemaInfo.schemaInstance.structure;
135
- const schemaRelationships = context.schemaInfo.schemaRelationships;
133
+ const schema = context.schemaInfo.schemaInstance
134
+ const schemaStructure = context.schemaInfo.schemaInstance.structure
135
+ const schemaRelationships = context.schemaInfo.schemaRelationships
136
136
 
137
137
  // Transform input if in simplified mode
138
138
  if (context.simplified) {
139
139
  context.inputRecord = transformSimplifiedToJsonApi(
140
140
  { inputRecord: context.inputRecord },
141
141
  { context: { scopeName, schemaStructure, schemaRelationships } }
142
- );
142
+ )
143
143
  } else {
144
144
  // Strict mode: validate no belongsTo fields in attributes
145
145
  if (context.inputRecord?.data?.attributes) {
@@ -148,7 +148,7 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
148
148
  throw new RestApiValidationError(
149
149
  `Field '${key}' is a foreign key and must be set via relationships, not attributes`,
150
150
  { fields: [`data.attributes.${key}`] }
151
- );
151
+ )
152
152
  }
153
153
  }
154
154
  }
@@ -157,15 +157,15 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
157
157
  if (context.inputRecord.data.type !== scopeName) {
158
158
  throw new RestApiValidationError(
159
159
  `Resource type mismatch. Expected '${scopeName}' but got '${context.inputRecord.data.type}'`,
160
- {
161
- fields: ['data.type'],
162
- violations: [{
163
- field: 'data.type',
164
- rule: 'resource_type_match',
165
- message: `Resource type must be '${scopeName}'`
166
- }]
160
+ {
161
+ fields: ['data.type'],
162
+ violations: [{
163
+ field: 'data.type',
164
+ rule: 'resource_type_match',
165
+ message: `Resource type must be '${scopeName}'`
166
+ }]
167
167
  }
168
- );
168
+ )
169
169
  }
170
170
 
171
171
  // Remove included validation since JSON:API doesn't support it
@@ -173,7 +173,7 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
173
173
  throw new RestApiPayloadError(
174
174
  context.method + ' requests cannot include an "included" array. JSON:API does not support creating multiple resources in a single request.',
175
175
  { path: 'included', expected: 'undefined', received: 'array' }
176
- );
176
+ )
177
177
  }
178
178
 
179
179
  // If both URL path ID and request body ID are provided, they must match
@@ -181,24 +181,24 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
181
181
  if (context.id && context.inputRecord.data.id && String(context.id) !== String(context.inputRecord.data.id)) {
182
182
  throw new RestApiValidationError(
183
183
  `ID mismatch. URL path ID '${context.id}' does not match request body ID '${context.inputRecord.data.id}'`,
184
- {
185
- fields: ['data.id'],
186
- violations: [{
187
- field: 'data.id',
188
- rule: 'id_consistency',
189
- message: `Request body ID must match URL path ID when both are provided`
190
- }]
184
+ {
185
+ fields: ['data.id'],
186
+ violations: [{
187
+ field: 'data.id',
188
+ rule: 'id_consistency',
189
+ message: 'Request body ID must match URL path ID when both are provided'
190
+ }]
191
191
  }
192
- );
192
+ )
193
193
  }
194
194
 
195
195
  // Return key schema-related objects for direct use in the main methods
196
- return { schema, schemaStructure, schemaRelationships };
196
+ return { schema, schemaStructure, schemaRelationships }
197
197
  }
198
198
 
199
199
  /**
200
200
  * Handles error cleanup and logging for write methods (POST, PUT, PATCH)
201
- *
201
+ *
202
202
  * @param {Error} error - The error that was caught
203
203
  * @param {Object} context - The request context
204
204
  * @param {string} method - The HTTP method name (POST, PUT, PATCH)
@@ -210,26 +210,26 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
210
210
  export const handleWriteMethodError = async (error, context, method, scopeName, log, runHooks) => {
211
211
  // Rollback transaction if we created it
212
212
  if (context.shouldCommit) {
213
- await context.transaction.rollback();
214
- await runHooks('afterRollback');
213
+ await context.transaction.rollback()
214
+ await runHooks('afterRollback')
215
215
  }
216
-
216
+
217
217
  // Create enhanced logger for error logging
218
- const enhancedLog = getEnhancedLogger(log);
219
-
218
+ const enhancedLog = getEnhancedLogger(log)
219
+
220
220
  // Log the full error details
221
221
  enhancedLog.logError(`Error in ${method} method`, error, {
222
222
  scopeName,
223
223
  method: method.toLowerCase(),
224
224
  inputRecord: context.inputRecord
225
- });
226
-
227
- throw error;
225
+ })
226
+
227
+ throw error
228
228
  }
229
229
 
230
230
  /**
231
231
  * Validates that a pivot resource exists for many-to-many relationships
232
- *
232
+ *
233
233
  * @param {Object} scopes - All available scopes/resources
234
234
  * @param {Object} relDef - The relationship definition
235
235
  * @param {string} relName - The relationship name
@@ -239,7 +239,7 @@ export const validatePivotResource = (scopes, relDef, relName) => {
239
239
  if (!scopes[relDef.through]) {
240
240
  throw new RestApiValidationError(
241
241
  `Pivot resource '${relDef.through}' not found for relationship '${relName}'`,
242
- {
242
+ {
243
243
  fields: [`relationships.${relName}`],
244
244
  violations: [{
245
245
  field: `relationships.${relName}`,
@@ -247,23 +247,23 @@ export const validatePivotResource = (scopes, relDef, relName) => {
247
247
  message: `Pivot resource '${relDef.through}' must be defined`
248
248
  }]
249
249
  }
250
- );
250
+ )
251
251
  }
252
252
  }
253
253
 
254
254
  /**
255
255
  * Gets the appropriate hook suffix based on HTTP method
256
- *
256
+ *
257
257
  * @param {string} method - The HTTP method (e.g., 'post', 'get')
258
258
  * @returns {string} The capitalized method name for hook naming
259
259
  */
260
260
  export const getMethodHookSuffix = (method) => {
261
- return method.charAt(0).toUpperCase() + method.slice(1);
261
+ return method.charAt(0).toUpperCase() + method.slice(1)
262
262
  }
263
263
 
264
264
  /**
265
265
  * Validates resource attributes before write operations
266
- *
266
+ *
267
267
  * @param {Object} params - Validation parameters
268
268
  * @param {Object} params.context - The request context
269
269
  * @param {Object} params.schema - The resource schema
@@ -272,147 +272,147 @@ export const getMethodHookSuffix = (method) => {
272
272
  * @param {boolean} params.isPartialValidation - Whether this is partial validation (for PATCH)
273
273
  * @throws {RestApiValidationError} If validation fails
274
274
  */
275
- export const validateResourceAttributesBeforeWrite = async ({
276
- context,
277
- schema,
278
- belongsToUpdates,
279
- runHooks,
275
+ export const validateResourceAttributesBeforeWrite = async ({
276
+ context,
277
+ schema,
278
+ belongsToUpdates,
279
+ runHooks,
280
280
  isPartialValidation = false
281
281
  }) => {
282
282
  // Dynamically get the method suffix
283
- const methodSpecificHookSuffix = getMethodHookSuffix(context.method);
283
+ const methodSpecificHookSuffix = getMethodHookSuffix(context.method)
284
284
 
285
- await runHooks('beforeSchemaValidate');
286
- await runHooks(`beforeSchemaValidate${methodSpecificHookSuffix}`);
285
+ await runHooks('beforeSchemaValidate')
286
+ await runHooks(`beforeSchemaValidate${methodSpecificHookSuffix}`)
287
287
 
288
288
  // Store original input attributes before validation adds defaults (primarily for POST)
289
289
  // Only if it's not already set and the current method is POST
290
290
  if (!context.originalInputAttributes && context.method === 'post') {
291
- context.originalInputAttributes = { ...(context.inputRecord.data.attributes || {}) };
291
+ context.originalInputAttributes = { ...(context.inputRecord.data.attributes || {}) }
292
292
  }
293
-
293
+
294
294
  // Merge belongsTo updates with attributes for validation
295
295
  const attributesToValidate = {
296
- ...context.inputRecord.data.attributes,
297
- ...belongsToUpdates
298
- };
299
-
296
+ ...context.inputRecord.data.attributes,
297
+ ...belongsToUpdates
298
+ }
299
+
300
300
  // Extract computed fields and foreign key fields before validation
301
- const computedFields = {};
302
- const foreignKeyFields = {};
303
- const schemaStructure = context.schemaInfo.schemaStructure || {};
301
+ const computedFields = {}
302
+ const foreignKeyFields = {}
303
+ const schemaStructure = context.schemaInfo.schemaStructure || {}
304
304
  Object.entries(attributesToValidate).forEach(([key, value]) => {
305
- const fieldDef = schemaStructure[key];
305
+ const fieldDef = schemaStructure[key]
306
306
  // Computed fields should be stripped from input
307
307
  if (fieldDef && fieldDef.computed === true && value !== undefined) {
308
- computedFields[key] = value;
308
+ computedFields[key] = value
309
309
  }
310
310
  // Foreign key fields (belongsTo with 'as' property) should NOT be in attributes
311
311
  // They should only come through relationships or belongsToUpdates
312
312
  // Check if key exists in belongsToUpdates (not just truthy value)
313
313
  if (fieldDef && fieldDef.belongsTo && fieldDef.as && value !== undefined && !(key in belongsToUpdates)) {
314
- foreignKeyFields[key] = value;
314
+ foreignKeyFields[key] = value
315
315
  }
316
- });
317
-
316
+ })
317
+
318
318
  // Warn if computed fields were sent in the input
319
319
  if (Object.keys(computedFields).length > 0) {
320
- const fieldNames = Object.keys(computedFields).join(', ');
321
- console.warn(`Computed fields [${fieldNames}] were sent in input for resource '${context.scopeName}' but will be ignored as they are output-only`);
320
+ const fieldNames = Object.keys(computedFields).join(', ')
321
+ console.warn(`Computed fields [${fieldNames}] were sent in input for resource '${context.scopeName}' but will be ignored as they are output-only`)
322
322
  }
323
-
323
+
324
324
  // Reject if foreign key fields were sent directly in attributes
325
325
  if (Object.keys(foreignKeyFields).length > 0) {
326
326
  const violations = Object.keys(foreignKeyFields).map(field => {
327
- const fieldDef = schemaStructure[field];
327
+ const fieldDef = schemaStructure[field]
328
328
  return {
329
329
  field: `data.attributes.${field}`,
330
330
  rule: 'foreign_key_in_attributes',
331
331
  message: `Foreign key field '${field}' should not be in attributes. Use 'data.relationships.${fieldDef.as}' instead.`
332
- };
333
- });
334
-
332
+ }
333
+ })
334
+
335
335
  throw new RestApiValidationError(
336
336
  'Foreign key fields cannot be set directly in attributes',
337
- {
337
+ {
338
338
  fields: violations.map(v => v.field),
339
339
  violations
340
340
  }
341
- );
341
+ )
342
342
  }
343
-
343
+
344
344
  // Filter out computed fields and foreign key fields from validation
345
345
  // Virtual fields MUST go through validation
346
346
  const attributesForValidation = Object.entries(attributesToValidate)
347
347
  .filter(([key, _]) => {
348
- const fieldDef = schemaStructure[key];
348
+ const fieldDef = schemaStructure[key]
349
349
  // Exclude computed fields
350
350
  if (fieldDef && fieldDef.computed === true) {
351
- return false;
351
+ return false
352
352
  }
353
353
  // Exclude foreign key fields that weren't provided via belongsToUpdates
354
354
  if (fieldDef && fieldDef.belongsTo && fieldDef.as && !belongsToUpdates[key]) {
355
- return false;
355
+ return false
356
356
  }
357
- return true;
357
+ return true
358
358
  })
359
359
  .reduce((acc, [key, value]) => {
360
- acc[key] = value;
361
- return acc;
362
- }, {});
360
+ acc[key] = value
361
+ return acc
362
+ }, {})
363
363
 
364
- const validationOptions = isPartialValidation ? { onlyObjectValues: true } : {};
364
+ const validationOptions = isPartialValidation ? { onlyObjectValues: true } : {}
365
365
 
366
- const { validatedObject, errors } = await schema.validate(attributesForValidation, validationOptions);
366
+ const { validatedObject, errors } = await schema.validate(attributesForValidation, validationOptions)
367
367
 
368
368
  if (Object.keys(errors).length > 0) {
369
369
  // --- START OF MODIFICATION ---
370
- const schemaStructure = context.schemaInfo.schemaInstance.structure; // Get the schema structure for lookup
370
+ const schemaStructure = context.schemaInfo.schemaInstance.structure // Get the schema structure for lookup
371
371
 
372
372
  const violations = Object.entries(errors).map(([field, error]) => {
373
- let fieldPath = `data.attributes.${field}`; // Default path for attributes
373
+ let fieldPath = `data.attributes.${field}` // Default path for attributes
374
374
 
375
- // Check if this field is a foreign key that has an 'as' alias
376
- const fieldDef = schemaStructure[field];
377
- if (fieldDef && fieldDef.belongsTo && fieldDef.as) {
375
+ // Check if this field is a foreign key that has an 'as' alias
376
+ const fieldDef = schemaStructure[field]
377
+ if (fieldDef && fieldDef.belongsTo && fieldDef.as) {
378
378
  // If it's a belongsTo field with an alias, rewrite the path to the relationship alias
379
- fieldPath = `data.relationships.${fieldDef.as}.data.id`;
380
- }
381
- // For many-to-many relationships, the original `transformSimplifiedToJsonApi`
382
- // already puts them under `relationships.relName.data`, so `field` here
383
- // would already be the relationship name, not a foreign key.
384
- // However, if a validation error somehow slips through for a pivot table field
385
- // that doesn't have an 'as' alias but is a foreign key, you might need
386
- // more sophisticated mapping. For now, this covers belongsTo.
387
-
388
- return {
389
- field: fieldPath,
390
- rule: error.code || 'invalid_value',
391
- message: error.message
392
- };
393
- });
394
- // --- END OF MODIFICATION ---
395
-
396
- throw new RestApiValidationError(
397
- 'Schema validation failed for resource attributes',
398
- {
399
- fields: violations.map(v => v.field), // Use the potentially rewritten fields
400
- violations
401
- }
402
- );
379
+ fieldPath = `data.relationships.${fieldDef.as}.data.id`
380
+ }
381
+ // For many-to-many relationships, the original `transformSimplifiedToJsonApi`
382
+ // already puts them under `relationships.relName.data`, so `field` here
383
+ // would already be the relationship name, not a foreign key.
384
+ // However, if a validation error somehow slips through for a pivot table field
385
+ // that doesn't have an 'as' alias but is a foreign key, you might need
386
+ // more sophisticated mapping. For now, this covers belongsTo.
387
+
388
+ return {
389
+ field: fieldPath,
390
+ rule: error.code || 'invalid_value',
391
+ message: error.message
392
+ }
393
+ })
394
+ // --- END OF MODIFICATION ---
395
+
396
+ throw new RestApiValidationError(
397
+ 'Schema validation failed for resource attributes',
398
+ {
399
+ fields: violations.map(v => v.field), // Use the potentially rewritten fields
400
+ violations
401
+ }
402
+ )
403
403
  }
404
-
404
+
405
405
  // Update attributes with validated values
406
406
  // Virtual fields have now been validated and cast properly
407
- context.inputRecord.data.attributes = validatedObject;
407
+ context.inputRecord.data.attributes = validatedObject
408
408
 
409
- await runHooks(`afterSchemaValidate${methodSpecificHookSuffix}`);
410
- await runHooks('afterSchemaValidate');
409
+ await runHooks(`afterSchemaValidate${methodSpecificHookSuffix}`)
410
+ await runHooks('afterSchemaValidate')
411
411
  }
412
412
 
413
413
  /**
414
414
  * Validates that the user has access to all resources referenced in relationships
415
- *
415
+ *
416
416
  * @param {object} context - The context object containing authentication info
417
417
  * @param {object} inputRecord - The input record containing relationships to validate
418
418
  * @param {object} helpers - Data helpers including dataGetMinimal
@@ -421,21 +421,21 @@ export const validateResourceAttributesBeforeWrite = async ({
421
421
  * @throws {Error} If user doesn't have access to any related resource
422
422
  */
423
423
  export const validateRelationshipAccess = async (context, inputRecord, helpers, runHooks, api) => {
424
- if (!inputRecord?.data?.relationships) return;
425
-
424
+ if (!inputRecord?.data?.relationships) return
425
+
426
426
  for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
427
- if (!relData?.data) continue;
428
-
427
+ if (!relData?.data) continue
428
+
429
429
  // Handle both single and array relationships
430
- const relatedItems = Array.isArray(relData.data) ? relData.data : [relData.data];
431
-
430
+ const relatedItems = Array.isArray(relData.data) ? relData.data : [relData.data]
431
+
432
432
  for (const item of relatedItems) {
433
433
  // Get the scope for the related resource
434
- const relatedScope = api.resources[item.type];
434
+ const relatedScope = api.resources[item.type]
435
435
  if (!relatedScope) {
436
- throw new Error(`Unknown resource type: ${item.type}`);
436
+ throw new Error(`Unknown resource type: ${item.type}`)
437
437
  }
438
-
438
+
439
439
  // Create context for dataGetMinimal
440
440
  const getContext = {
441
441
  ...context,
@@ -444,20 +444,19 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
444
444
  scopeName: item.type,
445
445
  method: 'get', // We're checking read permission
446
446
  isUpdate: false
447
- };
447
+ }
448
448
 
449
-
450
449
  // Get the minimal record
451
- const record = await helpers.dataGetMinimal({
452
- scopeName: item.type,
450
+ const record = await helpers.dataGetMinimal({
451
+ scopeName: item.type,
453
452
  context: getContext,
454
- runHooks
455
- });
456
-
453
+ runHooks
454
+ })
455
+
457
456
  if (!record) {
458
- throw new Error(`Cannot create relationship to non-existent ${item.type} with id ${item.id}`);
457
+ throw new Error(`Cannot create relationship to non-existent ${item.type} with id ${item.id}`)
459
458
  }
460
-
459
+
461
460
  // Check permissions using the related scope's checkPermissions
462
461
  await relatedScope.checkPermissions({
463
462
  method: 'get',
@@ -466,7 +465,7 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
466
465
  id: item.id,
467
466
  minimalRecord: record
468
467
  }
469
- });
468
+ })
470
469
  }
471
470
  }
472
471
  }
@@ -474,7 +473,7 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
474
473
  /**
475
474
  * Applies field setters to transform attribute values before storage
476
475
  * Setters are applied in dependency order to handle interdependent fields
477
- *
476
+ *
478
477
  * @param {object} attributes - The validated attributes to transform
479
478
  * @param {object} schemaInfo - Schema information including fieldSetters
480
479
  * @param {object} context - The request context
@@ -483,20 +482,20 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
483
482
  * @returns {object} Transformed attributes ready for storage
484
483
  */
485
484
  export const applyFieldSetters = async (attributes, schemaInfo, context, api, helpers) => {
486
- const fieldSetters = schemaInfo.fieldSetters || {};
487
- const sortedSetterFields = schemaInfo.sortedSetterFields || [];
488
-
485
+ const fieldSetters = schemaInfo.fieldSetters || {}
486
+ const sortedSetterFields = schemaInfo.sortedSetterFields || []
487
+
489
488
  // No setters to apply
490
489
  if (sortedSetterFields.length === 0) {
491
- return attributes;
490
+ return attributes
492
491
  }
493
-
494
- let transformedAttributes = { ...attributes };
495
-
492
+
493
+ const transformedAttributes = { ...attributes }
494
+
496
495
  for (const fieldName of sortedSetterFields) {
497
496
  // Only process fields that exist in the attributes
498
497
  if (fieldName in transformedAttributes) {
499
- const setterInfo = fieldSetters[fieldName];
498
+ const setterInfo = fieldSetters[fieldName]
500
499
  try {
501
500
  const setterContext = {
502
501
  attributes: transformedAttributes, // Current state with previous setters applied
@@ -508,134 +507,133 @@ export const applyFieldSetters = async (attributes, schemaInfo, context, api, he
508
507
  api,
509
508
  helpers,
510
509
  auth: context.auth
511
- };
510
+ }
512
511
  transformedAttributes[fieldName] = await setterInfo.setter(
513
512
  transformedAttributes[fieldName],
514
513
  setterContext
515
- );
514
+ )
516
515
  } catch (error) {
517
- console.warn(`Setter for field '${fieldName}' failed:`, error);
516
+ console.warn(`Setter for field '${fieldName}' failed:`, error)
518
517
  // Keep original value if setter fails
519
518
  }
520
519
  }
521
520
  }
522
-
523
- return transformedAttributes;
521
+
522
+ return transformedAttributes
524
523
  }
525
524
 
526
- export async function handleRecordReturnAfterWrite({
527
- context,
528
- scopeName,
529
- api,
530
- scopes,
531
- schemaStructure,
532
- schemaRelationships,
533
- scopeOptions,
534
- vars,
535
- runHooks,
536
- helpers,
537
- log
525
+ export async function handleRecordReturnAfterWrite ({
526
+ context,
527
+ scopeName,
528
+ api,
529
+ scopes,
530
+ schemaStructure,
531
+ schemaRelationships,
532
+ scopeOptions,
533
+ vars,
534
+ runHooks,
535
+ helpers,
536
+ log
538
537
  }) {
539
- // Create enhanced logger for warnings
540
- const enhancedLog = getEnhancedLogger(log);
541
- const methodSpecificHookSuffix = getMethodHookSuffix(context.method);
542
-
543
- // Step 1: Set up record state for hooks
544
- // Handle originalMinimalRecord and minimalRecord based on method type
545
- if (context.method === 'DELETE') {
546
- // For DELETE, keep the deleted record reference
547
- if (context.minimalRecord) {
548
- context.originalMinimalRecord = context.minimalRecord;
549
- }
550
- } else {
551
- // For POST, PUT, PATCH - save the original state if it exists
552
- if (context.minimalRecord) {
553
- context.originalMinimalRecord = context.minimalRecord;
554
- }
555
-
556
- // Fetch the current state of the record after the write operation
557
- try {
558
- const currentRecord = await helpers.dataGetMinimal({
559
- scopeName,
560
- context,
561
- runHooks
562
- });
563
- context.minimalRecord = currentRecord;
564
- } catch (error) {
565
- enhancedLog.warn(`Could not fetch minimal record after ${context.method} operation`, { error, id: context.id });
566
- }
538
+ // Create enhanced logger for warnings
539
+ const enhancedLog = getEnhancedLogger(log)
540
+ const methodSpecificHookSuffix = getMethodHookSuffix(context.method)
541
+
542
+ // Step 1: Set up record state for hooks
543
+ // Handle originalMinimalRecord and minimalRecord based on method type
544
+ if (context.method === 'DELETE') {
545
+ // For DELETE, keep the deleted record reference
546
+ if (context.minimalRecord) {
547
+ context.originalMinimalRecord = context.minimalRecord
548
+ }
549
+ } else {
550
+ // For POST, PUT, PATCH - save the original state if it exists
551
+ if (context.minimalRecord) {
552
+ context.originalMinimalRecord = context.minimalRecord
567
553
  }
568
554
 
569
- // Step 2: Determine what to return based on configuration
570
- const returnMode = context.returnRecordSetting[context.method];
571
-
572
- // Case 1: Return nothing (204 No Content)
573
- if (returnMode === 'no') {
574
- context.responseRecord = undefined;
575
- await runHooks('finish');
576
- await runHooks(`finish${methodSpecificHookSuffix}`);
577
- return undefined;
555
+ // Fetch the current state of the record after the write operation
556
+ try {
557
+ const currentRecord = await helpers.dataGetMinimal({
558
+ scopeName,
559
+ context,
560
+ runHooks
561
+ })
562
+ context.minimalRecord = currentRecord
563
+ } catch (error) {
564
+ enhancedLog.warn(`Could not fetch minimal record after ${context.method} operation`, { error, id: context.id })
578
565
  }
579
-
580
- // Case 2: Return minimal record (just type and id)
581
- if (returnMode === 'minimal') {
582
- if (context.simplified) {
583
- context.responseRecord = {
584
- id: String(context.id),
585
- type: scopeName
586
- };
587
- } else {
588
- context.responseRecord = {
589
- data: {
590
- type: scopeName,
591
- id: String(context.id)
592
- }
593
- };
566
+ }
567
+
568
+ // Step 2: Determine what to return based on configuration
569
+ const returnMode = context.returnRecordSetting[context.method]
570
+
571
+ // Case 1: Return nothing (204 No Content)
572
+ if (returnMode === 'no') {
573
+ context.responseRecord = undefined
574
+ await runHooks('finish')
575
+ await runHooks(`finish${methodSpecificHookSuffix}`)
576
+ return undefined
577
+ }
578
+
579
+ // Case 2: Return minimal record (just type and id)
580
+ if (returnMode === 'minimal') {
581
+ if (context.simplified) {
582
+ context.responseRecord = {
583
+ id: String(context.id),
584
+ type: scopeName
585
+ }
586
+ } else {
587
+ context.responseRecord = {
588
+ data: {
589
+ type: scopeName,
590
+ id: String(context.id)
594
591
  }
595
- await runHooks('finish');
596
- await runHooks(`finish${methodSpecificHookSuffix}`);
597
- return context.responseRecord;
598
- }
599
-
600
- // Case 3: Return full record
601
- if (returnMode === 'full') {
602
- // Fetch the complete record using the GET method
603
- const fullRecord = await api.resources[scopeName].get({
604
- id: context.id,
605
- queryParams: context.queryParams,
606
- transaction: context.transaction,
607
- simplified: context.simplified
608
- }, {...context });
609
-
610
- context.responseRecord = fullRecord || undefined;
611
-
612
- // Run finish hooks
613
- await runHooks('finish');
614
- await runHooks(`finish${methodSpecificHookSuffix}`);
615
-
616
- // No transformation needed - GET already returns the correct format
617
- // based on context.simplified
618
- return context.responseRecord;
592
+ }
619
593
  }
620
-
621
- // This should never be reached, but just in case
622
- throw new Error(`Invalid returnMode: ${returnMode}`);
594
+ await runHooks('finish')
595
+ await runHooks(`finish${methodSpecificHookSuffix}`)
596
+ return context.responseRecord
597
+ }
598
+
599
+ // Case 3: Return full record
600
+ if (returnMode === 'full') {
601
+ // Fetch the complete record using the GET method
602
+ const fullRecord = await api.resources[scopeName].get({
603
+ id: context.id,
604
+ queryParams: context.queryParams,
605
+ transaction: context.transaction,
606
+ simplified: context.simplified
607
+ }, { ...context })
608
+
609
+ context.responseRecord = fullRecord || undefined
610
+
611
+ // Run finish hooks
612
+ await runHooks('finish')
613
+ await runHooks(`finish${methodSpecificHookSuffix}`)
614
+
615
+ // No transformation needed - GET already returns the correct format
616
+ // based on context.simplified
617
+ return context.responseRecord
618
+ }
619
+
620
+ // This should never be reached, but just in case
621
+ throw new Error(`Invalid returnMode: ${returnMode}`)
623
622
  }
624
623
 
625
624
  export const findRelationshipDefinition = (schemaInfo, relationshipName) => {
626
625
  // First check schemaRelationships (for relationships defined in relationships object)
627
- const relDef = schemaInfo.schemaRelationships?.[relationshipName];
626
+ const relDef = schemaInfo.schemaRelationships?.[relationshipName]
628
627
  if (relDef) {
629
- return relDef;
628
+ return relDef
630
629
  }
631
-
630
+
632
631
  // Then check schema fields for belongsTo relationships with matching 'as' property
633
632
  for (const [fieldName, fieldDef] of Object.entries(schemaInfo.schemaStructure || {})) {
634
633
  if (fieldDef.as === relationshipName && fieldDef.belongsTo) {
635
- return fieldDef;
634
+ return fieldDef
636
635
  }
637
636
  }
638
-
639
- return null;
640
- };
641
637
 
638
+ return null
639
+ }