json-rest-api 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +99 -33
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,110 @@
1
+ import { getSchemaStructure } from '../querying-writing/knex-constants.js';
2
+
3
+ /**
4
+ * Processes belongsTo relationships from JSON:API input to foreign key updates
5
+ *
6
+ * @param {Object} scope - The scope object containing schema info
7
+ * @param {Object} deps - Dependencies object containing context with inputRecord
8
+ * @returns {Object} Object mapping foreign key fields to their values
9
+ *
10
+ * @example
11
+ * // Input: JSON:API document with relationships
12
+ * const inputRecord = {
13
+ * data: {
14
+ * type: 'articles',
15
+ * attributes: { title: 'My Article' },
16
+ * relationships: {
17
+ * author: {
18
+ * data: { type: 'users', id: '123' }
19
+ * },
20
+ * category: {
21
+ * data: { type: 'categories', id: '456' }
22
+ * }
23
+ * }
24
+ * }
25
+ * };
26
+ *
27
+ * // Schema has: author_id field with belongsTo: 'users', as: 'author'
28
+ * // Schema has: category_id field with belongsTo: 'categories', as: 'category'
29
+ *
30
+ * const updates = processBelongsToRelationships(scope, { context: { inputRecord } });
31
+ *
32
+ * // Output: Foreign key values extracted
33
+ * // {
34
+ * // author_id: '123',
35
+ * // category_id: '456'
36
+ * // }
37
+ *
38
+ * @example
39
+ * // Input: Removing a relationship (setting to null)
40
+ * const inputRecord = {
41
+ * data: {
42
+ * type: 'articles',
43
+ * id: '1',
44
+ * relationships: {
45
+ * author: {
46
+ * data: null // Remove author
47
+ * }
48
+ * }
49
+ * }
50
+ * };
51
+ *
52
+ * const updates = processBelongsToRelationships(scope, { context: { inputRecord } });
53
+ *
54
+ * // Output: Foreign key set to null
55
+ * // {
56
+ * // author_id: null
57
+ * // }
58
+ *
59
+ * @description
60
+ * Used by:
61
+ * - rest-api-knex-plugin's dataPut and dataPatch methods
62
+ * - Called before saving to extract foreign keys from JSON:API relationships
63
+ * - Works in conjunction with relationship-processor for complete handling
64
+ *
65
+ * Purpose:
66
+ * - Converts JSON:API relationship format to database foreign keys
67
+ * - Maps from relationship names (e.g., 'author') to field names (e.g., 'author_id')
68
+ * - Handles both setting relationships (with id) and removing them (null)
69
+ * - Only processes belongsTo relationships, not hasMany
70
+ *
71
+ * Data flow:
72
+ * 1. Receives JSON:API document with relationships section
73
+ * 2. Looks up schema to find foreign key fields
74
+ * 3. For each belongsTo field, checks if relationship exists in input
75
+ * 4. Extracts id from relationship data or sets null
76
+ * 5. Returns object ready to merge with attributes for database update
77
+ */
78
+ export const processBelongsToRelationships = (scope, deps) => {
79
+ const foreignKeyUpdates = {};
80
+
81
+ // Extract values from deps
82
+ const { context } = deps;
83
+ const inputRecord = context.inputRecord;
84
+
85
+ if (!inputRecord.data.relationships) {
86
+ return foreignKeyUpdates;
87
+ }
88
+
89
+ // Extract schema from scope
90
+ const {
91
+ vars: {
92
+ schemaInfo: { schema }
93
+ }
94
+ } = scope;
95
+
96
+ // Schema might be a Schema object or plain object
97
+ const schemaStructure = getSchemaStructure(schema);
98
+
99
+ for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
100
+ if (fieldDef.belongsTo && fieldDef.as) {
101
+ const relName = fieldDef.as;
102
+ if (inputRecord.data.relationships[relName] !== undefined) {
103
+ const relData = inputRecord.data.relationships[relName];
104
+ foreignKeyUpdates[fieldName] = relData.data?.id || null;
105
+ }
106
+ }
107
+ }
108
+
109
+ return foreignKeyUpdates;
110
+ };
@@ -0,0 +1,300 @@
1
+ import { RestApiResourceError } from '../../../../lib/rest-api-errors.js';
2
+ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-helpers.js';
3
+
4
+ /**
5
+ * Updates many-to-many relationships intelligently by synchronizing pivot table records
6
+ *
7
+ * @param {Object} scope - The scope object (not used directly, for consistency)
8
+ * @param {Object} deps - Dependencies object
9
+ * @returns {Promise<void>}
10
+ *
11
+ * @example
12
+ * // Input: Article currently has tags [1, 2], want to change to [1, 3]
13
+ * const deps = {
14
+ * api,
15
+ * context: {
16
+ * resourceId: '100',
17
+ * relDef: {
18
+ * through: 'article_tags', // Pivot table
19
+ * foreignKey: 'article_id', // Points to article
20
+ * otherKey: 'tag_id' // Points to tag
21
+ * },
22
+ * relData: [
23
+ * { type: 'tags', id: '1' }, // Keep this
24
+ * { type: 'tags', id: '3' } // Add this
25
+ * ],
26
+ * transaction: trx
27
+ * }
28
+ * };
29
+ *
30
+ * // Before: article_tags table
31
+ * // article_id | tag_id | created_at
32
+ * // 100 | 1 | 2024-01-01
33
+ * // 100 | 2 | 2024-01-02
34
+ *
35
+ * await updateManyToManyRelationship(null, deps);
36
+ *
37
+ * // After: article_tags table
38
+ * // article_id | tag_id | created_at
39
+ * // 100 | 1 | 2024-01-01 (preserved!)
40
+ * // 100 | 3 | 2024-12-01 (new)
41
+ * // Tag 2 was deleted, Tag 1 kept its metadata
42
+ *
43
+ * @example
44
+ * // Input: Pivot table has extra fields to preserve
45
+ * // article_tags has: article_id, tag_id, display_order, featured
46
+ *
47
+ * // Current data:
48
+ * // article_id | tag_id | display_order | featured
49
+ * // 100 | 1 | 1 | true
50
+ * // 100 | 2 | 2 | false
51
+ *
52
+ * const deps = {
53
+ * context: {
54
+ * resourceId: '100',
55
+ * relData: [
56
+ * { type: 'tags', id: '1' }, // Keep tag 1
57
+ * { type: 'tags', id: '5' } // Add tag 5
58
+ * ]
59
+ * }
60
+ * };
61
+ *
62
+ * await updateManyToManyRelationship(null, deps);
63
+ *
64
+ * // Result:
65
+ * // article_id | tag_id | display_order | featured
66
+ * // 100 | 1 | 1 | true (preserved!)
67
+ * // 100 | 5 | NULL | NULL (new with defaults)
68
+ *
69
+ * @example
70
+ * // Input: Clear all relationships
71
+ * const deps = {
72
+ * context: {
73
+ * resourceId: '100',
74
+ * relData: [] // Empty array means remove all
75
+ * }
76
+ * };
77
+ *
78
+ * await updateManyToManyRelationship(null, deps);
79
+ * // All article_tags records for article 100 are deleted
80
+ *
81
+ * @description
82
+ * Used by:
83
+ * - relationship-processor.js calls this for many-to-many updates
84
+ * - Used in PATCH operations to sync relationships
85
+ * - Also used in PUT operations (replaces deleteExistingPivotRecords pattern)
86
+ *
87
+ * Purpose:
88
+ * - Intelligently syncs pivot table to match desired state
89
+ * - Preserves existing pivot records that should remain (with their metadata)
90
+ * - Only deletes records that should be removed
91
+ * - Only creates records that are new
92
+ * - Much better than delete-all-then-recreate pattern
93
+ *
94
+ * Data flow:
95
+ * 1. Queries existing pivot records for the resource
96
+ * 2. Compares existing IDs with desired IDs
97
+ * 3. Calculates which to delete and which to add
98
+ * 4. Validates new related resources exist (optional)
99
+ * 5. Performs bulk delete for removed relationships
100
+ * 6. Performs bulk insert for new relationships
101
+ * 7. Records that exist in both are untouched (metadata preserved)
102
+ */
103
+ export const updateManyToManyRelationship = async (scope, deps) => {
104
+ // Extract values from deps
105
+ const { api, context } = deps;
106
+ const { resourceId, relDef, relData, transaction: trx } = context;
107
+
108
+ // Get the knex instance from the pivot scope
109
+ const pivotScope = api.resources[relDef.through];
110
+ if (!pivotScope) {
111
+ throw new Error(`Pivot table resource '${relDef.through}' not found`);
112
+ }
113
+
114
+ // Get the actual database table name (might be different from scope name)
115
+ const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through;
116
+
117
+ // Get existing pivot records directly from database
118
+ const existingRecords = await trx(tableName)
119
+ .where(relDef.foreignKey, resourceId)
120
+ .select(relDef.otherKey);
121
+
122
+ // Create sets for efficient comparison
123
+ const existingIds = new Set(existingRecords.map(r => String(r[relDef.otherKey])));
124
+ const newIds = new Set(relData.map(r => String(r.id)));
125
+
126
+ // Determine what to delete and add
127
+ const toDelete = [...existingIds].filter(id => !newIds.has(id));
128
+ const toAdd = [...newIds].filter(id => !existingIds.has(id));
129
+
130
+ // Validate related resources exist if needed (do this before any changes)
131
+ if (relDef.validateExists !== false && toAdd.length > 0) {
132
+ for (const relatedId of toAdd) {
133
+ const related = relData.find(r => String(r.id) === relatedId);
134
+ try {
135
+ await api.resources[related.type].get({
136
+ id: related.id,
137
+ transaction: trx
138
+ });
139
+ } catch (error) {
140
+ throw new RestApiResourceError(
141
+ `Related ${related.type} with id ${related.id} not found`,
142
+ {
143
+ subtype: 'not_found',
144
+ resourceType: related.type,
145
+ resourceId: related.id
146
+ }
147
+ );
148
+ }
149
+ }
150
+ }
151
+
152
+ // Bulk delete records that should be removed
153
+ if (toDelete.length > 0) {
154
+ await trx(tableName)
155
+ .where(relDef.foreignKey, resourceId)
156
+ .whereIn(relDef.otherKey, toDelete)
157
+ .delete();
158
+ }
159
+
160
+ // Bulk insert new records
161
+ if (toAdd.length > 0) {
162
+ const recordsToInsert = toAdd.map(relatedId => ({
163
+ [relDef.foreignKey]: resourceId,
164
+ [relDef.otherKey]: relatedId
165
+ }));
166
+
167
+ await trx(tableName).insert(recordsToInsert);
168
+ }
169
+
170
+ // Records that exist in both are automatically preserved with their pivot data
171
+ };
172
+
173
+ // Note: deleteExistingPivotRecords has been removed in favor of using
174
+ // updateManyToManyRelationship for all sync operations (including PUT).
175
+ // This aligns with industry standards where ORMs use intelligent sync
176
+ // rather than delete-all-then-recreate patterns.
177
+
178
+ /**
179
+ * Creates new pivot table records for many-to-many relationships
180
+ *
181
+ * @param {Object} api - The API instance with access to resources
182
+ * @param {string|number} resourceId - The ID of the primary resource
183
+ * @param {Object} relDef - The relationship definition
184
+ * @param {Array} relData - Array of related resources to link
185
+ * @param {Object} trx - Database transaction object
186
+ * @returns {Promise<void>}
187
+ *
188
+ * @example
189
+ * // Input: Create article-tag relationships
190
+ * const relDef = {
191
+ * through: 'article_tags',
192
+ * foreignKey: 'article_id',
193
+ * otherKey: 'tag_id'
194
+ * };
195
+ * const relData = [
196
+ * { type: 'tags', id: '10' },
197
+ * { type: 'tags', id: '20' },
198
+ * { type: 'tags', id: '30' }
199
+ * ];
200
+ *
201
+ * await createPivotRecords(api, '100', relDef, relData, trx);
202
+ *
203
+ * // Result: 3 new records in article_tags table
204
+ * // article_id | tag_id
205
+ * // 100 | 10
206
+ * // 100 | 20
207
+ * // 100 | 30
208
+ *
209
+ * @example
210
+ * // Input: Validation ensures related resources exist
211
+ * const relData = [
212
+ * { type: 'tags', id: '999' } // Non-existent tag
213
+ * ];
214
+ *
215
+ * try {
216
+ * await createPivotRecords(api, '100', relDef, relData, trx);
217
+ * } catch (error) {
218
+ * console.log(error.message);
219
+ * // "Related tags with id 999 not found"
220
+ * // Transaction rolled back, no records created
221
+ * }
222
+ *
223
+ * @example
224
+ * // Input: Skip validation for performance
225
+ * const relDef = {
226
+ * through: 'user_permissions',
227
+ * foreignKey: 'user_id',
228
+ * otherKey: 'permission_id',
229
+ * validateExists: false // Skip GET requests
230
+ * };
231
+ *
232
+ * // With 100 permissions, saves 100 GET requests
233
+ * await createPivotRecords(api, userId, relDef, permissions, trx);
234
+ *
235
+ * // Risk: Could create orphaned relationships if permissions don't exist
236
+ * // Benefit: Much faster for bulk operations when you trust the data
237
+ *
238
+ * @description
239
+ * Used by:
240
+ * - relationship-processor.js for POST operations
241
+ * - updateManyToManyRelationship internally for new relationships
242
+ * - Any code that needs to create pivot records
243
+ *
244
+ * Purpose:
245
+ * - Creates pivot table records to link resources
246
+ * - Validates related resources exist by default (referential integrity)
247
+ * - Supports bulk insert for efficiency
248
+ * - Works within transactions for atomicity
249
+ * - Allows skipping validation when performance matters
250
+ *
251
+ * Data flow:
252
+ * 1. Validates pivot table resource exists
253
+ * 2. Gets actual database table name
254
+ * 3. Optionally validates each related resource exists (GET requests)
255
+ * 4. Prepares bulk insert data with foreign keys
256
+ * 5. Performs single INSERT with all records
257
+ * 6. Returns (no data returned, throws on error)
258
+ */
259
+ export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
260
+ if (relData.length === 0) return; // Early exit if nothing to create
261
+
262
+ // Get pivot table info
263
+ const pivotScope = api.resources[relDef.through];
264
+ if (!pivotScope) {
265
+ throw new Error(`Pivot table resource '${relDef.through}' not found`);
266
+ }
267
+
268
+ // Get the actual database table name (might be different from scope name)
269
+ const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through;
270
+
271
+ // Validate all related resources exist if needed (do this before any inserts)
272
+ if (relDef.validateExists !== false) {
273
+ for (const related of relData) {
274
+ try {
275
+ await api.resources[related.type].get({
276
+ id: related.id,
277
+ transaction: trx
278
+ });
279
+ } catch (error) {
280
+ throw new RestApiResourceError(
281
+ `Related ${related.type} with id ${related.id} not found`,
282
+ {
283
+ subtype: 'not_found',
284
+ resourceType: related.type,
285
+ resourceId: related.id
286
+ }
287
+ );
288
+ }
289
+ }
290
+ }
291
+
292
+ // Prepare records for bulk insert
293
+ const recordsToInsert = relData.map(related => ({
294
+ [relDef.foreignKey]: resourceId,
295
+ [relDef.otherKey]: related.id
296
+ }));
297
+
298
+ // Bulk insert all pivot records in a single query
299
+ await trx(tableName).insert(recordsToInsert);
300
+ };
@@ -0,0 +1,252 @@
1
+ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
2
+
3
+ /**
4
+ * Processes JSON:API relationship data and converts to database operations
5
+ *
6
+ * @param {Object} scope - The scope object containing schema info
7
+ * @param {Object} deps - Dependencies object
8
+ * @returns {Object} Object with belongsToUpdates and manyToManyRelationships arrays
9
+ *
10
+ * @example
11
+ * // Input: Simple belongsTo relationship
12
+ * const inputRecord = {
13
+ * data: {
14
+ * type: 'articles',
15
+ * attributes: { title: 'My Article' },
16
+ * relationships: {
17
+ * author: {
18
+ * data: { type: 'users', id: '123' }
19
+ * },
20
+ * category: {
21
+ * data: { type: 'categories', id: '5' }
22
+ * }
23
+ * }
24
+ * }
25
+ * };
26
+ *
27
+ * // Schema has:
28
+ * // author_id: { belongsTo: 'users', as: 'author' }
29
+ * // category_id: { belongsTo: 'categories', as: 'category' }
30
+ *
31
+ * const result = processRelationships(scope, { context: { inputRecord } });
32
+ *
33
+ * // Output: Foreign keys extracted
34
+ * // {
35
+ * // belongsToUpdates: {
36
+ * // author_id: '123',
37
+ * // category_id: '5'
38
+ * // },
39
+ * // manyToManyRelationships: []
40
+ * // }
41
+ *
42
+ * @example
43
+ * // Input: Polymorphic relationship
44
+ * const inputRecord = {
45
+ * data: {
46
+ * type: 'comments',
47
+ * relationships: {
48
+ * commentable: {
49
+ * data: { type: 'posts', id: '456' }
50
+ * }
51
+ * }
52
+ * }
53
+ * };
54
+ *
55
+ * // Schema relationships has:
56
+ * // commentable: {
57
+ * // belongsToPolymorphic: {
58
+ * // types: ['posts', 'videos'],
59
+ * // typeField: 'commentable_type',
60
+ * // idField: 'commentable_id'
61
+ * // }
62
+ * // }
63
+ *
64
+ * const result = processRelationships(scope, { context: { inputRecord } });
65
+ *
66
+ * // Output: Both type and id fields set
67
+ * // {
68
+ * // belongsToUpdates: {
69
+ * // commentable_type: 'posts',
70
+ * // commentable_id: '456'
71
+ * // },
72
+ * // manyToManyRelationships: []
73
+ * // }
74
+ *
75
+ * @example
76
+ * // Input: Many-to-many relationship
77
+ * const inputRecord = {
78
+ * data: {
79
+ * type: 'articles',
80
+ * relationships: {
81
+ * tags: {
82
+ * data: [
83
+ * { type: 'tags', id: '10' },
84
+ * { type: 'tags', id: '20' }
85
+ * ]
86
+ * }
87
+ * }
88
+ * }
89
+ * };
90
+ *
91
+ * // Schema relationships has:
92
+ * // tags: {
93
+ * // hasMany: 'tags',
94
+ * // through: 'article_tags',
95
+ * // foreignKey: 'article_id',
96
+ * // otherKey: 'tag_id'
97
+ * // }
98
+ *
99
+ * const result = processRelationships(scope, { context: { inputRecord } });
100
+ *
101
+ * // Output: Many-to-many data collected
102
+ * // {
103
+ * // belongsToUpdates: {},
104
+ * // manyToManyRelationships: [{
105
+ * // relName: 'tags',
106
+ * // relDef: {
107
+ * // through: 'article_tags',
108
+ * // foreignKey: 'article_id',
109
+ * // otherKey: 'tag_id'
110
+ * // },
111
+ * // relData: [
112
+ * // { type: 'tags', id: '10' },
113
+ * // { type: 'tags', id: '20' }
114
+ * // ]
115
+ * // }]
116
+ * // }
117
+ *
118
+ * @example
119
+ * // Input: Clearing relationships
120
+ * const inputRecord = {
121
+ * data: {
122
+ * relationships: {
123
+ * author: { data: null }, // Clear belongsTo
124
+ * tags: { data: [] } // Clear many-to-many
125
+ * }
126
+ * }
127
+ * };
128
+ *
129
+ * const result = processRelationships(scope, { context: { inputRecord } });
130
+ *
131
+ * // Output: Null for belongsTo, empty array for many-to-many
132
+ * // {
133
+ * // belongsToUpdates: { author_id: null },
134
+ * // manyToManyRelationships: [{
135
+ * // relName: 'tags',
136
+ * // relDef: { ... },
137
+ * // relData: [] // Will delete all pivot records
138
+ * // }]
139
+ * // }
140
+ *
141
+ * @description
142
+ * Used by:
143
+ * - rest-api-knex-plugin's dataPut and dataPatch methods
144
+ * - Called before database writes to prepare relationship updates
145
+ *
146
+ * Purpose:
147
+ * - Converts JSON:API relationship format to database operations
148
+ * - Handles all relationship types: belongsTo, polymorphic, many-to-many
149
+ * - Validates polymorphic types against allowed values
150
+ * - Separates concerns: foreign keys vs pivot table operations
151
+ *
152
+ * Data flow:
153
+ * 1. Receives JSON:API document with relationships section
154
+ * 2. Analyzes schema to determine relationship types
155
+ * 3. For belongsTo: extracts foreign key values
156
+ * 4. For polymorphic: extracts both type and id fields
157
+ * 5. For many-to-many: collects data for pivot operations
158
+ * 6. Returns structured data for database updates
159
+ */
160
+ export const processRelationships = (scope, deps) => {
161
+ // Extract values from scope
162
+ const {
163
+ vars: {
164
+ schemaInfo: { schemaStructure: schemaFields, schemaRelationships: relationships }
165
+ }
166
+ } = scope;
167
+
168
+ // Extract values from deps
169
+ const { context } = deps;
170
+ const { inputRecord } = context;
171
+ const belongsToUpdates = {};
172
+ const manyToManyRelationships = [];
173
+
174
+ if (!inputRecord.data.relationships) {
175
+ return { belongsToUpdates, manyToManyRelationships };
176
+ }
177
+
178
+ for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
179
+ const relDef = relationships?.[relName];
180
+
181
+ // Find the schema field that defines this relationship
182
+ const schemaField = Object.entries(schemaFields).find(([fieldName, fieldDef]) =>
183
+ fieldDef.as === relName
184
+ );
185
+
186
+ if (schemaField) {
187
+ const [fieldName, fieldDef] = schemaField;
188
+
189
+ // Handle regular belongsTo (1:1)
190
+ if (fieldDef.belongsTo) {
191
+ if (relData.data === null) {
192
+ belongsToUpdates[fieldName] = null;
193
+ } else if (relData.data?.id) {
194
+ belongsToUpdates[fieldName] = relData.data.id;
195
+ }
196
+ }
197
+ }
198
+
199
+ // Check for polymorphic belongsTo defined in relationships object (not as schema field)
200
+ if (!schemaField && relDef?.belongsToPolymorphic && relData.data !== undefined) {
201
+ if (relData.data === null) {
202
+ const { typeField, idField } = relDef.belongsToPolymorphic;
203
+ belongsToUpdates[typeField] = null;
204
+ belongsToUpdates[idField] = null;
205
+ } else if (relData.data) {
206
+ const { type, id } = relData.data;
207
+ const { types, typeField, idField } = relDef.belongsToPolymorphic;
208
+
209
+ // Validate type is allowed
210
+ if (!types.includes(type)) {
211
+ throw new RestApiValidationError(
212
+ `Invalid type '${type}' for polymorphic relationship '${relName}'. Allowed types: ${types.join(', ')}`,
213
+ {
214
+ fields: [`data.relationships.${relName}.data.type`],
215
+ violations: [{
216
+ field: `data.relationships.${relName}.data.type`,
217
+ rule: 'polymorphic_type',
218
+ message: `Type must be one of: ${types.join(', ')}`
219
+ }]
220
+ }
221
+ );
222
+ }
223
+
224
+ belongsToUpdates[typeField] = type;
225
+ belongsToUpdates[idField] = id;
226
+ }
227
+ }
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) {
239
+ manyToManyRelationships.push({
240
+ relName,
241
+ relDef: {
242
+ through: relDef.through,
243
+ foreignKey: relDef.foreignKey,
244
+ otherKey: relDef.otherKey
245
+ },
246
+ relData: relData.data || [] // null means empty array for many-to-many
247
+ });
248
+ }
249
+ }
250
+
251
+ return { belongsToUpdates, manyToManyRelationships };
252
+ };