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.
Files changed (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +7 -11
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. 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 foreign key (traditional way)
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
- * author_id: 789 // Using foreign key name
63
+ * author: 789 // Using relationship name
64
64
  * };
65
65
  *
66
- * // Output: Foreign key becomes relationship
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' // author_id removed
72
+ * // title: 'Another Article' // author removed from attributes
73
73
  * // },
74
74
  * // relationships: {
75
- * // author: { // Uses 'as' name
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 alias (preferred way)
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 'as' alias directly
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
- * manyToMany: {
96
- * through: 'article_tags',
97
- * foreignKey: 'article_id',
98
- * otherKey: 'tag_id',
99
- * otherType: 'tags'
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 both foreign key and alias notation
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. Remaining fields become attributes
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 both foreign key and alias are provided - this is an error
185
- if (fieldDef.as && tempInput[fieldName] !== undefined && tempInput[fieldDef.as] !== undefined) {
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
- `Cannot specify both '${fieldName}' and '${fieldDef.as}' for the same relationship. ` +
188
- `Use either '${fieldName}' (foreign key) or '${fieldDef.as}' (relationship name), not both.`,
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, fieldDef.as],
242
+ fields: [fieldName],
191
243
  violations: [{
192
- field: 'input',
193
- rule: 'duplicate_relationship_specification',
194
- message: `Both '${fieldName}' and '${fieldDef.as}' were provided for the same relationship`
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 foreign key name (e.g., country_id)
201
- if (tempInput[fieldName] !== undefined) {
202
- valueToProcess = tempInput[fieldName];
203
- delete tempInput[fieldName]; // Remove from tempInput once processed
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
- const targetType = relConfig.hasMany || relConfig.manyToMany?.otherType || relConfig.through;
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
- * // chapters_ids: ['1001', '1002'],
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
- * - Restores foreign keys from relationships
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: Flattened with foreign keys restored
579
+ * // Output: Minimal relationship objects
501
580
  * // {
502
581
  * // id: '100',
503
582
  * // text: 'Great article!',
504
583
  * // created_at: '2024-01-15',
505
- * // author_id: '50', // From relationships.author
506
- * // article_id: '200' // From relationships.article
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: Array of IDs with _ids suffix
608
+ * // Output: Minimal relationship objects
530
609
  * // {
531
610
  * // id: '300',
532
611
  * // title: 'My Article',
533
- * // tags_ids: ['1', '2', '3'] // Relationship name + _ids
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 (note author.company_id)
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
- * // company_id: '5' // Author's relationships processed!
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
- * - Restores foreign keys from relationships
602
- * - Creates _ids arrays for to-many relationships
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: restores foreign key field
610
- * - Polymorphic: restores type and ID fields
611
- * - HasMany/ManyToMany: creates _ids array
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
- // Extract foreign keys from relationships
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
- simplified[fieldName] = relData.data.id;
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
- // For polymorphic relationships, restore both the type and id fields
654
- const { typeField, idField } = rel.belongsToPolymorphic;
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 many-to-many (just IDs, not nested)
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[`${relName}_ids`] = relData.data.map(item => item.id);
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
- simplified[relName] = transformSingleJsonApiToSimplified(
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
- * // hasMany: 'tags',
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 for many-to-many relationships defined in relationships object
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 };