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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +90 -32
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RestApiValidationError, RestApiPayloadError } from '
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
|
54
|
-
* //
|
|
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
|
|
62
|
-
* //
|
|
71
|
+
* @example
|
|
72
|
+
* // Input: Missing type field
|
|
63
73
|
* validateResourceIdentifier(
|
|
64
|
-
* { id: '123' },
|
|
74
|
+
* { id: '123' },
|
|
65
75
|
* "Relationship 'comments'",
|
|
66
76
|
* scopes
|
|
67
77
|
* );
|
|
68
|
-
* //
|
|
78
|
+
* // Throws: RestApiPayloadError
|
|
79
|
+
* // "Relationship 'comments': Resource identifier must have a non-empty 'type' string"
|
|
69
80
|
*
|
|
70
|
-
* @example
|
|
71
|
-
* //
|
|
81
|
+
* @example
|
|
82
|
+
* // Input: Unknown resource type
|
|
72
83
|
* validateResourceIdentifier(
|
|
73
|
-
* { type: '
|
|
84
|
+
* { type: 'unicorns', id: '456' },
|
|
74
85
|
* "Included resource",
|
|
75
|
-
* scopes
|
|
86
|
+
* scopes // scopes['unicorns'] doesn't exist
|
|
76
87
|
* );
|
|
77
|
-
* //
|
|
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
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
|
138
|
-
* //
|
|
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
|
|
146
|
-
* //
|
|
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
|
|
159
|
-
* //
|
|
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
|
|
167
|
-
* //
|
|
188
|
+
* @example
|
|
189
|
+
* // Input: Missing data property
|
|
168
190
|
* validateRelationship(
|
|
169
|
-
* {
|
|
170
|
-
* '
|
|
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
|
-
*
|
|
220
|
-
*
|
|
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
|
|
232
|
-
* //
|
|
264
|
+
* @example
|
|
265
|
+
* // Input: Basic GET request
|
|
233
266
|
* validateGetPayload({
|
|
234
267
|
* id: '123'
|
|
235
268
|
* });
|
|
269
|
+
* // Output: true
|
|
236
270
|
*
|
|
237
|
-
* @example
|
|
238
|
-
* //
|
|
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
|
|
247
|
-
* //
|
|
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
|
|
260
|
-
* //
|
|
295
|
+
* @example
|
|
296
|
+
* // Input: Missing ID parameter
|
|
261
297
|
* validateGetPayload({
|
|
262
298
|
* queryParams: { include: ['author'] }
|
|
263
299
|
* });
|
|
264
|
-
* //
|
|
300
|
+
* // Throws: RestApiValidationError
|
|
301
|
+
* // "GET request must include an id parameter"
|
|
265
302
|
*
|
|
266
|
-
* @example
|
|
267
|
-
* //
|
|
303
|
+
* @example
|
|
304
|
+
* // Input: Invalid field specification
|
|
268
305
|
* validateGetPayload({
|
|
269
|
-
* id:
|
|
306
|
+
* id: '123',
|
|
307
|
+
* queryParams: {
|
|
308
|
+
* fields: {
|
|
309
|
+
* articles: ['title', 'body'] // Should be string, not array
|
|
310
|
+
* }
|
|
311
|
+
* }
|
|
270
312
|
* });
|
|
271
|
-
* //
|
|
313
|
+
* // Throws: RestApiPayloadError
|
|
314
|
+
* // "queryParams.fields['articles'] must be a comma-separated string"
|
|
272
315
|
*
|
|
273
|
-
* @
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
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
|
-
*
|
|
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 -
|
|
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
|
|
377
|
-
* //
|
|
429
|
+
* @example
|
|
430
|
+
* // Input: Empty query (fetch all)
|
|
378
431
|
* validateQueryPayload({}, ['title', 'createdAt']);
|
|
432
|
+
* // Output: true
|
|
379
433
|
*
|
|
380
|
-
* @example
|
|
381
|
-
* //
|
|
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'
|
|
440
|
+
* 'author.name': 'John Doe' // Dot notation for relationships
|
|
387
441
|
* },
|
|
388
|
-
* sort: ['-publishedAt', 'title']
|
|
442
|
+
* sort: ['-publishedAt', 'title'] // DESC publishedAt, ASC title
|
|
389
443
|
* }
|
|
390
444
|
* }, ['publishedAt', 'title', 'createdAt']);
|
|
445
|
+
* // Output: true
|
|
391
446
|
*
|
|
392
|
-
* @example
|
|
393
|
-
* //
|
|
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
|
|
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
|
|
408
|
-
* //
|
|
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
|
|
427
|
-
* //
|
|
485
|
+
* @example
|
|
486
|
+
* // Input: Invalid sort field
|
|
428
487
|
* validateQueryPayload({
|
|
429
488
|
* queryParams: {
|
|
430
|
-
* sort: ['
|
|
489
|
+
* sort: ['password'] // Not in sortableFields
|
|
431
490
|
* }
|
|
432
491
|
* }, ['title', 'createdAt']);
|
|
433
|
-
* //
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
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
|
-
*
|
|
587
|
-
*
|
|
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 -
|
|
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
|
|
603
|
-
* //
|
|
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
|
|
615
|
-
* // Create
|
|
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
|
|
637
|
-
* //
|
|
705
|
+
* @example
|
|
706
|
+
* // Input: Client-generated ID
|
|
638
707
|
* validatePostPayload({
|
|
639
708
|
* data: {
|
|
640
709
|
* type: 'articles',
|
|
641
|
-
* id: 'article-2023-11-15
|
|
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
|
|
649
|
-
* //
|
|
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
|
|
673
|
-
* //
|
|
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
|
-
* //
|
|
750
|
+
* // Throws: RestApiPayloadError
|
|
751
|
+
* // 'POST request "data" must have a non-empty "type" string'
|
|
680
752
|
*
|
|
681
|
-
* @
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
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
|
-
*
|
|
836
|
-
*
|
|
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 -
|
|
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
|
|
851
|
-
* //
|
|
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
|
|
865
|
-
* // Replace
|
|
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' }
|
|
946
|
+
* data: { type: 'users', id: '99' } // New author
|
|
877
947
|
* },
|
|
878
948
|
* tags: {
|
|
879
|
-
* data: [
|
|
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
|
|
956
|
+
* data: null // Remove relationship
|
|
887
957
|
* }
|
|
888
958
|
* }
|
|
889
959
|
* }
|
|
890
960
|
* }, scopes);
|
|
961
|
+
* // Output: true (complete replacement)
|
|
891
962
|
*
|
|
892
|
-
* @example
|
|
893
|
-
* //
|
|
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
|
-
* //
|
|
971
|
+
* // Throws: RestApiPayloadError
|
|
972
|
+
* // 'PUT request "data" must have an "id" property'
|
|
901
973
|
*
|
|
902
|
-
* @example
|
|
903
|
-
* //
|
|
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: [
|
|
911
|
-
* { type: 'users', id: '1'
|
|
982
|
+
* included: [ // Not allowed!
|
|
983
|
+
* { type: 'users', id: '1' }
|
|
912
984
|
* ]
|
|
913
985
|
* }, scopes);
|
|
914
|
-
* //
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
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
|
-
*
|
|
1026
|
-
*
|
|
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 -
|
|
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
|
|
1041
|
-
* //
|
|
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
|
-
* //
|
|
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
|
|
1054
|
-
* // Update
|
|
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
|
-
* //
|
|
1143
|
+
* // Other fields untouched
|
|
1063
1144
|
* }
|
|
1064
1145
|
* }
|
|
1065
1146
|
* }, scopes);
|
|
1147
|
+
* // Output: true (partial update)
|
|
1066
1148
|
*
|
|
1067
|
-
* @example
|
|
1068
|
-
* //
|
|
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
|
-
* //
|
|
1159
|
+
* // Other relationships unchanged
|
|
1078
1160
|
* }
|
|
1079
1161
|
* }
|
|
1080
1162
|
* }, scopes);
|
|
1163
|
+
* // Output: true
|
|
1081
1164
|
*
|
|
1082
|
-
* @example
|
|
1083
|
-
* //
|
|
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
|
|
1173
|
+
* data: null // Remove to-one
|
|
1091
1174
|
* },
|
|
1092
1175
|
* tags: {
|
|
1093
|
-
* data: []
|
|
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
|
|
1118
|
-
* //
|
|
1183
|
+
* @example
|
|
1184
|
+
* // Input: No changes specified
|
|
1119
1185
|
* validatePatchPayload({
|
|
1120
1186
|
* data: {
|
|
1121
1187
|
* type: 'articles',
|
|
1122
1188
|
* id: '123'
|
|
1123
|
-
* //
|
|
1189
|
+
* // Missing both attributes and relationships!
|
|
1124
1190
|
* }
|
|
1125
1191
|
* }, scopes);
|
|
1126
|
-
* //
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
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') {
|