json-rest-api 1.0.7 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -1,4 +1,4 @@
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
@@ -7,6 +7,18 @@ import { RestApiValidationError, RestApiPayloadError } from '../../../lib/rest-a
7
7
  * @param {number} maxDepth - Maximum allowed depth
8
8
  * @returns {boolean} True if valid
9
9
  * @throws {RestApiValidationError} If depth is exceeded
10
+ *
11
+ * @example
12
+ * // Input: Valid include paths
13
+ * validateIncludeDepth(['author', 'comments.author'], 3);
14
+ * // Output: true (depths are 1 and 2, both under limit)
15
+ *
16
+ * @example
17
+ * // Input: Path exceeding depth limit
18
+ * validateIncludeDepth(['author.company.employees.manager'], 3);
19
+ * // Throws: RestApiValidationError
20
+ * // "Include path 'author.company.employees.manager' exceeds maximum depth of 3"
21
+ *
10
22
  * @private
11
23
  */
12
24
  function validateIncludeDepth(includes, maxDepth) {
@@ -41,40 +53,51 @@ function validateIncludeDepth(includes, maxDepth) {
41
53
  /**
42
54
  * Validates a JSON:API resource identifier object
43
55
  *
44
- * Ensures that a resource identifier follows JSON:API spec with proper type and id.
45
- * Used internally to validate relationship data and included resources.
46
- *
47
- * @param {Object} identifier - The resource identifier to validate
48
- * @param {string} context - Context for error messages (e.g., "Relationship 'author'")
49
- * @param {Object} scopes - The scopes proxy object to check if type exists
56
+ * @param {Object} identifier - Resource identifier to validate
57
+ * @param {string} context - Context for error messages
58
+ * @param {Object} scopes - Scopes proxy to verify resource types
50
59
  * @returns {boolean} True if valid
51
60
  * @throws {RestApiPayloadError|RestApiValidationError} If validation fails
52
61
  *
53
- * @example <caption>Valid resource identifier</caption>
54
- * // This passes validation:
62
+ * @example
63
+ * // Input: Valid identifier
55
64
  * validateResourceIdentifier(
56
65
  * { type: 'articles', id: '123' },
57
66
  * "Relationship 'author'",
58
67
  * scopes
59
68
  * );
69
+ * // Output: true
60
70
  *
61
- * @example <caption>Invalid - missing type</caption>
62
- * // This throws RestApiPayloadError:
71
+ * @example
72
+ * // Input: Missing type field
63
73
  * validateResourceIdentifier(
64
- * { id: '123' }, // Missing 'type'
74
+ * { id: '123' },
65
75
  * "Relationship 'comments'",
66
76
  * scopes
67
77
  * );
68
- * // Error: "Relationship 'comments': Resource identifier must have a non-empty 'type' string"
78
+ * // Throws: RestApiPayloadError
79
+ * // "Relationship 'comments': Resource identifier must have a non-empty 'type' string"
69
80
  *
70
- * @example <caption>Invalid - unknown resource type</caption>
71
- * // This throws RestApiValidationError if 'unknown' scope doesn't exist:
81
+ * @example
82
+ * // Input: Unknown resource type
72
83
  * validateResourceIdentifier(
73
- * { type: 'unknown', id: '456' },
84
+ * { type: 'unicorns', id: '456' },
74
85
  * "Included resource",
75
- * scopes
86
+ * scopes // scopes['unicorns'] doesn't exist
76
87
  * );
77
- * // Error: "Included resource: Unknown resource type 'unknown'. No scope with this name exists."
88
+ * // Throws: RestApiValidationError
89
+ * // "Included resource: Unknown resource type 'unicorns'. No scope with this name exists."
90
+ *
91
+ * @description
92
+ * Used by:
93
+ * - validateRelationship to check relationship data
94
+ * - validatePostPayload to validate included resources
95
+ * - Internal validation of resource references
96
+ *
97
+ * Purpose:
98
+ * - Ensures JSON:API compliance for resource identifiers
99
+ * - Validates resource types exist in the system
100
+ * - Provides clear error context for debugging
78
101
  *
79
102
  * @private
80
103
  */
@@ -124,26 +147,23 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
124
147
  /**
125
148
  * Validates a relationship object in JSON:API format
126
149
  *
127
- * Ensures relationships have proper structure with 'data' property containing
128
- * either null, a single resource identifier, or an array of resource identifiers.
129
- * Used to validate relationships in POST, PUT, and PATCH payloads.
130
- *
131
- * @param {Object} relationship - The relationship to validate
132
- * @param {string} relationshipName - Name of the relationship for error context
133
- * @param {Object} scopes - The scopes proxy object to check if type exists
150
+ * @param {Object} relationship - Relationship object to validate
151
+ * @param {string} relationshipName - Name for error context
152
+ * @param {Object} scopes - Scopes proxy to verify resource types
134
153
  * @returns {boolean} True if valid
135
154
  * @throws {RestApiPayloadError} If validation fails
136
155
  *
137
- * @example <caption>Valid to-one relationship</caption>
138
- * // Setting an article's author:
156
+ * @example
157
+ * // Input: To-one relationship
139
158
  * validateRelationship(
140
159
  * { data: { type: 'users', id: '42' } },
141
160
  * 'author',
142
161
  * scopes
143
162
  * );
163
+ * // Output: true
144
164
  *
145
- * @example <caption>Valid to-many relationship</caption>
146
- * // Setting an article's tags:
165
+ * @example
166
+ * // Input: To-many relationship
147
167
  * validateRelationship(
148
168
  * {
149
169
  * data: [
@@ -154,22 +174,38 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
154
174
  * 'tags',
155
175
  * scopes
156
176
  * );
177
+ * // Output: true
157
178
  *
158
- * @example <caption>Valid null relationship (removing association)</caption>
159
- * // Removing an article's featured image:
179
+ * @example
180
+ * // Input: Null relationship (remove association)
160
181
  * validateRelationship(
161
182
  * { data: null },
162
183
  * 'featuredImage',
163
184
  * scopes
164
185
  * );
186
+ * // Output: true (null is valid for clearing)
165
187
  *
166
- * @example <caption>Valid empty to-many relationship</caption>
167
- * // Clearing all comments:
188
+ * @example
189
+ * // Input: Missing data property
168
190
  * validateRelationship(
169
- * { data: [] },
170
- * 'comments',
191
+ * { type: 'users', id: '1' }, // Wrong structure
192
+ * 'author',
171
193
  * scopes
172
194
  * );
195
+ * // Throws: RestApiPayloadError
196
+ * // "Relationship 'author' must have a 'data' property"
197
+ *
198
+ * @description
199
+ * Used by:
200
+ * - validatePostPayload for new resource relationships
201
+ * - validatePutPayload for relationship replacement
202
+ * - validatePatchPayload for relationship updates
203
+ *
204
+ * Purpose:
205
+ * - Ensures JSON:API relationship structure
206
+ * - Validates both to-one and to-many relationships
207
+ * - Handles null/empty cases for clearing relationships
208
+ * - Validates each resource identifier in arrays
173
209
  *
174
210
  * @private
175
211
  */
@@ -216,65 +252,85 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
216
252
  /**
217
253
  * Validates query parameters for GET requests (single resource retrieval)
218
254
  *
219
- * Ensures GET requests have required 'id' parameter and validates optional
220
- * query parameters like 'include' and 'fields' for sparse fieldsets.
221
- * This is called by the REST API plugin before executing a GET request.
222
- *
223
- * @param {Object} params - The parameters object containing id and queryParams
224
- * @param {string|number} params.id - The resource ID to fetch
255
+ * @param {Object} params - Parameters containing id and queryParams
256
+ * @param {string|number} params.id - Resource ID to fetch
225
257
  * @param {Object} [params.queryParams] - Optional query parameters
226
258
  * @param {string[]} [params.queryParams.include] - Related resources to include
227
259
  * @param {Object} [params.queryParams.fields] - Sparse fieldsets by resource type
260
+ * @param {number} maxIncludeDepth - Maximum allowed include depth
228
261
  * @returns {boolean} True if valid
229
262
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
230
263
  *
231
- * @example <caption>Basic GET validation</caption>
232
- * // Fetch article 123:
264
+ * @example
265
+ * // Input: Basic GET request
233
266
  * validateGetPayload({
234
267
  * id: '123'
235
268
  * });
269
+ * // Output: true
236
270
  *
237
- * @example <caption>GET with includes</caption>
238
- * // Fetch article with author and comments:
271
+ * @example
272
+ * // Input: GET with nested includes
239
273
  * validateGetPayload({
240
274
  * id: '123',
241
275
  * queryParams: {
242
276
  * include: ['author', 'comments.author']
243
277
  * }
244
278
  * });
279
+ * // Output: true (include depths are 1 and 2)
245
280
  *
246
- * @example <caption>GET with sparse fieldsets</caption>
247
- * // Fetch only specific fields:
281
+ * @example
282
+ * // Input: GET with sparse fieldsets
248
283
  * validateGetPayload({
249
284
  * id: '123',
250
285
  * queryParams: {
251
286
  * include: ['author'],
252
287
  * fields: {
253
- * articles: 'title,body',
254
- * users: 'name,email'
288
+ * articles: 'title,body', // Only these article fields
289
+ * users: 'name,email' // Only these user fields
255
290
  * }
256
291
  * }
257
292
  * });
293
+ * // Output: true
258
294
  *
259
- * @example <caption>Invalid - missing ID</caption>
260
- * // This throws RestApiValidationError:
295
+ * @example
296
+ * // Input: Missing ID parameter
261
297
  * validateGetPayload({
262
298
  * queryParams: { include: ['author'] }
263
299
  * });
264
- * // Error: "GET request must include an id parameter"
300
+ * // Throws: RestApiValidationError
301
+ * // "GET request must include an id parameter"
265
302
  *
266
- * @example <caption>Invalid - null ID</caption>
267
- * // This throws RestApiValidationError:
303
+ * @example
304
+ * // Input: Invalid field specification
268
305
  * validateGetPayload({
269
- * id: null
306
+ * id: '123',
307
+ * queryParams: {
308
+ * fields: {
309
+ * articles: ['title', 'body'] // Should be string, not array
310
+ * }
311
+ * }
270
312
  * });
271
- * // Error: "GET request id cannot be null, undefined, or empty"
313
+ * // Throws: RestApiPayloadError
314
+ * // "queryParams.fields['articles'] must be a comma-separated string"
272
315
  *
273
- * @example <caption>Why this is useful upstream</caption>
274
- * // The REST API plugin uses this to ensure requests are valid before
275
- * // passing them to the storage layer (e.g., Knex plugin), preventing
276
- * // invalid queries from reaching the database and providing consistent
277
- * // error messages across all storage implementations.
316
+ * @description
317
+ * Used by:
318
+ * - rest-api-plugin.get() method before data fetching
319
+ * - Validates parameters before passing to storage layer
320
+ *
321
+ * Purpose:
322
+ * - Ensures required ID parameter is present and valid
323
+ * - Validates JSON:API query parameter structure
324
+ * - Prevents invalid queries from reaching database
325
+ * - Enforces include depth limits for performance
326
+ * - Provides consistent error messages across storage backends
327
+ *
328
+ * Data flow:
329
+ * 1. Checks params object structure
330
+ * 2. Validates required ID parameter
331
+ * 3. If queryParams exist, validates each type
332
+ * 4. Validates include paths don't exceed depth limit
333
+ * 5. Returns true or throws descriptive error
278
334
  */
279
335
  export function validateGetPayload(params, maxIncludeDepth = 3) {
280
336
  if (!params || typeof params !== 'object') {
@@ -358,54 +414,56 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
358
414
  /**
359
415
  * Validates query parameters for collection requests (query/list operations)
360
416
  *
361
- * Ensures collection queries have valid filters, sorting, pagination, includes,
362
- * and sparse fieldsets. Validates that sort fields are in the allowed list.
363
- * This is called by the REST API plugin before executing a query request.
364
- *
365
- * @param {Object} params - The parameters object
417
+ * @param {Object} params - Parameters object
366
418
  * @param {Object} [params.queryParams] - Query parameters for the collection
367
419
  * @param {string[]} [params.queryParams.include] - Related resources to include
368
420
  * @param {Object} [params.queryParams.fields] - Sparse fieldsets by resource type
369
421
  * @param {Object} [params.queryParams.filters] - Filter conditions
370
422
  * @param {string[]} [params.queryParams.sort] - Sort fields (prefix with - for DESC)
371
423
  * @param {Object} [params.queryParams.page] - Pagination parameters
372
- * @param {string[]} sortableFields - Array of fields that can be sorted
424
+ * @param {string[]} sortableFields - Fields allowed for sorting
425
+ * @param {number} maxIncludeDepth - Maximum allowed include depth
373
426
  * @returns {boolean} True if valid
374
427
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
375
428
  *
376
- * @example <caption>Basic query validation</caption>
377
- * // Simple query with no parameters:
429
+ * @example
430
+ * // Input: Empty query (fetch all)
378
431
  * validateQueryPayload({}, ['title', 'createdAt']);
432
+ * // Output: true
379
433
  *
380
- * @example <caption>Query with filters and sorting</caption>
381
- * // Find published articles sorted by date:
434
+ * @example
435
+ * // Input: Query with filters and sorting
382
436
  * validateQueryPayload({
383
437
  * queryParams: {
384
438
  * filters: {
385
439
  * status: 'published',
386
- * 'author.name': 'John Doe' // Cross-table filter
440
+ * 'author.name': 'John Doe' // Dot notation for relationships
387
441
  * },
388
- * sort: ['-publishedAt', 'title'], // DESC by date, then ASC by title
442
+ * sort: ['-publishedAt', 'title'] // DESC publishedAt, ASC title
389
443
  * }
390
444
  * }, ['publishedAt', 'title', 'createdAt']);
445
+ * // Output: true
391
446
  *
392
- * @example <caption>Query with pagination</caption>
393
- * // Page-based pagination:
447
+ * @example
448
+ * // Input: Different pagination styles
449
+ * // Page-based:
394
450
  * validateQueryPayload({
395
451
  * queryParams: {
396
- * page: { number: 2, size: 20 }
452
+ * page: { number: 2, size: 20 } // Page 2, 20 items per page
397
453
  * }
398
454
  * }, []);
455
+ * // Output: true
399
456
  *
400
- * // Offset-based pagination:
457
+ * // Offset-based:
401
458
  * validateQueryPayload({
402
459
  * queryParams: {
403
- * page: { offset: 40, limit: 20 }
460
+ * page: { offset: 40, limit: 20 } // Skip 40, take 20
404
461
  * }
405
462
  * }, []);
463
+ * // Output: true
406
464
  *
407
- * @example <caption>Complex query with all features</caption>
408
- * // Full-featured query:
465
+ * @example
466
+ * // Input: Complex query with all features
409
467
  * validateQueryPayload({
410
468
  * queryParams: {
411
469
  * include: ['author', 'tags', 'comments.author'],
@@ -422,22 +480,36 @@ export function validateGetPayload(params, maxIncludeDepth = 3) {
422
480
  * page: { number: 1, size: 10 }
423
481
  * }
424
482
  * }, ['publishedAt', 'title', 'updatedAt']);
483
+ * // Output: true (all parameters valid)
425
484
  *
426
- * @example <caption>Invalid - non-sortable field</caption>
427
- * // This throws RestApiValidationError:
485
+ * @example
486
+ * // Input: Invalid sort field
428
487
  * validateQueryPayload({
429
488
  * queryParams: {
430
- * sort: ['secretField'] // Not in sortableFields
489
+ * sort: ['password'] // Not in sortableFields
431
490
  * }
432
491
  * }, ['title', 'createdAt']);
433
- * // Error: "Field 'secretField' is not sortable. Sortable fields are: title, createdAt"
434
- *
435
- * @example <caption>Why this is useful upstream</caption>
436
- * // The REST API plugin uses this to:
437
- * // 1. Prevent SQL injection by validating filter keys
438
- * // 2. Ensure consistent pagination across storage backends
439
- * // 3. Prevent sorting on non-indexed fields that could slow queries
440
- * // 4. Provide clear error messages before hitting the database
492
+ * // Throws: RestApiValidationError
493
+ * // "Field 'password' is not sortable. Sortable fields are: title, createdAt"
494
+ *
495
+ * @description
496
+ * Used by:
497
+ * - rest-api-plugin.query() method before data fetching
498
+ * - Validates collection request parameters
499
+ *
500
+ * Purpose:
501
+ * - Validates all JSON:API query parameters for collections
502
+ * - Prevents SQL injection via filter key validation
503
+ * - Enforces sortable field restrictions for performance
504
+ * - Ensures consistent pagination across storage backends
505
+ * - Validates include depth limits
506
+ *
507
+ * Data flow:
508
+ * 1. Validates params object structure
509
+ * 2. For each query parameter type, validates format
510
+ * 3. Checks sort fields against allowed list
511
+ * 4. Validates pagination parameters are numbers/strings
512
+ * 5. Returns true or throws descriptive error
441
513
  */
442
514
  export function validateQueryPayload(params, sortableFields = [], maxIncludeDepth = 3) {
443
515
  if (!params || typeof params !== 'object') {
@@ -583,24 +655,19 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
583
655
  /**
584
656
  * Validates a JSON:API document for POST requests (resource creation)
585
657
  *
586
- * Ensures POST payloads follow JSON:API spec with proper data structure,
587
- * validates relationships reference existing resources, and checks that
588
- * included resources have required IDs. This is the main validation gateway
589
- * for creating new resources through the REST API.
590
- *
591
- * @param {Object} inputRecord - The JSON:API document to validate
592
- * @param {Object} inputRecord.data - The primary resource to create
658
+ * @param {Object} inputRecord - JSON:API document to validate
659
+ * @param {Object} inputRecord.data - Primary resource to create
593
660
  * @param {string} inputRecord.data.type - Resource type (must match a scope)
594
661
  * @param {string|number} [inputRecord.data.id] - Optional client-generated ID
595
662
  * @param {Object} [inputRecord.data.attributes] - Resource attributes
596
663
  * @param {Object} [inputRecord.data.relationships] - Resource relationships
597
664
  * @param {Object[]} [inputRecord.included] - Related resources for compound documents
598
- * @param {Object} scopes - The scopes proxy object to check if type exists
665
+ * @param {Object} scopes - Scopes proxy to verify resource types exist
599
666
  * @returns {boolean} True if valid
600
667
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
601
668
  *
602
- * @example <caption>Basic POST validation</caption>
603
- * // Create a simple article:
669
+ * @example
670
+ * // Input: Simple resource creation
604
671
  * validatePostPayload({
605
672
  * data: {
606
673
  * type: 'articles',
@@ -610,9 +677,10 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
610
677
  * }
611
678
  * }
612
679
  * }, scopes);
680
+ * // Output: true
613
681
  *
614
- * @example <caption>POST with relationships</caption>
615
- * // Create article with author and tags:
682
+ * @example
683
+ * // Input: Create with relationships
616
684
  * validatePostPayload({
617
685
  * data: {
618
686
  * type: 'articles',
@@ -621,10 +689,10 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
621
689
  * },
622
690
  * relationships: {
623
691
  * author: {
624
- * data: { type: 'users', id: '42' }
692
+ * data: { type: 'users', id: '42' } // To-one
625
693
  * },
626
694
  * tags: {
627
- * data: [
695
+ * data: [ // To-many
628
696
  * { type: 'tags', id: '1' },
629
697
  * { type: 'tags', id: '2' }
630
698
  * ]
@@ -632,21 +700,23 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
632
700
  * }
633
701
  * }
634
702
  * }, scopes);
703
+ * // Output: true (validates each relationship)
635
704
  *
636
- * @example <caption>POST with client-generated ID</caption>
637
- * // Some APIs allow client-generated IDs:
705
+ * @example
706
+ * // Input: Client-generated ID
638
707
  * validatePostPayload({
639
708
  * data: {
640
709
  * type: 'articles',
641
- * id: 'article-2023-11-15-hello-world', // Client-provided ID
710
+ * id: 'article-2023-11-15', // Client provides ID
642
711
  * attributes: {
643
712
  * title: 'Hello World'
644
713
  * }
645
714
  * }
646
715
  * }, scopes);
716
+ * // Output: true (ID is optional for POST)
647
717
  *
648
- * @example <caption>POST with included resources (compound document)</caption>
649
- * // Create article with embedded author data for client convenience:
718
+ * @example
719
+ * // Input: Compound document with included
650
720
  * validatePostPayload({
651
721
  * data: {
652
722
  * type: 'articles',
@@ -657,10 +727,10 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
657
727
  * }
658
728
  * }
659
729
  * },
660
- * included: [
730
+ * included: [ // Related resource data
661
731
  * {
662
732
  * type: 'users',
663
- * id: '42',
733
+ * id: '42', // Must have ID
664
734
  * attributes: {
665
735
  * name: 'John Doe',
666
736
  * email: 'john@example.com'
@@ -668,33 +738,37 @@ export function validateQueryPayload(params, sortableFields = [], maxIncludeDept
668
738
  * }
669
739
  * ]
670
740
  * }, scopes);
741
+ * // Output: true (included resources validated)
671
742
  *
672
- * @example <caption>Invalid - missing type</caption>
673
- * // This throws RestApiPayloadError:
743
+ * @example
744
+ * // Input: Missing required type
674
745
  * validatePostPayload({
675
746
  * data: {
676
747
  * attributes: { title: 'Missing Type' }
677
748
  * }
678
749
  * }, scopes);
679
- * // Error: 'POST request "data" must have a non-empty "type" string'
750
+ * // Throws: RestApiPayloadError
751
+ * // 'POST request "data" must have a non-empty "type" string'
680
752
  *
681
- * @example <caption>Invalid - unknown resource type</caption>
682
- * // This throws RestApiValidationError:
683
- * validatePostPayload({
684
- * data: {
685
- * type: 'unicorns', // Not a registered scope
686
- * attributes: { name: 'Sparkles' }
687
- * }
688
- * }, scopes);
689
- * // Error: 'POST request "data.type" 'unicorns' is not a valid resource type'
690
- *
691
- * @example <caption>Why this is useful upstream</caption>
692
- * // The REST API plugin uses this to:
693
- * // 1. Ensure data integrity before hitting the database
694
- * // 2. Validate relationships exist before creating foreign key references
695
- * // 3. Provide consistent API behavior across different storage backends
696
- * // 4. Enable compound document creation while maintaining data consistency
697
- * // 5. Catch errors early with meaningful messages for API consumers
753
+ * @description
754
+ * Used by:
755
+ * - rest-api-plugin.post() method before creating resources
756
+ * - Validates complete document structure
757
+ *
758
+ * Purpose:
759
+ * - Ensures JSON:API compliance for resource creation
760
+ * - Validates resource types exist in system
761
+ * - Checks relationship references are valid
762
+ * - Validates included resources have required fields
763
+ * - Enables compound document creation patterns
764
+ *
765
+ * Data flow:
766
+ * 1. Validates document has required 'data' property
767
+ * 2. Checks primary resource type exists in scopes
768
+ * 3. Validates attributes object if present
769
+ * 4. Validates each relationship if present
770
+ * 5. Validates included resources array if present
771
+ * 6. Returns true or throws descriptive error
698
772
  */
699
773
  export function validatePostPayload(inputRecord, scopes = null) {
700
774
  if (!inputRecord || typeof inputRecord !== 'object') {
@@ -832,27 +906,22 @@ export function validatePostPayload(inputRecord, scopes = null) {
832
906
  /**
833
907
  * Validates a JSON:API document for PUT requests (full resource replacement)
834
908
  *
835
- * Ensures PUT payloads follow JSON:API spec for complete resource replacement.
836
- * Unlike PATCH, PUT requires the complete resource representation and doesn't
837
- * allow 'included' arrays (as PUT shouldn't create new related resources).
838
- * The ID is required and must match the URL parameter.
839
- *
840
- * @param {Object} inputRecord - The JSON:API document to validate
841
- * @param {Object} inputRecord.data - The complete resource representation
909
+ * @param {Object} inputRecord - JSON:API document to validate
910
+ * @param {Object} inputRecord.data - Complete resource representation
842
911
  * @param {string} inputRecord.data.type - Resource type (must match existing)
843
912
  * @param {string|number} inputRecord.data.id - Resource ID (required)
844
913
  * @param {Object} [inputRecord.data.attributes] - Complete attributes
845
914
  * @param {Object} [inputRecord.data.relationships] - Complete relationships
846
- * @param {Object} scopes - The scopes proxy object to check if type exists
915
+ * @param {Object} scopes - Scopes proxy to verify resource types exist
847
916
  * @returns {boolean} True if valid
848
917
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
849
918
  *
850
- * @example <caption>Basic PUT validation</caption>
851
- * // Replace entire article:
919
+ * @example
920
+ * // Input: Complete resource replacement
852
921
  * validatePutPayload({
853
922
  * data: {
854
923
  * type: 'articles',
855
- * id: '123',
924
+ * id: '123', // Required for PUT
856
925
  * attributes: {
857
926
  * title: 'Updated Title',
858
927
  * body: 'Completely new body text',
@@ -860,9 +929,10 @@ export function validatePostPayload(inputRecord, scopes = null) {
860
929
  * }
861
930
  * }
862
931
  * }, scopes);
932
+ * // Output: true
863
933
  *
864
- * @example <caption>PUT with relationship replacement</caption>
865
- * // Replace article with new author and tags:
934
+ * @example
935
+ * // Input: Replace with new relationships
866
936
  * validatePutPayload({
867
937
  * data: {
868
938
  * type: 'articles',
@@ -873,53 +943,68 @@ export function validatePostPayload(inputRecord, scopes = null) {
873
943
  * },
874
944
  * relationships: {
875
945
  * author: {
876
- * data: { type: 'users', id: '99' } // Changed author
946
+ * data: { type: 'users', id: '99' } // New author
877
947
  * },
878
948
  * tags: {
879
- * data: [ // Completely new set of tags
949
+ * data: [ // Replace all tags
880
950
  * { type: 'tags', id: '5' },
881
951
  * { type: 'tags', id: '6' },
882
952
  * { type: 'tags', id: '7' }
883
953
  * ]
884
954
  * },
885
955
  * featuredImage: {
886
- * data: null // Remove featured image
956
+ * data: null // Remove relationship
887
957
  * }
888
958
  * }
889
959
  * }
890
960
  * }, scopes);
961
+ * // Output: true (complete replacement)
891
962
  *
892
- * @example <caption>Invalid - missing ID</caption>
893
- * // This throws RestApiPayloadError:
963
+ * @example
964
+ * // Input: Missing required ID
894
965
  * validatePutPayload({
895
966
  * data: {
896
967
  * type: 'articles',
897
968
  * attributes: { title: 'No ID' }
898
969
  * }
899
970
  * }, scopes);
900
- * // Error: 'PUT request "data" must have an "id" property'
971
+ * // Throws: RestApiPayloadError
972
+ * // 'PUT request "data" must have an "id" property'
901
973
  *
902
- * @example <caption>Invalid - includes not allowed</caption>
903
- * // This throws RestApiPayloadError:
974
+ * @example
975
+ * // Input: Includes not allowed
904
976
  * validatePutPayload({
905
977
  * data: {
906
978
  * type: 'articles',
907
979
  * id: '123',
908
980
  * attributes: { title: 'Updated' }
909
981
  * },
910
- * included: [ // Not allowed in PUT
911
- * { type: 'users', id: '1', attributes: { name: 'John' } }
982
+ * included: [ // Not allowed!
983
+ * { type: 'users', id: '1' }
912
984
  * ]
913
985
  * }, scopes);
914
- * // Error: 'PUT requests cannot include an "included" array'
915
- *
916
- * @example <caption>Why this is useful upstream</caption>
917
- * // The REST API plugin uses PUT validation to:
918
- * // 1. Ensure idempotent operations (same PUT = same result)
919
- * // 2. Prevent accidental partial updates (use PATCH for that)
920
- * // 3. Maintain clear semantics: PUT = replace, PATCH = modify
921
- * // 4. Prevent side effects like creating related resources
922
- * // 5. Ensure the ID in payload matches the URL parameter
986
+ * // Throws: RestApiPayloadError
987
+ * // 'PUT requests cannot include an "included" array for creating new resources'
988
+ *
989
+ * @description
990
+ * Used by:
991
+ * - rest-api-plugin.put() method for full replacement
992
+ * - Enforces PUT semantics vs PATCH
993
+ *
994
+ * Purpose:
995
+ * - Ensures complete resource replacement semantics
996
+ * - Requires ID to match URL parameter
997
+ * - Prevents included resources (no side effects)
998
+ * - Maintains idempotency of PUT operations
999
+ * - Differentiates from PATCH partial updates
1000
+ *
1001
+ * Data flow:
1002
+ * 1. Validates document structure with data property
1003
+ * 2. Ensures no included array present
1004
+ * 3. Validates resource type exists in scopes
1005
+ * 4. Requires ID property (unlike POST)
1006
+ * 5. Validates attributes and relationships if present
1007
+ * 6. Returns true or throws descriptive error
923
1008
  */
924
1009
  export function validatePutPayload(inputRecord, scopes = null) {
925
1010
  if (!inputRecord || typeof inputRecord !== 'object') {
@@ -1022,36 +1107,32 @@ export function validatePutPayload(inputRecord, scopes = null) {
1022
1107
  /**
1023
1108
  * Validates a JSON:API document for PATCH requests (partial resource updates)
1024
1109
  *
1025
- * Ensures PATCH payloads follow JSON:API spec for partial updates. Unlike PUT,
1026
- * PATCH allows sending only the fields/relationships that need updating.
1027
- * Requires at least one of 'attributes' or 'relationships' to be present.
1028
- * Like PUT, doesn't allow 'included' arrays.
1029
- *
1030
- * @param {Object} inputRecord - The JSON:API document to validate
1031
- * @param {Object} inputRecord.data - The partial resource representation
1110
+ * @param {Object} inputRecord - JSON:API document to validate
1111
+ * @param {Object} inputRecord.data - Partial resource representation
1032
1112
  * @param {string} inputRecord.data.type - Resource type (must match existing)
1033
1113
  * @param {string|number} inputRecord.data.id - Resource ID (required)
1034
1114
  * @param {Object} [inputRecord.data.attributes] - Attributes to update
1035
1115
  * @param {Object} [inputRecord.data.relationships] - Relationships to update
1036
- * @param {Object} scopes - The scopes proxy object to check if type exists
1116
+ * @param {Object} scopes - Scopes proxy to verify resource types exist
1037
1117
  * @returns {boolean} True if valid
1038
1118
  * @throws {RestApiValidationError|RestApiPayloadError} If validation fails
1039
1119
  *
1040
- * @example <caption>PATCH single attribute</caption>
1041
- * // Just update the title:
1120
+ * @example
1121
+ * // Input: Update single field
1042
1122
  * validatePatchPayload({
1043
1123
  * data: {
1044
1124
  * type: 'articles',
1045
1125
  * id: '123',
1046
1126
  * attributes: {
1047
- * title: 'New Title Only'
1048
- * // Other attributes remain unchanged
1127
+ * title: 'New Title Only' // Only title changes
1128
+ * // body, status, etc. remain unchanged
1049
1129
  * }
1050
1130
  * }
1051
1131
  * }, scopes);
1132
+ * // Output: true
1052
1133
  *
1053
- * @example <caption>PATCH multiple attributes</caption>
1054
- * // Update status and timestamp:
1134
+ * @example
1135
+ * // Input: Update multiple fields
1055
1136
  * validatePatchPayload({
1056
1137
  * data: {
1057
1138
  * type: 'articles',
@@ -1059,79 +1140,78 @@ export function validatePutPayload(inputRecord, scopes = null) {
1059
1140
  * attributes: {
1060
1141
  * status: 'published',
1061
1142
  * publishedAt: '2023-11-15T10:00:00Z'
1062
- * // title, body, etc. remain unchanged
1143
+ * // Other fields untouched
1063
1144
  * }
1064
1145
  * }
1065
1146
  * }, scopes);
1147
+ * // Output: true (partial update)
1066
1148
  *
1067
- * @example <caption>PATCH relationships only</caption>
1068
- * // Just change the author:
1149
+ * @example
1150
+ * // Input: Update relationships only
1069
1151
  * validatePatchPayload({
1070
1152
  * data: {
1071
1153
  * type: 'articles',
1072
1154
  * id: '123',
1073
1155
  * relationships: {
1074
1156
  * author: {
1075
- * data: { type: 'users', id: '456' }
1157
+ * data: { type: 'users', id: '456' } // Change author
1076
1158
  * }
1077
- * // tags, comments, etc. remain unchanged
1159
+ * // Other relationships unchanged
1078
1160
  * }
1079
1161
  * }
1080
1162
  * }, scopes);
1163
+ * // Output: true
1081
1164
  *
1082
- * @example <caption>PATCH to clear relationships</caption>
1083
- * // Remove featured image, clear all tags:
1165
+ * @example
1166
+ * // Input: Clear relationships
1084
1167
  * validatePatchPayload({
1085
1168
  * data: {
1086
1169
  * type: 'articles',
1087
1170
  * id: '123',
1088
1171
  * relationships: {
1089
1172
  * featuredImage: {
1090
- * data: null // Remove to-one relationship
1173
+ * data: null // Remove to-one
1091
1174
  * },
1092
1175
  * tags: {
1093
- * data: [] // Clear to-many relationship
1094
- * }
1095
- * }
1096
- * }
1097
- * }, scopes);
1098
- *
1099
- * @example <caption>PATCH both attributes and relationships</caption>
1100
- * // Complex update:
1101
- * validatePatchPayload({
1102
- * data: {
1103
- * type: 'articles',
1104
- * id: '123',
1105
- * attributes: {
1106
- * status: 'archived',
1107
- * archivedAt: '2023-11-15T15:30:00Z'
1108
- * },
1109
- * relationships: {
1110
- * archivedBy: {
1111
- * data: { type: 'users', id: '789' }
1176
+ * data: [] // Clear to-many
1112
1177
  * }
1113
1178
  * }
1114
1179
  * }
1115
1180
  * }, scopes);
1181
+ * // Output: true (clearing is valid)
1116
1182
  *
1117
- * @example <caption>Invalid - no changes specified</caption>
1118
- * // This throws RestApiValidationError:
1183
+ * @example
1184
+ * // Input: No changes specified
1119
1185
  * validatePatchPayload({
1120
1186
  * data: {
1121
1187
  * type: 'articles',
1122
1188
  * id: '123'
1123
- * // No attributes or relationships!
1189
+ * // Missing both attributes and relationships!
1124
1190
  * }
1125
1191
  * }, scopes);
1126
- * // Error: 'PATCH request "data" must have at least one of "attributes" or "relationships"'
1127
- *
1128
- * @example <caption>Why this is useful upstream</caption>
1129
- * // The REST API plugin uses PATCH validation to:
1130
- * // 1. Enable efficient partial updates (only send what changes)
1131
- * // 2. Reduce bandwidth for large resources
1132
- * // 3. Prevent race conditions (don't overwrite fields you didn't intend to)
1133
- * // 4. Support field-level permissions (some fields might be read-only)
1134
- * // 5. Maintain clear semantics vs PUT (PATCH = modify, PUT = replace)
1192
+ * // Throws: RestApiValidationError
1193
+ * // 'PATCH request "data" must have at least one of "attributes" or "relationships"'
1194
+ *
1195
+ * @description
1196
+ * Used by:
1197
+ * - rest-api-plugin.patch() method for partial updates
1198
+ * - Enforces PATCH semantics vs PUT
1199
+ *
1200
+ * Purpose:
1201
+ * - Enables efficient partial updates (send only changes)
1202
+ * - Requires at least one change (attributes or relationships)
1203
+ * - Prevents included resources (no side effects)
1204
+ * - Reduces bandwidth for large resources
1205
+ * - Prevents race conditions in concurrent updates
1206
+ *
1207
+ * Data flow:
1208
+ * 1. Validates document structure with data property
1209
+ * 2. Ensures no included array present
1210
+ * 3. Validates resource type exists in scopes
1211
+ * 4. Requires ID property for targeting
1212
+ * 5. Requires at least attributes or relationships
1213
+ * 6. Validates each if present
1214
+ * 7. Returns true or throws descriptive error
1135
1215
  */
1136
1216
  export function validatePatchPayload(inputRecord, scopes = null) {
1137
1217
  if (!inputRecord || typeof inputRecord !== 'object') {