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,62 +1,62 @@
1
- import { RestApiValidationError, RestApiPayloadError } from '../../../../lib/rest-api-errors.js';
1
+ import { RestApiValidationError, RestApiPayloadError } from '../../../../lib/rest-api-errors.js'
2
2
 
3
3
  /**
4
4
  * Validates that include paths don't exceed maximum depth
5
- *
5
+ *
6
6
  * @param {string[]} includes - Array of include paths to validate
7
7
  * @param {number} maxDepth - Maximum allowed depth
8
8
  * @returns {void}
9
9
  * @throws {RestApiValidationError} If depth is exceeded
10
- *
10
+ *
11
11
  * @example
12
12
  * // Input: Valid include paths
13
13
  * validateIncludeDepth(['author', 'comments.author'], 3);
14
14
  * // Output: Validation succeeds (no error thrown)
15
- *
15
+ *
16
16
  * @example
17
17
  * // Input: Path exceeding depth limit
18
18
  * validateIncludeDepth(['author.company.employees.manager'], 3);
19
19
  * // Throws: RestApiValidationError
20
20
  * // "Include path 'author.company.employees.manager' exceeds maximum depth of 3"
21
- *
21
+ *
22
22
  * @private
23
23
  */
24
- function validateIncludeDepth(includes, maxDepth) {
24
+ function validateIncludeDepth (includes, maxDepth) {
25
25
  if (!includes || !Array.isArray(includes)) {
26
- return;
26
+ return
27
27
  }
28
-
28
+
29
29
  for (const includePath of includes) {
30
30
  if (typeof includePath !== 'string') {
31
- continue; // Let other validators handle type errors
31
+ continue // Let other validators handle type errors
32
32
  }
33
-
34
- const depth = includePath.split('.').length;
33
+
34
+ const depth = includePath.split('.').length
35
35
  if (depth > maxDepth) {
36
36
  throw new RestApiValidationError(
37
37
  `Include path '${includePath}' exceeds maximum depth of ${maxDepth}`,
38
- {
39
- fields: ['include'],
40
- violations: [{
41
- field: 'include',
42
- rule: 'max_depth',
43
- message: `Path '${includePath}' has depth ${depth}, maximum allowed is ${maxDepth}`
44
- }]
38
+ {
39
+ fields: ['include'],
40
+ violations: [{
41
+ field: 'include',
42
+ rule: 'max_depth',
43
+ message: `Path '${includePath}' has depth ${depth}, maximum allowed is ${maxDepth}`
44
+ }]
45
45
  }
46
- );
46
+ )
47
47
  }
48
48
  }
49
49
  }
50
50
 
51
51
  /**
52
52
  * Validates a JSON:API resource identifier object
53
- *
54
- * @param {Object} identifier - Resource identifier to validate
53
+ *
54
+ * @param {Object} identifier - Resource identifier to validate
55
55
  * @param {string} context - Context for error messages
56
56
  * @param {Object} scopes - Scopes proxy to verify resource types
57
57
  * @returns {boolean} True if valid
58
58
  * @throws {RestApiPayloadError|RestApiValidationError} If validation fails
59
- *
59
+ *
60
60
  * @example
61
61
  * // Input: Valid identifier
62
62
  * validateResourceIdentifier(
@@ -65,8 +65,8 @@ function validateIncludeDepth(includes, maxDepth) {
65
65
  * scopes
66
66
  * );
67
67
  * // Output: true
68
- *
69
- * @example
68
+ *
69
+ * @example
70
70
  * // Input: Missing type field
71
71
  * validateResourceIdentifier(
72
72
  * { id: '123' },
@@ -75,7 +75,7 @@ function validateIncludeDepth(includes, maxDepth) {
75
75
  * );
76
76
  * // Throws: RestApiPayloadError
77
77
  * // "Relationship 'comments': Resource identifier must have a non-empty 'type' string"
78
- *
78
+ *
79
79
  * @example
80
80
  * // Input: Unknown resource type
81
81
  * validateResourceIdentifier(
@@ -85,70 +85,70 @@ function validateIncludeDepth(includes, maxDepth) {
85
85
  * );
86
86
  * // Throws: RestApiValidationError
87
87
  * // "Included resource: Unknown resource type 'unicorns'. No scope with this name exists."
88
- *
88
+ *
89
89
  * @description
90
90
  * Used by:
91
91
  * - validateRelationship to check relationship data
92
92
  * - validatePostPayload to validate included resources
93
93
  * - Internal validation of resource references
94
- *
94
+ *
95
95
  * Purpose:
96
96
  * - Ensures JSON:API compliance for resource identifiers
97
97
  * - Validates resource types exist in the system
98
98
  * - Provides clear error context for debugging
99
- *
99
+ *
100
100
  * @private
101
101
  */
102
- function validateResourceIdentifier(identifier, context, scopes = null) {
102
+ function validateResourceIdentifier (identifier, context, scopes = null) {
103
103
  if (!identifier || typeof identifier !== 'object') {
104
104
  throw new RestApiPayloadError(
105
105
  `${context}: Resource identifier must be an object`,
106
106
  { path: context, expected: 'object', received: typeof identifier }
107
- );
107
+ )
108
108
  }
109
-
109
+
110
110
  if (typeof identifier.type !== 'string' || !identifier.type) {
111
111
  throw new RestApiPayloadError(
112
112
  `${context}: Resource identifier must have a non-empty 'type' string`,
113
113
  { path: `${context}.type`, expected: 'non-empty string', received: identifier.type }
114
- );
114
+ )
115
115
  }
116
-
116
+
117
117
  // Check if type is valid by checking if scope exists
118
118
  if (scopes && !scopes[identifier.type]) {
119
119
  throw new RestApiValidationError(
120
120
  `${context}: Unknown resource type '${identifier.type}'. No scope with this name exists.`,
121
- {
121
+ {
122
122
  fields: [`${context}.type`],
123
123
  violations: [{ field: `${context}.type`, rule: 'valid_resource_type', message: `Resource type '${identifier.type}' does not exist` }]
124
124
  }
125
- );
125
+ )
126
126
  }
127
-
127
+
128
128
  if (!('id' in identifier)) {
129
129
  throw new RestApiPayloadError(
130
130
  `${context}: Resource identifier must have an 'id' property`,
131
131
  { path: `${context}.id`, expected: 'property exists', received: 'missing' }
132
- );
132
+ )
133
133
  }
134
-
134
+
135
135
  if (identifier.id !== null && typeof identifier.id !== 'string' && typeof identifier.id !== 'number') {
136
136
  throw new RestApiPayloadError(
137
137
  `${context}: Resource identifier 'id' must be a string, number, or null`,
138
138
  { path: `${context}.id`, expected: 'string, number, or null', received: typeof identifier.id }
139
- );
139
+ )
140
140
  }
141
141
  }
142
142
 
143
143
  /**
144
144
  * Validates a relationship object in JSON:API format
145
- *
145
+ *
146
146
  * @param {Object} relationship - Relationship object to validate
147
147
  * @param {string} relationshipName - Name for error context
148
148
  * @param {Object} scopes - Scopes proxy to verify resource types
149
149
  * @returns {void}
150
150
  * @throws {RestApiPayloadError} If validation fails
151
- *
151
+ *
152
152
  * @example
153
153
  * // Input: To-one relationship
154
154
  * validateRelationship(
@@ -157,21 +157,21 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
157
157
  * scopes
158
158
  * );
159
159
  * // Output: Validation succeeds (no error thrown)
160
- *
160
+ *
161
161
  * @example
162
- * // Input: To-many relationship
162
+ * // Input: To-many relationship
163
163
  * validateRelationship(
164
- * {
164
+ * {
165
165
  * data: [
166
166
  * { type: 'tags', id: '1' },
167
167
  * { type: 'tags', id: '2' }
168
- * ]
168
+ * ]
169
169
  * },
170
170
  * 'tags',
171
171
  * scopes
172
172
  * );
173
173
  * // Output: Validation succeeds (no error thrown)
174
- *
174
+ *
175
175
  * @example
176
176
  * // Input: Null relationship (remove association)
177
177
  * validateRelationship(
@@ -180,7 +180,7 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
180
180
  * scopes
181
181
  * );
182
182
  * // Output: Validation succeeds (null is valid for clearing)
183
- *
183
+ *
184
184
  * @example
185
185
  * // Input: Missing data property
186
186
  * validateRelationship(
@@ -190,62 +190,62 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
190
190
  * );
191
191
  * // Throws: RestApiPayloadError
192
192
  * // "Relationship 'author' must have a 'data' property"
193
- *
193
+ *
194
194
  * @description
195
195
  * Used by:
196
196
  * - validatePostPayload for new resource relationships
197
197
  * - validatePutPayload for relationship replacement
198
198
  * - validatePatchPayload for relationship updates
199
- *
199
+ *
200
200
  * Purpose:
201
201
  * - Ensures JSON:API relationship structure
202
202
  * - Validates both to-one and to-many relationships
203
203
  * - Handles null/empty cases for clearing relationships
204
204
  * - Validates each resource identifier in arrays
205
- *
205
+ *
206
206
  * @private
207
207
  */
208
- function validateRelationship(relationship, relationshipName, scopes = null) {
208
+ function validateRelationship (relationship, relationshipName, scopes = null) {
209
209
  if (!relationship || typeof relationship !== 'object') {
210
210
  throw new RestApiPayloadError(
211
211
  `Relationship '${relationshipName}' must be an object`,
212
212
  { path: `relationships.${relationshipName}`, expected: 'object', received: typeof relationship }
213
- );
213
+ )
214
214
  }
215
-
215
+
216
216
  if (!('data' in relationship)) {
217
217
  throw new RestApiPayloadError(
218
218
  `Relationship '${relationshipName}' must have a 'data' property`,
219
219
  { path: `relationships.${relationshipName}.data`, expected: 'property exists', received: 'missing' }
220
- );
220
+ )
221
221
  }
222
-
223
- const { data } = relationship;
224
-
222
+
223
+ const { data } = relationship
224
+
225
225
  // data can be null (empty to-one relationship)
226
226
  if (data === null) {
227
- return;
227
+ return
228
228
  }
229
-
229
+
230
230
  // data can be a single resource identifier (to-one)
231
231
  if (!Array.isArray(data)) {
232
- validateResourceIdentifier(data, `Relationship '${relationshipName}'`, scopes);
233
- return;
232
+ validateResourceIdentifier(data, `Relationship '${relationshipName}'`, scopes)
233
+ return
234
234
  }
235
-
235
+
236
236
  // data can be an array of resource identifiers (to-many)
237
237
  if (data.length === 0) {
238
- return; // Empty to-many relationship is valid
238
+ return // Empty to-many relationship is valid
239
239
  }
240
-
240
+
241
241
  data.forEach((identifier, index) => {
242
- validateResourceIdentifier(identifier, `Relationship '${relationshipName}[${index}]'`, scopes);
243
- });
242
+ validateResourceIdentifier(identifier, `Relationship '${relationshipName}[${index}]'`, scopes)
243
+ })
244
244
  }
245
245
 
246
246
  /**
247
247
  * Validates query parameters for GET requests (single resource retrieval)
248
- *
248
+ *
249
249
  * @param {Object} params - Parameters containing id and queryParams
250
250
  * @param {string|number} params.id - Resource ID to fetch
251
251
  * @param {Object} [params.queryParams] - Optional query parameters
@@ -254,14 +254,14 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
254
254
  * @param {number} maxIncludeDepth - Maximum allowed include depth
255
255
  * @returns {boolean} True if valid
256
256
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
257
- *
257
+ *
258
258
  * @example
259
259
  * // Input: Basic GET request
260
260
  * validateGetPayload({
261
261
  * id: '123'
262
262
  * });
263
263
  * // Output: true
264
- *
264
+ *
265
265
  * @example
266
266
  * // Input: GET with nested includes
267
267
  * validateGetPayload({
@@ -271,7 +271,7 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
271
271
  * }
272
272
  * });
273
273
  * // Output: true (include depths are 1 and 2)
274
- *
274
+ *
275
275
  * @example
276
276
  * // Input: GET with sparse fieldsets
277
277
  * validateGetPayload({
@@ -285,7 +285,7 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
285
285
  * }
286
286
  * });
287
287
  * // Output: true
288
- *
288
+ *
289
289
  * @example
290
290
  * // Input: Missing ID parameter
291
291
  * validateGetPayload({
@@ -293,7 +293,7 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
293
293
  * });
294
294
  * // Throws: RestApiValidationError
295
295
  * // "GET request must include an id parameter"
296
- *
296
+ *
297
297
  * @example
298
298
  * // Input: Invalid field specification
299
299
  * validateGetPayload({
@@ -304,21 +304,21 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
304
304
  * }
305
305
  * }
306
306
  * });
307
- * // Throws: RestApiPayloadError
307
+ * // Throws: RestApiPayloadError
308
308
  * // "queryParams.fields['articles'] must be a comma-separated string"
309
- *
309
+ *
310
310
  * @description
311
311
  * Used by:
312
312
  * - rest-api-plugin.get() method before data fetching
313
313
  * - Validates parameters before passing to storage layer
314
- *
314
+ *
315
315
  * Purpose:
316
316
  * - Ensures required ID parameter is present and valid
317
317
  * - Validates JSON:API query parameter structure
318
318
  * - Prevents invalid queries from reaching database
319
319
  * - Enforces include depth limits for performance
320
320
  * - Provides consistent error messages across storage backends
321
- *
321
+ *
322
322
  * Data flow:
323
323
  * 1. Checks params object structure
324
324
  * 2. Validates required ID parameter
@@ -326,88 +326,88 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
326
326
  * 4. Validates include paths don't exceed depth limit
327
327
  * 5. Returns true or throws descriptive error
328
328
  */
329
- export function validateGetPayload(params, maxIncludeDepth = 3) {
329
+ export function validateGetPayload (params, maxIncludeDepth = 3) {
330
330
  if (!params || typeof params !== 'object') {
331
331
  throw new RestApiPayloadError(
332
332
  'GET parameters must be an object',
333
333
  { path: 'params', expected: 'object', received: typeof params }
334
- );
334
+ )
335
335
  }
336
-
336
+
337
337
  // Validate ID
338
338
  if (!('id' in params)) {
339
339
  throw new RestApiValidationError(
340
340
  'GET request must include an id parameter',
341
341
  { fields: ['id'], violations: [{ field: 'id', rule: 'required', message: 'ID parameter is required' }] }
342
- );
342
+ )
343
343
  }
344
-
344
+
345
345
  if (params.id === null || params.id === undefined || params.id === '') {
346
346
  throw new RestApiValidationError(
347
347
  'GET request id cannot be null, undefined, or empty',
348
348
  { fields: ['id'], violations: [{ field: 'id', rule: 'not_empty', message: 'ID cannot be null, undefined, or empty' }] }
349
- );
349
+ )
350
350
  }
351
-
351
+
352
352
  // Validate queryParams if present
353
353
  if (params.queryParams) {
354
354
  if (typeof params.queryParams !== 'object') {
355
355
  throw new RestApiPayloadError(
356
356
  'queryParams must be an object',
357
357
  { path: 'queryParams', expected: 'object', received: typeof params.queryParams }
358
- );
358
+ )
359
359
  }
360
-
361
- const { include, fields } = params.queryParams;
362
-
360
+
361
+ const { include, fields } = params.queryParams
362
+
363
363
  // Validate include
364
364
  if (include !== undefined) {
365
365
  if (!Array.isArray(include)) {
366
366
  throw new RestApiPayloadError(
367
367
  'queryParams.include must be an array of strings',
368
368
  { path: 'queryParams.include', expected: 'array', received: typeof include }
369
- );
369
+ )
370
370
  }
371
-
371
+
372
372
  include.forEach((path, index) => {
373
373
  if (typeof path !== 'string') {
374
374
  throw new RestApiPayloadError(
375
375
  `queryParams.include[${index}] must be a string`,
376
376
  { path: `queryParams.include[${index}]`, expected: 'string', received: typeof path }
377
- );
377
+ )
378
378
  }
379
- });
380
-
379
+ })
380
+
381
381
  // Validate include depth
382
- validateIncludeDepth(include, maxIncludeDepth);
382
+ validateIncludeDepth(include, maxIncludeDepth)
383
383
  }
384
-
384
+
385
385
  // Validate fields
386
386
  if (fields !== undefined) {
387
387
  if (typeof fields !== 'object' || fields === null || Array.isArray(fields)) {
388
388
  throw new RestApiPayloadError(
389
389
  'queryParams.fields must be an object',
390
390
  { path: 'queryParams.fields', expected: 'object', received: Array.isArray(fields) ? 'array' : typeof fields }
391
- );
391
+ )
392
392
  }
393
-
393
+
394
394
  Object.entries(fields).forEach(([resourceType, fieldList]) => {
395
395
  if (typeof fieldList !== 'string') {
396
396
  throw new RestApiPayloadError(
397
397
  `queryParams.fields['${resourceType}'] must be a comma-separated string`,
398
398
  { path: `queryParams.fields['${resourceType}']`, expected: 'string', received: typeof fieldList }
399
- );
399
+ )
400
400
  }
401
- });
401
+ })
402
402
  }
403
403
  }
404
-
405
- return true;
404
+
405
+ return true
406
406
  }
407
407
 
408
408
  /**
409
409
  * Validates query parameters for collection requests (query/list operations)
410
- *
410
+ *
411
411
  * @param {Object} params - Parameters object
412
412
  * @param {Object} [params.queryParams] - Query parameters for the collection
413
413
  * @param {string[]} [params.queryParams.include] - Related resources to include
@@ -419,12 +419,12 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
419
419
  * @param {number} maxIncludeDepth - Maximum allowed include depth
420
420
  * @returns {boolean} True if valid
421
421
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
422
- *
422
+ *
423
423
  * @example
424
424
  * // Input: Empty query (fetch all)
425
425
  * validateQueryPayload({}, ['title', 'createdAt']);
426
426
  * // Output: true
427
- *
427
+ *
428
428
  * @example
429
429
  * // Input: Query with filters and sorting
430
430
  * validateQueryPayload({
@@ -437,7 +437,7 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
437
437
  * }
438
438
  * }, ['publishedAt', 'title', 'createdAt']);
439
439
  * // Output: true
440
- *
440
+ *
441
441
  * @example
442
442
  * // Input: Different pagination styles
443
443
  * // Page-based:
@@ -447,7 +447,7 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
447
447
  * }
448
448
  * }, []);
449
449
  * // Output: true
450
- *
450
+ *
451
451
  * // Offset-based:
452
452
  * validateQueryPayload({
453
453
  * queryParams: {
@@ -455,7 +455,7 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
455
455
  * }
456
456
  * }, []);
457
457
  * // Output: true
458
- *
458
+ *
459
459
  * @example
460
460
  * // Input: Complex query with all features
461
461
  * validateQueryPayload({
@@ -475,7 +475,7 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
475
475
  * }
476
476
  * }, ['publishedAt', 'title', 'updatedAt']);
477
477
  * // Output: true (all parameters valid)
478
- *
478
+ *
479
479
  * @example
480
480
  * // Input: Invalid sort field
481
481
  * validateQueryPayload({
@@ -485,19 +485,19 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
485
485
  * }, ['title', 'createdAt']);
486
486
  * // Throws: RestApiValidationError
487
487
  * // "Field 'password' is not sortable. Sortable fields are: title, createdAt"
488
- *
488
+ *
489
489
  * @description
490
490
  * Used by:
491
491
  * - rest-api-plugin.query() method before data fetching
492
492
  * - Validates collection request parameters
493
- *
493
+ *
494
494
  * Purpose:
495
495
  * - Validates all JSON:API query parameters for collections
496
496
  * - Prevents SQL injection via filter key validation
497
497
  * - Enforces sortable field restrictions for performance
498
498
  * - Ensures consistent pagination across storage backends
499
499
  * - Validates include depth limits
500
- *
500
+ *
501
501
  * Data flow:
502
502
  * 1. Validates params object structure
503
503
  * 2. For each query parameter type, validates format
@@ -505,150 +505,150 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
505
505
  * 4. Validates pagination parameters are numbers/strings
506
506
  * 5. Returns true or throws descriptive error
507
507
  */
508
- export function validateQueryPayload(params, sortableFields = [], maxIncludeDepth = 3) {
508
+ export function validateQueryPayload (params, sortableFields = [], maxIncludeDepth = 3) {
509
509
  if (!params || typeof params !== 'object') {
510
510
  throw new RestApiPayloadError(
511
511
  'Query parameters must be an object',
512
512
  { path: 'params', expected: 'object', received: typeof params }
513
- );
513
+ )
514
514
  }
515
-
515
+
516
516
  // queryParams is optional but if present must be an object
517
517
  if (params.queryParams) {
518
518
  if (typeof params.queryParams !== 'object') {
519
519
  throw new RestApiPayloadError(
520
520
  'queryParams must be an object',
521
521
  { path: 'queryParams', expected: 'object', received: typeof params.queryParams }
522
- );
522
+ )
523
523
  }
524
-
525
- const { include, fields, filters, sort, page } = params.queryParams;
526
-
524
+
525
+ const { include, fields, filters, sort, page } = params.queryParams
526
+
527
527
  // Validate include
528
528
  if (include !== undefined) {
529
529
  if (!Array.isArray(include)) {
530
530
  throw new RestApiPayloadError(
531
531
  'queryParams.include must be an array of strings',
532
532
  { path: 'queryParams.include', expected: 'array', received: typeof include }
533
- );
533
+ )
534
534
  }
535
-
535
+
536
536
  include.forEach((path, index) => {
537
537
  if (typeof path !== 'string') {
538
538
  throw new RestApiPayloadError(
539
539
  `queryParams.include[${index}] must be a string`,
540
540
  { path: `queryParams.include[${index}]`, expected: 'string', received: typeof path }
541
- );
541
+ )
542
542
  }
543
- });
544
-
543
+ })
544
+
545
545
  // Validate include depth
546
- validateIncludeDepth(include, maxIncludeDepth);
546
+ validateIncludeDepth(include, maxIncludeDepth)
547
547
  }
548
-
548
+
549
549
  // Validate fields
550
550
  if (fields !== undefined) {
551
551
  if (typeof fields !== 'object' || fields === null || Array.isArray(fields)) {
552
552
  throw new RestApiPayloadError(
553
553
  'queryParams.fields must be an object',
554
554
  { path: 'queryParams.fields', expected: 'object', received: Array.isArray(fields) ? 'array' : typeof fields }
555
- );
555
+ )
556
556
  }
557
-
557
+
558
558
  Object.entries(fields).forEach(([resourceType, fieldList]) => {
559
559
  if (typeof fieldList !== 'string') {
560
560
  throw new RestApiPayloadError(
561
561
  `queryParams.fields['${resourceType}'] must be a comma-separated string`,
562
562
  { path: `queryParams.fields['${resourceType}']`, expected: 'string', received: typeof fieldList }
563
- );
563
+ )
564
564
  }
565
- });
565
+ })
566
566
  }
567
-
567
+
568
568
  // Validate filters
569
569
  if (filters !== undefined) {
570
570
  if (typeof filters !== 'object' || filters === null || Array.isArray(filters)) {
571
571
  throw new RestApiPayloadError(
572
572
  'queryParams.filters must be an object',
573
573
  { path: 'queryParams.filters', expected: 'object', received: Array.isArray(filters) ? 'array' : typeof filters }
574
- );
574
+ )
575
575
  }
576
576
  }
577
-
577
+
578
578
  // Validate sort
579
579
  if (sort !== undefined) {
580
580
  if (!Array.isArray(sort)) {
581
581
  throw new RestApiPayloadError(
582
582
  'queryParams.sort must be an array of strings',
583
583
  { path: 'queryParams.sort', expected: 'array', received: typeof sort }
584
- );
584
+ )
585
585
  }
586
-
586
+
587
587
  sort.forEach((field, index) => {
588
588
  if (typeof field !== 'string') {
589
589
  throw new RestApiPayloadError(
590
590
  `queryParams.sort[${index}] must be a string`,
591
591
  { path: `queryParams.sort[${index}]`, expected: 'string', received: typeof field }
592
- );
592
+ )
593
593
  }
594
-
594
+
595
595
  // Check if field is sortable (remove leading - for descending sort)
596
- const fieldName = field.startsWith('-') ? field.substring(1) : field;
596
+ const fieldName = field.startsWith('-') ? field.substring(1) : field
597
597
  if (sortableFields.length > 0 && !sortableFields.includes(fieldName)) {
598
598
  throw new RestApiValidationError(
599
599
  `Field '${fieldName}' is not sortable. Sortable fields are: ${sortableFields.join(', ')}`,
600
600
  { fields: ['sort'], violations: [{ field: 'sort', rule: 'sortable_field', message: `Field '${fieldName}' is not in the list of sortable fields` }] }
601
- );
601
+ )
602
602
  }
603
- });
603
+ })
604
604
  }
605
-
605
+
606
606
  // Validate page
607
607
  if (page !== undefined) {
608
608
  if (typeof page !== 'object' || page === null || Array.isArray(page)) {
609
609
  throw new RestApiPayloadError(
610
610
  'queryParams.page must be an object',
611
611
  { path: 'queryParams.page', expected: 'object', received: Array.isArray(page) ? 'array' : typeof page }
612
- );
612
+ )
613
613
  }
614
-
614
+
615
615
  // Common pagination parameters
616
616
  if ('number' in page && typeof page.number !== 'number' && typeof page.number !== 'string') {
617
617
  throw new RestApiPayloadError(
618
618
  'queryParams.page.number must be a number or string',
619
619
  { path: 'queryParams.page.number', expected: 'number or string', received: typeof page.number }
620
- );
620
+ )
621
621
  }
622
-
622
+
623
623
  if ('size' in page && typeof page.size !== 'number' && typeof page.size !== 'string') {
624
624
  throw new RestApiPayloadError(
625
625
  'queryParams.page.size must be a number or string',
626
626
  { path: 'queryParams.page.size', expected: 'number or string', received: typeof page.size }
627
- );
627
+ )
628
628
  }
629
-
629
+
630
630
  if ('limit' in page && typeof page.limit !== 'number' && typeof page.limit !== 'string') {
631
631
  throw new RestApiPayloadError(
632
632
  'queryParams.page.limit must be a number or string',
633
633
  { path: 'queryParams.page.limit', expected: 'number or string', received: typeof page.limit }
634
- );
634
+ )
635
635
  }
636
-
636
+
637
637
  if ('offset' in page && typeof page.offset !== 'number' && typeof page.offset !== 'string') {
638
638
  throw new RestApiPayloadError(
639
639
  'queryParams.page.offset must be a number or string',
640
640
  { path: 'queryParams.page.offset', expected: 'number or string', received: typeof page.offset }
641
- );
641
+ )
642
642
  }
643
643
  }
644
644
  }
645
-
646
- return true;
645
+
646
+ return true
647
647
  }
648
648
 
649
649
  /**
650
650
  * Validates a JSON:API document for POST requests (resource creation)
651
- *
651
+ *
652
652
  * @param {Object} inputRecord - JSON:API document to validate
653
653
  * @param {Object} inputRecord.data - Primary resource to create
654
654
  * @param {string} inputRecord.data.type - Resource type (must match a scope)
@@ -659,7 +659,7 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
659
659
  * @param {Object} scopes - Scopes proxy to verify resource types exist
660
660
  * @returns {boolean} True if valid
661
661
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
662
- *
662
+ *
663
663
  * @example
664
664
  * // Input: Simple resource creation
665
665
  * validatePostPayload({
@@ -672,7 +672,7 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
672
672
  * }
673
673
  * }, scopes);
674
674
  * // Output: true
675
- *
675
+ *
676
676
  * @example
677
677
  * // Input: Create with relationships
678
678
  * validatePostPayload({
@@ -695,7 +695,7 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
695
695
  * }
696
696
  * }, scopes);
697
697
  * // Output: true (validates each relationship)
698
- *
698
+ *
699
699
  * @example
700
700
  * // Input: Client-generated ID
701
701
  * validatePostPayload({
@@ -708,7 +708,7 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
708
708
  * }
709
709
  * }, scopes);
710
710
  * // Output: true (ID is optional for POST)
711
- *
711
+ *
712
712
  * @example
713
713
  * // Input: Compound document with included
714
714
  * validatePostPayload({
@@ -733,7 +733,7 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
733
733
  * ]
734
734
  * }, scopes);
735
735
  * // Output: true (included resources validated)
736
- *
736
+ *
737
737
  * @example
738
738
  * // Input: Missing required type
739
739
  * validatePostPayload({
@@ -743,19 +743,19 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
743
743
  * }, scopes);
744
744
  * // Throws: RestApiPayloadError
745
745
  * // 'POST request "data" must have a non-empty "type" string'
746
- *
746
+ *
747
747
  * @description
748
748
  * Used by:
749
749
  * - rest-api-plugin.post() method before creating resources
750
750
  * - Validates complete document structure
751
- *
751
+ *
752
752
  * Purpose:
753
753
  * - Ensures JSON:API compliance for resource creation
754
754
  * - Validates resource types exist in system
755
755
  * - Checks relationship references are valid
756
756
  * - Validates included resources have required fields
757
757
  * - Enables compound document creation patterns
758
- *
758
+ *
759
759
  * Data flow:
760
760
  * 1. Validates document has required 'data' property
761
761
  * 2. Checks primary resource type exists in scopes
@@ -764,142 +764,142 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
764
764
  * 5. Validates included resources array if present
765
765
  * 6. Returns true or throws descriptive error
766
766
  */
767
- export function validatePostPayload(inputRecord, scopes = null) {
767
+ export function validatePostPayload (inputRecord, scopes = null) {
768
768
  if (!inputRecord || typeof inputRecord !== 'object') {
769
769
  throw new RestApiPayloadError(
770
770
  'POST request body must be a JSON:API document object',
771
771
  { path: 'body', expected: 'object', received: typeof inputRecord }
772
- );
772
+ )
773
773
  }
774
-
774
+
775
775
  // Validate required 'data' property
776
776
  if (!('data' in inputRecord)) {
777
777
  throw new RestApiPayloadError(
778
778
  'POST request body must have a "data" property',
779
779
  { path: 'data', expected: 'property exists', received: 'missing' }
780
- );
780
+ )
781
781
  }
782
-
783
- const { data, included } = inputRecord;
784
-
782
+
783
+ const { data, included } = inputRecord
784
+
785
785
  // Validate primary data
786
786
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
787
787
  throw new RestApiPayloadError(
788
788
  'POST request "data" must be a single resource object',
789
789
  { path: 'data', expected: 'object', received: Array.isArray(data) ? 'array' : typeof data }
790
- );
790
+ )
791
791
  }
792
-
792
+
793
793
  if (typeof data.type !== 'string' || !data.type) {
794
794
  throw new RestApiPayloadError(
795
795
  'POST request "data" must have a non-empty "type" string',
796
796
  { path: 'data.type', expected: 'non-empty string', received: data.type || 'empty' }
797
- );
797
+ )
798
798
  }
799
-
799
+
800
800
  // Check if primary resource type is valid
801
801
  if (scopes && !scopes[data.type]) {
802
802
  throw new RestApiValidationError(
803
803
  `POST request "data.type" '${data.type}' is not a valid resource type. No scope with this name exists.`,
804
804
  { fields: ['data.type'], violations: [{ field: 'data.type', rule: 'valid_resource_type', message: `Resource type '${data.type}' does not exist` }] }
805
- );
805
+ )
806
806
  }
807
-
807
+
808
808
  // For POST, id is optional (server may generate it)
809
809
  if ('id' in data && data.id !== null && typeof data.id !== 'string' && typeof data.id !== 'number') {
810
810
  throw new RestApiPayloadError(
811
811
  'POST request "data.id" if present must be a string, number, or null',
812
812
  { path: 'data.id', expected: 'string, number, or null', received: typeof data.id }
813
- );
813
+ )
814
814
  }
815
-
815
+
816
816
  // Validate attributes if present
817
817
  if ('attributes' in data) {
818
818
  if (typeof data.attributes !== 'object' || data.attributes === null || Array.isArray(data.attributes)) {
819
819
  throw new RestApiPayloadError(
820
820
  'POST request "data.attributes" must be an object',
821
821
  { path: 'data.attributes', expected: 'object', received: Array.isArray(data.attributes) ? 'array' : typeof data.attributes }
822
- );
822
+ )
823
823
  }
824
824
  }
825
-
825
+
826
826
  // Validate relationships if present
827
827
  if ('relationships' in data) {
828
828
  if (typeof data.relationships !== 'object' || data.relationships === null || Array.isArray(data.relationships)) {
829
829
  throw new RestApiPayloadError(
830
830
  'POST request "data.relationships" must be an object',
831
831
  { path: 'data.relationships', expected: 'object', received: Array.isArray(data.relationships) ? 'array' : typeof data.relationships }
832
- );
832
+ )
833
833
  }
834
-
834
+
835
835
  Object.entries(data.relationships).forEach(([relName, relationship]) => {
836
- validateRelationship(relationship, relName, scopes);
837
- });
836
+ validateRelationship(relationship, relName, scopes)
837
+ })
838
838
  }
839
-
839
+
840
840
  // Validate included resources if present
841
841
  if (included !== undefined) {
842
842
  if (!Array.isArray(included)) {
843
843
  throw new RestApiPayloadError(
844
844
  'POST request "included" must be an array',
845
845
  { path: 'included', expected: 'array', received: typeof included }
846
- );
846
+ )
847
847
  }
848
-
848
+
849
849
  included.forEach((resource, index) => {
850
850
  if (!resource || typeof resource !== 'object') {
851
851
  throw new RestApiPayloadError(
852
852
  `POST request "included[${index}]" must be a resource object`,
853
853
  { path: `included[${index}]`, expected: 'object', received: typeof resource }
854
- );
854
+ )
855
855
  }
856
-
856
+
857
857
  if (typeof resource.type !== 'string' || !resource.type) {
858
858
  throw new RestApiPayloadError(
859
859
  `POST request "included[${index}]" must have a non-empty "type" string`,
860
860
  { path: `included[${index}].type`, expected: 'non-empty string', received: resource.type || 'empty' }
861
- );
861
+ )
862
862
  }
863
-
863
+
864
864
  // Check if included resource type is valid
865
865
  if (scopes && !scopes[resource.type]) {
866
866
  throw new RestApiValidationError(
867
867
  `POST request "included[${index}].type" '${resource.type}' is not a valid resource type. No scope with this name exists.`,
868
868
  { fields: [`included[${index}].type`], violations: [{ field: `included[${index}].type`, rule: 'valid_resource_type', message: `Resource type '${resource.type}' does not exist` }] }
869
- );
869
+ )
870
870
  }
871
-
871
+
872
872
  if (!('id' in resource) || resource.id === null || resource.id === undefined) {
873
873
  throw new RestApiPayloadError(
874
874
  `POST request "included[${index}]" must have a non-null "id"`,
875
875
  { path: `included[${index}].id`, expected: 'non-null value', received: 'null or undefined' }
876
- );
876
+ )
877
877
  }
878
-
878
+
879
879
  if (typeof resource.id !== 'string' && typeof resource.id !== 'number') {
880
880
  throw new RestApiPayloadError(
881
881
  `POST request "included[${index}].id" must be a string or number`,
882
882
  { path: `included[${index}].id`, expected: 'string or number', received: typeof resource.id }
883
- );
883
+ )
884
884
  }
885
-
885
+
886
886
  if ('attributes' in resource) {
887
887
  if (typeof resource.attributes !== 'object' || resource.attributes === null || Array.isArray(resource.attributes)) {
888
888
  throw new RestApiPayloadError(
889
889
  `POST request "included[${index}].attributes" must be an object`,
890
890
  { path: `included[${index}].attributes`, expected: 'object', received: Array.isArray(resource.attributes) ? 'array' : typeof resource.attributes }
891
- );
891
+ )
892
892
  }
893
893
  }
894
- });
894
+ })
895
895
  }
896
-
897
- return true;
896
+
897
+ return true
898
898
  }
899
899
 
900
900
  /**
901
901
  * Validates a JSON:API document for PUT requests (full resource replacement)
902
- *
902
+ *
903
903
  * @param {Object} inputRecord - JSON:API document to validate
904
904
  * @param {Object} inputRecord.data - Complete resource representation
905
905
  * @param {string} inputRecord.data.type - Resource type (must match existing)
@@ -909,7 +909,7 @@ export function validatePostPayload(inputRecord, scopes = null) {
909
909
  * @param {Object} scopes - Scopes proxy to verify resource types exist
910
910
  * @returns {boolean} True if valid
911
911
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
912
- *
912
+ *
913
913
  * @example
914
914
  * // Input: Complete resource replacement
915
915
  * validatePutPayload({
@@ -924,7 +924,7 @@ export function validatePostPayload(inputRecord, scopes = null) {
924
924
  * }
925
925
  * }, scopes);
926
926
  * // Output: true
927
- *
927
+ *
928
928
  * @example
929
929
  * // Input: Replace with new relationships
930
930
  * validatePutPayload({
@@ -953,7 +953,7 @@ export function validatePostPayload(inputRecord, scopes = null) {
953
953
  * }
954
954
  * }, scopes);
955
955
  * // Output: true (complete replacement)
956
- *
956
+ *
957
957
  * @example
958
958
  * // Input: Missing required ID
959
959
  * validatePutPayload({
@@ -964,7 +964,7 @@ export function validatePostPayload(inputRecord, scopes = null) {
964
964
  * }, scopes);
965
965
  * // Throws: RestApiPayloadError
966
966
  * // 'PUT request "data" must have an "id" property'
967
- *
967
+ *
968
968
  * @example
969
969
  * // Input: Includes not allowed
970
970
  * validatePutPayload({
@@ -979,19 +979,19 @@ export function validatePostPayload(inputRecord, scopes = null) {
979
979
  * }, scopes);
980
980
  * // Throws: RestApiPayloadError
981
981
  * // 'PUT requests cannot include an "included" array for creating new resources'
982
- *
982
+ *
983
983
  * @description
984
984
  * Used by:
985
985
  * - rest-api-plugin.put() method for full replacement
986
986
  * - Enforces PUT semantics vs PATCH
987
- *
987
+ *
988
988
  * Purpose:
989
989
  * - Ensures complete resource replacement semantics
990
990
  * - Requires ID to match URL parameter
991
991
  * - Prevents included resources (no side effects)
992
992
  * - Maintains idempotency of PUT operations
993
993
  * - Differentiates from PATCH partial updates
994
- *
994
+ *
995
995
  * Data flow:
996
996
  * 1. Validates document structure with data property
997
997
  * 2. Ensures no included array present
@@ -1000,107 +1000,107 @@ export function validatePostPayload(inputRecord, scopes = null) {
1000
1000
  * 5. Validates attributes and relationships if present
1001
1001
  * 6. Returns true or throws descriptive error
1002
1002
  */
1003
- export function validatePutPayload(inputRecord, scopes = null) {
1003
+ export function validatePutPayload (inputRecord, scopes = null) {
1004
1004
  if (!inputRecord || typeof inputRecord !== 'object') {
1005
1005
  throw new RestApiPayloadError(
1006
1006
  'PUT request body must be a JSON:API document object',
1007
1007
  { path: 'body', expected: 'object', received: typeof inputRecord }
1008
- );
1008
+ )
1009
1009
  }
1010
-
1010
+
1011
1011
  // Validate required 'data' property
1012
1012
  if (!('data' in inputRecord)) {
1013
1013
  throw new RestApiPayloadError(
1014
1014
  'PUT request body must have a "data" property',
1015
1015
  { path: 'data', expected: 'property exists', received: 'missing' }
1016
- );
1016
+ )
1017
1017
  }
1018
-
1019
- const { data, included } = inputRecord;
1020
-
1018
+
1019
+ const { data, included } = inputRecord
1020
+
1021
1021
  // PUT cannot have included array
1022
1022
  if (included !== undefined) {
1023
1023
  throw new RestApiPayloadError(
1024
1024
  'PUT requests cannot include an "included" array for creating new resources',
1025
1025
  { path: 'included', expected: 'undefined', received: 'array' }
1026
- );
1026
+ )
1027
1027
  }
1028
-
1028
+
1029
1029
  // Validate primary data
1030
1030
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
1031
1031
  throw new RestApiPayloadError(
1032
1032
  'PUT request "data" must be a single resource object',
1033
1033
  { path: 'data', expected: 'object', received: Array.isArray(data) ? 'array' : typeof data }
1034
- );
1034
+ )
1035
1035
  }
1036
-
1036
+
1037
1037
  if (typeof data.type !== 'string' || !data.type) {
1038
1038
  throw new RestApiPayloadError(
1039
1039
  'PUT request "data" must have a non-empty "type" string',
1040
1040
  { path: 'data.type', expected: 'non-empty string', received: data.type || 'empty' }
1041
- );
1041
+ )
1042
1042
  }
1043
-
1043
+
1044
1044
  // Check if resource type is valid
1045
1045
  if (scopes && !scopes[data.type]) {
1046
1046
  throw new RestApiValidationError(
1047
1047
  `PUT request "data.type" '${data.type}' is not a valid resource type. No scope with this name exists.`,
1048
1048
  { fields: ['data.type'], violations: [{ field: 'data.type', rule: 'valid_resource_type', message: `Resource type '${data.type}' does not exist` }] }
1049
- );
1049
+ )
1050
1050
  }
1051
-
1051
+
1052
1052
  // For PUT, id is required
1053
1053
  if (!('id' in data)) {
1054
1054
  throw new RestApiPayloadError(
1055
1055
  'PUT request "data" must have an "id" property',
1056
1056
  { path: 'data.id', expected: 'property exists', received: 'missing' }
1057
- );
1057
+ )
1058
1058
  }
1059
-
1059
+
1060
1060
  if (data.id === null || data.id === undefined || data.id === '') {
1061
1061
  throw new RestApiValidationError(
1062
1062
  'PUT request "data.id" cannot be null, undefined, or empty',
1063
1063
  { fields: ['data.id'], violations: [{ field: 'data.id', rule: 'not_empty', message: 'ID cannot be null, undefined, or empty' }] }
1064
- );
1064
+ )
1065
1065
  }
1066
-
1066
+
1067
1067
  if (typeof data.id !== 'string' && typeof data.id !== 'number') {
1068
1068
  throw new RestApiPayloadError(
1069
1069
  'PUT request "data.id" must be a string or number',
1070
1070
  { path: 'data.id', expected: 'string or number', received: typeof data.id }
1071
- );
1071
+ )
1072
1072
  }
1073
-
1073
+
1074
1074
  // Validate attributes if present
1075
1075
  if ('attributes' in data) {
1076
1076
  if (typeof data.attributes !== 'object' || data.attributes === null || Array.isArray(data.attributes)) {
1077
1077
  throw new RestApiPayloadError(
1078
1078
  'PUT request "data.attributes" must be an object',
1079
1079
  { path: 'data.attributes', expected: 'object', received: Array.isArray(data.attributes) ? 'array' : typeof data.attributes }
1080
- );
1080
+ )
1081
1081
  }
1082
1082
  }
1083
-
1083
+
1084
1084
  // Validate relationships if present
1085
1085
  if ('relationships' in data) {
1086
1086
  if (typeof data.relationships !== 'object' || data.relationships === null || Array.isArray(data.relationships)) {
1087
1087
  throw new RestApiPayloadError(
1088
1088
  'PUT request "data.relationships" must be an object',
1089
1089
  { path: 'data.relationships', expected: 'object', received: Array.isArray(data.relationships) ? 'array' : typeof data.relationships }
1090
- );
1090
+ )
1091
1091
  }
1092
-
1092
+
1093
1093
  Object.entries(data.relationships).forEach(([relName, relationship]) => {
1094
- validateRelationship(relationship, relName, scopes);
1095
- });
1094
+ validateRelationship(relationship, relName, scopes)
1095
+ })
1096
1096
  }
1097
-
1098
- return true;
1097
+
1098
+ return true
1099
1099
  }
1100
1100
 
1101
1101
  /**
1102
1102
  * Validates a JSON:API document for PATCH requests (partial resource updates)
1103
- *
1103
+ *
1104
1104
  * @param {Object} inputRecord - JSON:API document to validate
1105
1105
  * @param {Object} inputRecord.data - Partial resource representation
1106
1106
  * @param {string} inputRecord.data.type - Resource type (must match existing)
@@ -1110,7 +1110,7 @@ export function validatePutPayload(inputRecord, scopes = null) {
1110
1110
  * @param {Object} scopes - Scopes proxy to verify resource types exist
1111
1111
  * @returns {boolean} True if valid
1112
1112
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
1113
- *
1113
+ *
1114
1114
  * @example
1115
1115
  * // Input: Update single field
1116
1116
  * validatePatchPayload({
@@ -1124,7 +1124,7 @@ export function validatePutPayload(inputRecord, scopes = null) {
1124
1124
  * }
1125
1125
  * }, scopes);
1126
1126
  * // Output: true
1127
- *
1127
+ *
1128
1128
  * @example
1129
1129
  * // Input: Update multiple fields
1130
1130
  * validatePatchPayload({
@@ -1139,7 +1139,7 @@ export function validatePutPayload(inputRecord, scopes = null) {
1139
1139
  * }
1140
1140
  * }, scopes);
1141
1141
  * // Output: true (partial update)
1142
- *
1142
+ *
1143
1143
  * @example
1144
1144
  * // Input: Update relationships only
1145
1145
  * validatePatchPayload({
@@ -1155,7 +1155,7 @@ export function validatePutPayload(inputRecord, scopes = null) {
1155
1155
  * }
1156
1156
  * }, scopes);
1157
1157
  * // Output: true
1158
- *
1158
+ *
1159
1159
  * @example
1160
1160
  * // Input: Clear relationships
1161
1161
  * validatePatchPayload({
@@ -1173,7 +1173,7 @@ export function validatePutPayload(inputRecord, scopes = null) {
1173
1173
  * }
1174
1174
  * }, scopes);
1175
1175
  * // Output: true (clearing is valid)
1176
- *
1176
+ *
1177
1177
  * @example
1178
1178
  * // Input: No changes specified
1179
1179
  * validatePatchPayload({
@@ -1185,19 +1185,19 @@ export function validatePutPayload(inputRecord, scopes = null) {
1185
1185
  * }, scopes);
1186
1186
  * // Throws: RestApiValidationError
1187
1187
  * // 'PATCH request "data" must have at least one of "attributes" or "relationships"'
1188
- *
1188
+ *
1189
1189
  * @description
1190
1190
  * Used by:
1191
1191
  * - rest-api-plugin.patch() method for partial updates
1192
1192
  * - Enforces PATCH semantics vs PUT
1193
- *
1193
+ *
1194
1194
  * Purpose:
1195
1195
  * - Enables efficient partial updates (send only changes)
1196
1196
  * - Requires at least one change (attributes or relationships)
1197
1197
  * - Prevents included resources (no side effects)
1198
1198
  * - Reduces bandwidth for large resources
1199
1199
  * - Prevents race conditions in concurrent updates
1200
- *
1200
+ *
1201
1201
  * Data flow:
1202
1202
  * 1. Validates document structure with data property
1203
1203
  * 2. Ensures no included array present
@@ -1207,108 +1207,108 @@ export function validatePutPayload(inputRecord, scopes = null) {
1207
1207
  * 6. Validates each if present
1208
1208
  * 7. Returns true or throws descriptive error
1209
1209
  */
1210
- export function validatePatchPayload(inputRecord, scopes = null) {
1210
+ export function validatePatchPayload (inputRecord, scopes = null) {
1211
1211
  if (!inputRecord || typeof inputRecord !== 'object') {
1212
1212
  throw new RestApiPayloadError(
1213
1213
  'PATCH request body must be a JSON:API document object',
1214
1214
  { path: 'body', expected: 'object', received: typeof inputRecord }
1215
- );
1215
+ )
1216
1216
  }
1217
-
1217
+
1218
1218
  // Validate required 'data' property
1219
1219
  if (!('data' in inputRecord)) {
1220
1220
  throw new RestApiPayloadError(
1221
1221
  'PATCH request body must have a "data" property',
1222
1222
  { path: 'data', expected: 'property exists', received: 'missing' }
1223
- );
1223
+ )
1224
1224
  }
1225
-
1226
- const { data, included } = inputRecord;
1227
-
1225
+
1226
+ const { data, included } = inputRecord
1227
+
1228
1228
  // PATCH cannot have included array
1229
1229
  if (included !== undefined) {
1230
1230
  throw new RestApiPayloadError(
1231
1231
  'PATCH requests cannot include an "included" array for creating new resources',
1232
1232
  { path: 'included', expected: 'undefined', received: 'array' }
1233
- );
1233
+ )
1234
1234
  }
1235
-
1235
+
1236
1236
  // Validate primary data
1237
1237
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
1238
1238
  throw new RestApiPayloadError(
1239
1239
  'PATCH request "data" must be a single resource object',
1240
1240
  { path: 'data', expected: 'object', received: Array.isArray(data) ? 'array' : typeof data }
1241
- );
1241
+ )
1242
1242
  }
1243
-
1243
+
1244
1244
  if (typeof data.type !== 'string' || !data.type) {
1245
1245
  throw new RestApiPayloadError(
1246
1246
  'PATCH request "data" must have a non-empty "type" string',
1247
1247
  { path: 'data.type', expected: 'non-empty string', received: data.type || 'empty' }
1248
- );
1248
+ )
1249
1249
  }
1250
-
1250
+
1251
1251
  // Check if resource type is valid
1252
1252
  if (scopes && !scopes[data.type]) {
1253
1253
  throw new RestApiValidationError(
1254
1254
  `PATCH request "data.type" '${data.type}' is not a valid resource type. No scope with this name exists.`,
1255
1255
  { fields: ['data.type'], violations: [{ field: 'data.type', rule: 'valid_resource_type', message: `Resource type '${data.type}' does not exist` }] }
1256
- );
1256
+ )
1257
1257
  }
1258
-
1258
+
1259
1259
  // For PATCH, id is required
1260
1260
  if (!('id' in data)) {
1261
1261
  throw new RestApiPayloadError(
1262
1262
  'PATCH request "data" must have an "id" property',
1263
1263
  { path: 'data.id', expected: 'property exists', received: 'missing' }
1264
- );
1264
+ )
1265
1265
  }
1266
-
1266
+
1267
1267
  if (data.id === null || data.id === undefined || data.id === '') {
1268
1268
  throw new RestApiValidationError(
1269
1269
  'PATCH request "data.id" cannot be null, undefined, or empty',
1270
1270
  { fields: ['data.id'], violations: [{ field: 'data.id', rule: 'not_empty', message: 'ID cannot be null, undefined, or empty' }] }
1271
- );
1271
+ )
1272
1272
  }
1273
-
1273
+
1274
1274
  if (typeof data.id !== 'string' && typeof data.id !== 'number') {
1275
1275
  throw new RestApiPayloadError(
1276
1276
  'PATCH request "data.id" must be a string or number',
1277
1277
  { path: 'data.id', expected: 'string or number', received: typeof data.id }
1278
- );
1278
+ )
1279
1279
  }
1280
-
1280
+
1281
1281
  // For PATCH, at least one of attributes or relationships should be present
1282
1282
  if (!('attributes' in data) && !('relationships' in data)) {
1283
1283
  throw new RestApiValidationError(
1284
1284
  'PATCH request "data" must have at least one of "attributes" or "relationships"',
1285
1285
  { fields: ['data'], violations: [{ field: 'data', rule: 'partial_update', message: 'Must include at least one of attributes or relationships' }] }
1286
- );
1286
+ )
1287
1287
  }
1288
-
1288
+
1289
1289
  // Validate attributes if present
1290
1290
  if ('attributes' in data && data.attributes !== undefined) {
1291
1291
  if (typeof data.attributes !== 'object' || data.attributes === null || Array.isArray(data.attributes)) {
1292
1292
  throw new RestApiPayloadError(
1293
1293
  'PATCH request "data.attributes" must be an object',
1294
1294
  { path: 'data.attributes', expected: 'object', received: Array.isArray(data.attributes) ? 'array' : typeof data.attributes }
1295
- );
1295
+ )
1296
1296
  }
1297
1297
  }
1298
-
1298
+
1299
1299
  // Validate relationships if present
1300
1300
  if ('relationships' in data && data.relationships !== undefined) {
1301
1301
  if (typeof data.relationships !== 'object' || data.relationships === null || Array.isArray(data.relationships)) {
1302
1302
  throw new RestApiPayloadError(
1303
1303
  'PATCH request "data.relationships" must be an object',
1304
1304
  { path: 'data.relationships', expected: 'object', received: Array.isArray(data.relationships) ? 'array' : typeof data.relationships }
1305
- );
1305
+ )
1306
1306
  }
1307
-
1307
+
1308
1308
  Object.entries(data.relationships).forEach(([relName, relationship]) => {
1309
- validateRelationship(relationship, relName, scopes);
1310
- });
1309
+ validateRelationship(relationship, relName, scopes)
1310
+ })
1311
1311
  }
1312
-
1313
- return true;
1314
- }
1312
+
1313
+ return true
1314
+ }