json-rest-api 1.0.10 → 1.0.13
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/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +7 -11
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -48,7 +48,7 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
48
48
|
* // }
|
|
49
49
|
*
|
|
50
50
|
* @example
|
|
51
|
-
* // Input: Object with
|
|
51
|
+
* // Input: Object with relationship name (preferred way)
|
|
52
52
|
* const schema = {
|
|
53
53
|
* title: { type: 'string' },
|
|
54
54
|
* author_id: {
|
|
@@ -60,19 +60,19 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
60
60
|
* const input = {
|
|
61
61
|
* id: '456',
|
|
62
62
|
* title: 'Another Article',
|
|
63
|
-
*
|
|
63
|
+
* author: 789 // Using relationship name
|
|
64
64
|
* };
|
|
65
65
|
*
|
|
66
|
-
* // Output:
|
|
66
|
+
* // Output: Relationship becomes JSON:API relationship
|
|
67
67
|
* // {
|
|
68
68
|
* // data: {
|
|
69
69
|
* // type: 'articles',
|
|
70
70
|
* // id: '456',
|
|
71
71
|
* // attributes: {
|
|
72
|
-
* // title: 'Another Article' //
|
|
72
|
+
* // title: 'Another Article' // author removed from attributes
|
|
73
73
|
* // },
|
|
74
74
|
* // relationships: {
|
|
75
|
-
* // author: {
|
|
75
|
+
* // author: {
|
|
76
76
|
* // data: { type: 'users', id: '789' }
|
|
77
77
|
* // }
|
|
78
78
|
* // }
|
|
@@ -80,11 +80,11 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
80
80
|
* // }
|
|
81
81
|
*
|
|
82
82
|
* @example
|
|
83
|
-
* // Input: Object with relationship
|
|
83
|
+
* // Input: Object with relationship name (only supported way)
|
|
84
84
|
* const input = {
|
|
85
85
|
* id: '456',
|
|
86
86
|
* title: 'Another Article',
|
|
87
|
-
* author: 789 // Using
|
|
87
|
+
* author: 789 // Using relationship name
|
|
88
88
|
* };
|
|
89
89
|
* // Output: Same JSON:API structure as above
|
|
90
90
|
*
|
|
@@ -92,12 +92,11 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
92
92
|
* // Input: Many-to-many relationship
|
|
93
93
|
* const relationships = {
|
|
94
94
|
* tags: {
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* }
|
|
95
|
+
* type: 'manyToMany',
|
|
96
|
+
* through: 'article_tags',
|
|
97
|
+
* foreignKey: 'article_id',
|
|
98
|
+
* otherKey: 'tag_id',
|
|
99
|
+
* target: 'tags'
|
|
101
100
|
* }
|
|
102
101
|
* };
|
|
103
102
|
* const input = {
|
|
@@ -134,6 +133,57 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
134
133
|
* }
|
|
135
134
|
* };
|
|
136
135
|
* // Output: Returned unchanged (detected by data.type)
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* // Input: Polymorphic relationship with minimal object format
|
|
139
|
+
* const relationships = {
|
|
140
|
+
* commentable: {
|
|
141
|
+
* belongsToPolymorphic: {
|
|
142
|
+
* types: ['posts', 'videos'],
|
|
143
|
+
* typeField: 'commentable_type',
|
|
144
|
+
* idField: 'commentable_id'
|
|
145
|
+
* }
|
|
146
|
+
* }
|
|
147
|
+
* };
|
|
148
|
+
* const input = {
|
|
149
|
+
* text: 'Great content!',
|
|
150
|
+
* commentable: { id: '456', _type: 'posts' } // Minimal object format
|
|
151
|
+
* };
|
|
152
|
+
*
|
|
153
|
+
* // Output: Proper JSON:API structure for polymorphic
|
|
154
|
+
* // {
|
|
155
|
+
* // data: {
|
|
156
|
+
* // type: 'comments',
|
|
157
|
+
* // attributes: {
|
|
158
|
+
* // text: 'Great content!'
|
|
159
|
+
* // },
|
|
160
|
+
* // relationships: {
|
|
161
|
+
* // commentable: {
|
|
162
|
+
* // data: { type: 'posts', id: '456' }
|
|
163
|
+
* // }
|
|
164
|
+
* // }
|
|
165
|
+
* // }
|
|
166
|
+
* // }
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* // Input: Clearing polymorphic relationship
|
|
170
|
+
* const input = {
|
|
171
|
+
* text: 'Updated comment',
|
|
172
|
+
* commentable: null // Clear the relationship
|
|
173
|
+
* };
|
|
174
|
+
*
|
|
175
|
+
* // Output: Relationship set to null
|
|
176
|
+
* // {
|
|
177
|
+
* // data: {
|
|
178
|
+
* // type: 'comments',
|
|
179
|
+
* // attributes: {
|
|
180
|
+
* // text: 'Updated comment'
|
|
181
|
+
* // },
|
|
182
|
+
* // relationships: {
|
|
183
|
+
* // commentable: { data: null }
|
|
184
|
+
* // }
|
|
185
|
+
* // }
|
|
186
|
+
* // }
|
|
137
187
|
*
|
|
138
188
|
* @description
|
|
139
189
|
* Used by:
|
|
@@ -143,7 +193,8 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
143
193
|
* Purpose:
|
|
144
194
|
* - Provides developer-friendly plain object API
|
|
145
195
|
* - Automatically detects and converts relationships
|
|
146
|
-
* - Supports
|
|
196
|
+
* - Supports relationship alias notation
|
|
197
|
+
* - Supports polymorphic relationships with minimal object format
|
|
147
198
|
* - Maintains JSON:API compliance internally
|
|
148
199
|
* - Allows gradual migration from simple to JSON:API
|
|
149
200
|
*
|
|
@@ -152,7 +203,8 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
152
203
|
* 2. Extracts ID from input object
|
|
153
204
|
* 3. Processes belongsTo relationships from schema
|
|
154
205
|
* 4. Processes hasMany/manyToMany from relationships
|
|
155
|
-
* 5.
|
|
206
|
+
* 5. Processes polymorphic belongsTo from relationships
|
|
207
|
+
* 6. Remaining fields become attributes
|
|
156
208
|
* 6. Builds proper JSON:API document structure
|
|
157
209
|
*/
|
|
158
210
|
export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
@@ -181,31 +233,27 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
181
233
|
const relAlias = fieldDef.as || fieldName; // Use 'as' alias or foreign key name as default alias
|
|
182
234
|
let valueToProcess = undefined;
|
|
183
235
|
|
|
184
|
-
// Check if
|
|
185
|
-
if (
|
|
236
|
+
// Check if the user provided the foreign key name (e.g., country_id) - NO LONGER ALLOWED
|
|
237
|
+
if (tempInput[fieldName] !== undefined) {
|
|
186
238
|
throw new RestApiValidationError(
|
|
187
|
-
`
|
|
188
|
-
`Use
|
|
239
|
+
`Foreign key field '${fieldName}' is no longer supported for relationship input. ` +
|
|
240
|
+
`Use the relationship name '${fieldDef.as || fieldName}' instead.`,
|
|
189
241
|
{
|
|
190
|
-
fields: [fieldName
|
|
242
|
+
fields: [fieldName],
|
|
191
243
|
violations: [{
|
|
192
|
-
field:
|
|
193
|
-
rule: '
|
|
194
|
-
message: `
|
|
244
|
+
field: fieldName,
|
|
245
|
+
rule: 'deprecated_foreign_key',
|
|
246
|
+
message: `Use relationship name instead of foreign key field`
|
|
195
247
|
}]
|
|
196
248
|
}
|
|
197
249
|
);
|
|
198
250
|
}
|
|
199
|
-
|
|
200
|
-
// Check if the user provided the
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// Check if the user provided the 'as' alias (e.g., country)
|
|
206
|
-
else if (fieldDef.as && tempInput[fieldDef.as] !== undefined) {
|
|
207
|
-
valueToProcess = tempInput[fieldDef.as];
|
|
208
|
-
delete tempInput[fieldDef.as]; // Remove from tempInput once processed
|
|
251
|
+
|
|
252
|
+
// Check if the user provided the relationship name (e.g., country)
|
|
253
|
+
const relName = fieldDef.as || fieldName;
|
|
254
|
+
if (tempInput[relName] !== undefined) {
|
|
255
|
+
valueToProcess = tempInput[relName];
|
|
256
|
+
delete tempInput[relName]; // Remove from tempInput once processed
|
|
209
257
|
}
|
|
210
258
|
|
|
211
259
|
if (valueToProcess !== undefined) {
|
|
@@ -224,12 +272,21 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
224
272
|
const value = tempInput[relName];
|
|
225
273
|
delete tempInput[relName]; // Remove from tempInput once processed
|
|
226
274
|
|
|
227
|
-
if ((relConfig.hasMany || relConfig.manyToMany) && Array.isArray(value)) {
|
|
228
|
-
|
|
275
|
+
if ((relConfig.type === 'hasMany' || relConfig.type === 'manyToMany') && Array.isArray(value)) {
|
|
276
|
+
// Determine the target type based on relationship type:
|
|
277
|
+
// - For manyToMany: the targetType is the relationship name itself
|
|
278
|
+
// - For hasMany/hasOne: the targetType is specified in the target property
|
|
279
|
+
const targetType = relConfig.type === 'manyToMany' ? relName : relConfig.target;
|
|
229
280
|
relationshipsData[relName] = {
|
|
230
281
|
data: value.map(relId => ({ type: targetType, id: String(relId) }))
|
|
231
282
|
};
|
|
232
283
|
}
|
|
284
|
+
// Handle hasOne relationships
|
|
285
|
+
else if (relConfig.type === 'hasOne' && value !== undefined) {
|
|
286
|
+
relationshipsData[relName] = {
|
|
287
|
+
data: value ? { type: relConfig.target, id: String(value) } : null
|
|
288
|
+
};
|
|
289
|
+
}
|
|
233
290
|
// Handle single belongsTo relationships if they were also defined in 'relationships'
|
|
234
291
|
// (though typically they are defined in schema.belongsTo)
|
|
235
292
|
else if (relConfig.belongsTo && value !== undefined) {
|
|
@@ -237,6 +294,28 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
237
294
|
data: value ? { type: relConfig.belongsTo, id: String(value) } : null
|
|
238
295
|
};
|
|
239
296
|
}
|
|
297
|
+
// Handle polymorphic belongsTo relationships with minimal object format
|
|
298
|
+
else if (relConfig.belongsToPolymorphic && value !== undefined) {
|
|
299
|
+
if (value === null) {
|
|
300
|
+
relationshipsData[relName] = { data: null };
|
|
301
|
+
} else if (typeof value === 'object' && value !== null && value.id && value._type) {
|
|
302
|
+
relationshipsData[relName] = {
|
|
303
|
+
data: { type: value._type, id: String(value.id) }
|
|
304
|
+
};
|
|
305
|
+
} else {
|
|
306
|
+
throw new RestApiValidationError(
|
|
307
|
+
`Invalid format for polymorphic relationship '${relName}'. Expected { id: 'value', _type: 'type' } or null`,
|
|
308
|
+
{
|
|
309
|
+
fields: [relName],
|
|
310
|
+
violations: [{
|
|
311
|
+
field: relName,
|
|
312
|
+
rule: 'polymorphic_format',
|
|
313
|
+
message: 'Must be an object with id and _type properties, or null'
|
|
314
|
+
}]
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
240
319
|
}
|
|
241
320
|
}
|
|
242
321
|
|
|
@@ -390,7 +469,7 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
390
469
|
* // id: '75',
|
|
391
470
|
* // name: 'Jane Smith'
|
|
392
471
|
* // },
|
|
393
|
-
* //
|
|
472
|
+
* // chapters: [{ id: '1001' }, { id: '1002' }],
|
|
394
473
|
* // chapters: [ // Expanded array
|
|
395
474
|
* // { id: '1001', number: 1, title: 'Introduction' },
|
|
396
475
|
* // { id: '1002', number: 2, title: 'Basics' }
|
|
@@ -404,7 +483,7 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
404
483
|
*
|
|
405
484
|
* Purpose:
|
|
406
485
|
* - Provides cleaner response format for clients
|
|
407
|
-
* -
|
|
486
|
+
* - Creates minimal relationship objects from relationships
|
|
408
487
|
* - Expands included resources for convenience
|
|
409
488
|
* - Maintains compatibility with collection metadata
|
|
410
489
|
* - Handles both single and array responses
|
|
@@ -497,13 +576,13 @@ export const transformJsonApiToSimplified = (scope, deps) => {
|
|
|
497
576
|
* { context: { schemaStructure: schema } }
|
|
498
577
|
* );
|
|
499
578
|
*
|
|
500
|
-
* // Output:
|
|
579
|
+
* // Output: Minimal relationship objects
|
|
501
580
|
* // {
|
|
502
581
|
* // id: '100',
|
|
503
582
|
* // text: 'Great article!',
|
|
504
583
|
* // created_at: '2024-01-15',
|
|
505
|
-
* //
|
|
506
|
-
* //
|
|
584
|
+
* // author: { id: '50' }, // Minimal object from relationships.author
|
|
585
|
+
* // article: { id: '200' } // Minimal object from relationships.article
|
|
507
586
|
* // }
|
|
508
587
|
*
|
|
509
588
|
* @example
|
|
@@ -526,11 +605,73 @@ export const transformJsonApiToSimplified = (scope, deps) => {
|
|
|
526
605
|
* tags: { manyToMany: { via: 'article_tags' } }
|
|
527
606
|
* };
|
|
528
607
|
*
|
|
529
|
-
* // Output:
|
|
608
|
+
* // Output: Minimal relationship objects
|
|
530
609
|
* // {
|
|
531
610
|
* // id: '300',
|
|
532
611
|
* // title: 'My Article',
|
|
533
|
-
* //
|
|
612
|
+
* // tags: [{ id: '1' }, { id: '2' }, { id: '3' }] // Minimal objects
|
|
613
|
+
* // }
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* // Input: Polymorphic relationship
|
|
617
|
+
* const data = {
|
|
618
|
+
* type: 'reviews',
|
|
619
|
+
* id: '500',
|
|
620
|
+
* attributes: { rating: 5, comment: 'Great!' },
|
|
621
|
+
* relationships: {
|
|
622
|
+
* reviewable: {
|
|
623
|
+
* data: { type: 'authors', id: '75' }
|
|
624
|
+
* }
|
|
625
|
+
* }
|
|
626
|
+
* };
|
|
627
|
+
* const relationships = {
|
|
628
|
+
* reviewable: {
|
|
629
|
+
* belongsToPolymorphic: {
|
|
630
|
+
* types: ['authors', 'publishers'],
|
|
631
|
+
* typeField: 'reviewable_type',
|
|
632
|
+
* idField: 'reviewable_id'
|
|
633
|
+
* }
|
|
634
|
+
* }
|
|
635
|
+
* };
|
|
636
|
+
*
|
|
637
|
+
* // Output: Minimal polymorphic object with type
|
|
638
|
+
* // {
|
|
639
|
+
* // id: '500',
|
|
640
|
+
* // rating: 5,
|
|
641
|
+
* // comment: 'Great!',
|
|
642
|
+
* // reviewable: { id: '75', type: 'authors' } // Minimal object with type
|
|
643
|
+
* // }
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* // Input: Polymorphic relationship with included resource
|
|
647
|
+
* const dataWithInclude = {
|
|
648
|
+
* type: 'reviews',
|
|
649
|
+
* id: '500',
|
|
650
|
+
* attributes: { rating: 5, comment: 'Great!' },
|
|
651
|
+
* relationships: {
|
|
652
|
+
* reviewable: {
|
|
653
|
+
* data: { type: 'authors', id: '75' }
|
|
654
|
+
* }
|
|
655
|
+
* }
|
|
656
|
+
* };
|
|
657
|
+
* const included = [
|
|
658
|
+
* {
|
|
659
|
+
* type: 'authors',
|
|
660
|
+
* id: '75',
|
|
661
|
+
* attributes: { name: 'Jane Smith', bio: 'Fantastic author' }
|
|
662
|
+
* }
|
|
663
|
+
* ];
|
|
664
|
+
*
|
|
665
|
+
* // Output: Full polymorphic object (type field removed as redundant)
|
|
666
|
+
* // {
|
|
667
|
+
* // id: '500',
|
|
668
|
+
* // rating: 5,
|
|
669
|
+
* // comment: 'Great!',
|
|
670
|
+
* // reviewable: { // Full object when included
|
|
671
|
+
* // id: '75',
|
|
672
|
+
* // name: 'Jane Smith',
|
|
673
|
+
* // bio: 'Fantastic author'
|
|
674
|
+
* // }
|
|
534
675
|
* // }
|
|
535
676
|
*
|
|
536
677
|
* @example
|
|
@@ -574,17 +715,15 @@ export const transformJsonApiToSimplified = (scope, deps) => {
|
|
|
574
715
|
* }
|
|
575
716
|
* ];
|
|
576
717
|
*
|
|
577
|
-
* // Output: Recursively expanded
|
|
718
|
+
* // Output: Recursively expanded
|
|
578
719
|
* // {
|
|
579
720
|
* // id: '400',
|
|
580
721
|
* // title: 'JavaScript Guide',
|
|
581
|
-
* // author_id: '75',
|
|
582
722
|
* // author: { // Expanded and transformed
|
|
583
723
|
* // id: '75',
|
|
584
724
|
* // name: 'Jane Smith',
|
|
585
|
-
* //
|
|
725
|
+
* // company: { id: '5' } // Author's relationships processed as minimal objects!
|
|
586
726
|
* // },
|
|
587
|
-
* // chapters_ids: ['1001', '1002'],
|
|
588
727
|
* // chapters: [
|
|
589
728
|
* // { id: '1001', number: 1, title: 'Introduction' },
|
|
590
729
|
* // { id: '1002', number: 2, title: 'Basics' }
|
|
@@ -598,17 +737,17 @@ export const transformJsonApiToSimplified = (scope, deps) => {
|
|
|
598
737
|
*
|
|
599
738
|
* Purpose:
|
|
600
739
|
* - Core transformation logic for single resources
|
|
601
|
-
* -
|
|
602
|
-
* - Creates
|
|
740
|
+
* - Creates minimal relationship objects from relationships
|
|
741
|
+
* - Creates arrays of minimal relationship objects for to-many relationships
|
|
603
742
|
* - Recursively expands and transforms included data
|
|
604
743
|
* - Handles polymorphic relationships
|
|
605
744
|
*
|
|
606
745
|
* Data flow:
|
|
607
746
|
* 1. Copies ID and attributes to result
|
|
608
747
|
* 2. Processes each relationship:
|
|
609
|
-
* - BelongsTo:
|
|
610
|
-
* - Polymorphic:
|
|
611
|
-
* - HasMany/ManyToMany: creates
|
|
748
|
+
* - BelongsTo: creates minimal relationship object
|
|
749
|
+
* - Polymorphic: creates minimal relationship object with type
|
|
750
|
+
* - HasMany/ManyToMany: creates array of minimal relationship objects
|
|
612
751
|
* 3. If included data exists:
|
|
613
752
|
* - Finds matching included resources
|
|
614
753
|
* - Recursively transforms them (preserving their relationships!)
|
|
@@ -634,7 +773,7 @@ export const transformSingleJsonApiToSimplified = (scope, deps) => {
|
|
|
634
773
|
// Add attributes
|
|
635
774
|
Object.assign(simplified, data.attributes || {});
|
|
636
775
|
|
|
637
|
-
//
|
|
776
|
+
// Process relationships to create minimal objects
|
|
638
777
|
if (data.relationships) {
|
|
639
778
|
for (const [relName, relData] of Object.entries(data.relationships)) {
|
|
640
779
|
// Find schema field for this belongsTo relationship
|
|
@@ -643,23 +782,22 @@ export const transformSingleJsonApiToSimplified = (scope, deps) => {
|
|
|
643
782
|
if (schemaEntry) {
|
|
644
783
|
const [fieldName, fieldDef] = schemaEntry;
|
|
645
784
|
if (fieldDef.belongsTo && relData.data) {
|
|
646
|
-
|
|
785
|
+
// Create minimal relationship object (no more foreign key field)
|
|
786
|
+
simplified[relName] = { id: relData.data.id };
|
|
647
787
|
}
|
|
648
788
|
}
|
|
649
789
|
|
|
650
790
|
// Check if this is a polymorphic relationship from the relationships config
|
|
651
791
|
const rel = relationships?.[relName];
|
|
652
792
|
if (rel?.belongsToPolymorphic && relData.data) {
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
simplified[typeField] = relData.data.type;
|
|
656
|
-
simplified[idField] = relData.data.id;
|
|
793
|
+
// Create minimal relationship object for polymorphic (no more type/id fields)
|
|
794
|
+
simplified[relName] = { id: relData.data.id, _type: relData.data.type };
|
|
657
795
|
}
|
|
658
796
|
|
|
659
|
-
// Handle
|
|
660
|
-
if (rel?.hasMany || rel?.manyToMany) {
|
|
797
|
+
// Handle to-many relationships (create minimal objects with just IDs)
|
|
798
|
+
if (rel?.type === 'hasMany' || rel?.type === 'manyToMany') {
|
|
661
799
|
if (relData.data && Array.isArray(relData.data)) {
|
|
662
|
-
simplified[
|
|
800
|
+
simplified[relName] = relData.data.map(item => ({ id: item.id }));
|
|
663
801
|
}
|
|
664
802
|
}
|
|
665
803
|
|
|
@@ -681,6 +819,10 @@ export const transformSingleJsonApiToSimplified = (scope, deps) => {
|
|
|
681
819
|
scopes
|
|
682
820
|
} }
|
|
683
821
|
);
|
|
822
|
+
// For polymorphic relationships, add _type field to identify the resource type
|
|
823
|
+
if (rel?.belongsToPolymorphic) {
|
|
824
|
+
itemSimplified._type = item.type;
|
|
825
|
+
}
|
|
684
826
|
return itemSimplified;
|
|
685
827
|
});
|
|
686
828
|
}
|
|
@@ -688,7 +830,7 @@ export const transformSingleJsonApiToSimplified = (scope, deps) => {
|
|
|
688
830
|
const nestedData = findIncluded(relData.data);
|
|
689
831
|
if (nestedData) {
|
|
690
832
|
// Transform the single included record to restore its relationship fields
|
|
691
|
-
|
|
833
|
+
const itemSimplified = transformSingleJsonApiToSimplified(
|
|
692
834
|
{ data: nestedData, included: null },
|
|
693
835
|
{ context: {
|
|
694
836
|
schemaStructure: scopes?.[nestedData.type]?.vars?.schemaInfo?.schema || {},
|
|
@@ -696,6 +838,11 @@ export const transformSingleJsonApiToSimplified = (scope, deps) => {
|
|
|
696
838
|
scopes
|
|
697
839
|
} }
|
|
698
840
|
);
|
|
841
|
+
// For polymorphic relationships, add _type field to identify the resource type
|
|
842
|
+
if (rel?.belongsToPolymorphic) {
|
|
843
|
+
itemSimplified._type = nestedData.type;
|
|
844
|
+
}
|
|
845
|
+
simplified[relName] = itemSimplified;
|
|
699
846
|
}
|
|
700
847
|
}
|
|
701
848
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const passthrough = (value) => value;
|
|
2
|
+
const identityTranslate = (_field, value) => value;
|
|
3
|
+
const identityScope = (query) => query;
|
|
4
|
+
|
|
5
|
+
const selectColumnsOnBuilder = (builder, columns) => {
|
|
6
|
+
if (!columns) return builder;
|
|
7
|
+
if (Array.isArray(columns)) {
|
|
8
|
+
columns.forEach((column) => builder.select(column));
|
|
9
|
+
} else if (typeof columns === 'object') {
|
|
10
|
+
builder.select(columns);
|
|
11
|
+
}
|
|
12
|
+
return builder;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const normalizeArray = (value, normalizeFn) => value.map((entry) => normalizeFn(entry));
|
|
16
|
+
|
|
17
|
+
const normalizeBelongsToValue = (value) => {
|
|
18
|
+
if (value === null || value === undefined) return value;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return normalizeArray(value, normalizeBelongsToValue);
|
|
21
|
+
}
|
|
22
|
+
return String(value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const baseAdapter = ({
|
|
26
|
+
knex,
|
|
27
|
+
tableName,
|
|
28
|
+
idColumn,
|
|
29
|
+
translateColumn,
|
|
30
|
+
translateFilterValue = identityTranslate,
|
|
31
|
+
applyResourceScope = identityScope,
|
|
32
|
+
isCanonical = false,
|
|
33
|
+
}) => {
|
|
34
|
+
const buildBaseQuery = ({ transaction } = {}) => {
|
|
35
|
+
const query = (transaction || knex)(tableName);
|
|
36
|
+
return applyResourceScope(query);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
isCanonical: () => isCanonical,
|
|
41
|
+
getTableName: () => tableName,
|
|
42
|
+
getIdColumn: () => idColumn,
|
|
43
|
+
translateColumn,
|
|
44
|
+
translateFilterValue,
|
|
45
|
+
applyResourceScope,
|
|
46
|
+
buildBaseQuery,
|
|
47
|
+
selectColumns: (builder, columns) => selectColumnsOnBuilder(builder, columns),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const createLegacyAdapter = ({ knex, schemaInfo }) => {
|
|
52
|
+
const tableName = schemaInfo.tableName;
|
|
53
|
+
const idColumn = schemaInfo.idProperty || 'id';
|
|
54
|
+
|
|
55
|
+
const translateColumn = (column) => column;
|
|
56
|
+
|
|
57
|
+
return baseAdapter({
|
|
58
|
+
knex,
|
|
59
|
+
tableName,
|
|
60
|
+
idColumn,
|
|
61
|
+
translateColumn,
|
|
62
|
+
translateFilterValue: identityTranslate,
|
|
63
|
+
applyResourceScope: identityScope,
|
|
64
|
+
isCanonical: false,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
69
|
+
const descriptor = schemaInfo.descriptor || {};
|
|
70
|
+
const canonical = descriptor.canonical || {};
|
|
71
|
+
const canonicalFieldMap = descriptor.canonicalFieldMap || {};
|
|
72
|
+
const fieldsInfo = descriptor.fields || {};
|
|
73
|
+
const belongsToInfo = descriptor.belongsTo || {};
|
|
74
|
+
|
|
75
|
+
const translateColumn = (field) => {
|
|
76
|
+
if (!field) return field;
|
|
77
|
+
if (field === 'id') return 'id';
|
|
78
|
+
|
|
79
|
+
const canonicalEntry = canonicalFieldMap[field];
|
|
80
|
+
if (typeof canonicalEntry === 'string') {
|
|
81
|
+
return canonicalEntry;
|
|
82
|
+
}
|
|
83
|
+
if (canonicalEntry && typeof canonicalEntry === 'object') {
|
|
84
|
+
if (canonicalEntry.slot) return canonicalEntry.slot;
|
|
85
|
+
if (canonicalEntry.slotColumn) return canonicalEntry.slotColumn;
|
|
86
|
+
if (canonicalEntry.idSlot) return canonicalEntry.idSlot;
|
|
87
|
+
if (canonicalEntry.typeSlot && field.endsWith('_type')) return canonicalEntry.typeSlot;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!canonicalEntry && field.endsWith('_id')) {
|
|
91
|
+
const alias = field.slice(0, -3);
|
|
92
|
+
const aliasEntry = canonicalFieldMap[alias];
|
|
93
|
+
if (typeof aliasEntry === 'string') return aliasEntry;
|
|
94
|
+
if (aliasEntry?.idSlot) return aliasEntry.idSlot;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!canonicalEntry && field.endsWith('_type')) {
|
|
98
|
+
const alias = field.slice(0, -5);
|
|
99
|
+
const aliasEntry = canonicalFieldMap[alias];
|
|
100
|
+
if (aliasEntry?.typeSlot) return aliasEntry.typeSlot;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fieldInfo = fieldsInfo[field];
|
|
104
|
+
if (fieldInfo?.slot) return fieldInfo.slot;
|
|
105
|
+
|
|
106
|
+
const belongsInfo = belongsToInfo[field];
|
|
107
|
+
if (belongsInfo?.idColumn) return belongsInfo.idColumn;
|
|
108
|
+
|
|
109
|
+
return field;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const translateFilterValue = (field, value) => {
|
|
113
|
+
if (value === null || value === undefined) return value;
|
|
114
|
+
|
|
115
|
+
const searchField = schemaInfo.searchSchemaStructure?.[field];
|
|
116
|
+
const schemaField = schemaInfo.schemaStructure?.[field];
|
|
117
|
+
const isRelationship = Boolean(
|
|
118
|
+
searchField?.isRelationship
|
|
119
|
+
|| schemaField?.belongsTo
|
|
120
|
+
|| schemaField?.belongsToPolymorphic
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (!isRelationship) {
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
return normalizeArray(value, passthrough);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return normalizeBelongsToValue(value);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const applyResourceScope = (query) => {
|
|
134
|
+
return query
|
|
135
|
+
.where(canonical.tenantColumn, descriptor.tenant)
|
|
136
|
+
.where(canonical.resourceColumn, descriptor.resource);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return baseAdapter({
|
|
140
|
+
knex,
|
|
141
|
+
tableName: canonical.tableName,
|
|
142
|
+
idColumn: 'id',
|
|
143
|
+
translateColumn,
|
|
144
|
+
translateFilterValue,
|
|
145
|
+
applyResourceScope,
|
|
146
|
+
isCanonical: true,
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const createStorageAdapter = ({ knex, schemaInfo }) => {
|
|
151
|
+
if (!schemaInfo) {
|
|
152
|
+
throw new Error('createStorageAdapter requires schemaInfo');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (schemaInfo?.descriptor?.canonical?.tableName) {
|
|
156
|
+
return createCanonicalAdapter({ knex, schemaInfo });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return createLegacyAdapter({ knex, schemaInfo });
|
|
160
|
+
};
|
|
@@ -90,7 +90,7 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
|
|
|
90
90
|
*
|
|
91
91
|
* // Schema relationships has:
|
|
92
92
|
* // tags: {
|
|
93
|
-
* //
|
|
93
|
+
* // type: 'manyToMany',
|
|
94
94
|
* // through: 'article_tags',
|
|
95
95
|
* // foreignKey: 'article_id',
|
|
96
96
|
* // otherKey: 'tag_id'
|
|
@@ -226,16 +226,8 @@ export const processRelationships = (scope, deps) => {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// Check
|
|
230
|
-
if (relDef?.manyToMany && relData.data !== undefined) {
|
|
231
|
-
manyToManyRelationships.push({
|
|
232
|
-
relName,
|
|
233
|
-
relDef: relDef.manyToMany, // Pass the manyToMany object, not the whole relationship
|
|
234
|
-
relData: relData.data || [] // null means empty array for many-to-many
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
// Also check for hasMany with through (alternative many-to-many syntax)
|
|
238
|
-
else if (relDef?.hasMany && relDef?.through && relData.data !== undefined) {
|
|
229
|
+
// Check relationship type and process accordingly
|
|
230
|
+
if (relDef?.type === 'manyToMany' && relData.data !== undefined) {
|
|
239
231
|
manyToManyRelationships.push({
|
|
240
232
|
relName,
|
|
241
233
|
relDef: {
|
|
@@ -246,6 +238,7 @@ export const processRelationships = (scope, deps) => {
|
|
|
246
238
|
relData: relData.data || [] // null means empty array for many-to-many
|
|
247
239
|
});
|
|
248
240
|
}
|
|
241
|
+
// Note: hasOne and hasMany don't need processing here as they don't update the current record
|
|
249
242
|
}
|
|
250
243
|
|
|
251
244
|
return { belongsToUpdates, manyToManyRelationships };
|