json-rest-api 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -1,700 +0,0 @@
1
- import {
2
- RestApiResourceError,
3
- RestApiValidationError,
4
- RestApiPayloadError
5
- } from '../../lib/rest-api-errors.js';
6
- import { createPivotRecords } from './lib/many-to-many-manipulations.js';
7
- import { parseJsonApiQuery } from './utils/connectors-query-parser.js';
8
-
9
- /**
10
- * Helper function to get relationship definition from either schemaRelationships or schema fields
11
- */
12
- const findRelationshipDefinition = (schemaInfo, relationshipName) => {
13
- // First check schemaRelationships (for relationships defined in relationships object)
14
- const relDef = schemaInfo.schemaRelationships?.[relationshipName];
15
- if (relDef) {
16
- return relDef;
17
- }
18
-
19
- // Then check schema fields for belongsTo relationships with matching 'as' property
20
- for (const [fieldName, fieldDef] of Object.entries(schemaInfo.schemaStructure || {})) {
21
- if (fieldDef.as === relationshipName && (fieldDef.belongsTo || fieldDef.belongsToPolymorphic)) {
22
- return fieldDef;
23
- }
24
- }
25
-
26
- return null;
27
- };
28
-
29
- /**
30
- * Helper function to handle errors in write methods
31
- */
32
- const handleWriteMethodError = async (error, context, method, scopeName, log) => {
33
- if (context.shouldCommit && context.transaction) {
34
- try {
35
- await context.transaction.rollback();
36
- } catch (rollbackError) {
37
- log.error(`Failed to rollback transaction after error in ${method}:`, rollbackError);
38
- }
39
- }
40
- throw error;
41
- };
42
-
43
- export const RestApiRelationshipsPlugin = {
44
- name: 'rest-api-relationships',
45
- dependencies: ['rest-api', 'rest-api-knex'],
46
-
47
- async install({ api, addScopeMethod, helpers, on, log, runHooks, addHook, scopes }) {
48
-
49
- // Store api reference for use in scope methods
50
- const apiRef = api;
51
-
52
- /**
53
- * GET RELATIONSHIP
54
- * Retrieves relationship linkage data (just resource identifiers)
55
- * GET /api/articles/1/relationships/author
56
- *
57
- * @param {string} id - The ID of the resource
58
- * @param {string} relationshipName - The name of the relationship
59
- * @returns {Promise<object>} Relationship linkage with links
60
- */
61
- addScopeMethod('getRelationship', async ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName }) => {
62
- context.method = 'getRelationship';
63
- context.id = params.id;
64
- context.relationshipName = params.relationshipName;
65
- context.schemaInfo = scopes[scopeName].vars.schemaInfo;
66
-
67
- // Validate the relationship exists
68
- const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
69
-
70
- if (!relDef) {
71
- throw new RestApiResourceError(
72
- `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
73
- { subtype: 'relationship_not_found' }
74
- );
75
- }
76
-
77
- // Check permissions
78
- await runHooks('checkPermissions');
79
- await runHooks('checkPermissionsGetRelationship');
80
-
81
- // Reuse existing get method with minimal fields
82
- const fullRecord = await scope.get({
83
- id: context.id,
84
- queryParams: {
85
- include: [context.relationshipName],
86
- fields: { [scopeName]: vars.idProperty || 'id' }
87
- },
88
- transaction: context.transaction,
89
- simplified: false,
90
- isTransport: params.isTransport
91
- });
92
-
93
- if (!fullRecord || !fullRecord.data) {
94
- throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
95
- }
96
-
97
- // Extract just the relationship data
98
- const relationshipData = fullRecord.data.relationships?.[context.relationshipName];
99
-
100
- // Build response with links
101
- return {
102
- links: {
103
- self: `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/relationships/${context.relationshipName}`,
104
- related: `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/${context.relationshipName}`
105
- },
106
- data: relationshipData?.data || (relDef.hasMany || relDef.manyToMany ? [] : null)
107
- };
108
- });
109
-
110
- /**
111
- * GET RELATED
112
- * Retrieves the actual related resources (full data)
113
- * GET /api/articles/1/comments
114
- *
115
- * @param {string} id - The ID of the parent resource
116
- * @param {string} relationshipName - The name of the relationship
117
- * @param {object} queryParams - Standard query parameters
118
- * @returns {Promise<object>} Related resources with full data
119
- */
120
- addScopeMethod('getRelated', async ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName }) => {
121
- context.method = 'getRelated';
122
- context.id = params.id;
123
- context.relationshipName = params.relationshipName;
124
- context.queryParams = params.queryParams || {};
125
- context.schemaInfo = scopes[scopeName].vars.schemaInfo;
126
- context.transaction = params.transaction;
127
- context.db = context.transaction || api.knex.instance
128
-
129
- // Validate the relationship exists
130
- const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
131
-
132
- if (!relDef) {
133
- throw new RestApiResourceError(
134
- `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
135
- { subtype: 'relationship_not_found' }
136
- );
137
- }
138
-
139
- // Determine target type - relationships are stored as strings
140
- const targetType = relDef.belongsTo || relDef.hasMany ||
141
- relDef.manyToMany || relDef.hasOne;
142
-
143
- if (!targetType || !scopes[targetType]) {
144
- throw new RestApiResourceError(
145
- `Related resource type '${targetType}' not found`,
146
- { subtype: 'related_type_not_found' }
147
- );
148
- }
149
-
150
- // Check permissions
151
- await runHooks('checkPermissions');
152
- await runHooks('checkPermissionsGetRelated');
153
-
154
- // Verify parent exists
155
- const exists = await helpers.dataExists({
156
- scopeName,
157
- context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
158
- });
159
-
160
- if (!exists) {
161
- throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
162
- }
163
-
164
- // Handle to-one relationships (belongsTo and hasOne)
165
- // For example: GET /api/books/1/country or GET /api/books/1/publisher
166
- if (relDef.belongsTo || relDef.hasOne) {
167
- // OPTIMIZATION: Detect if we actually need to make two API calls
168
- //
169
- // The naive approach always makes 2 calls:
170
- // 1. Get parent with relationship included (fetches FULL related record)
171
- // 2. Extract just the ID and fetch the same record again with queryParams
172
- //
173
- // This optimization checks if there are queryParams that would affect
174
- // the related resource. If not, we can use the data from the first call.
175
- const hasRelevantQueryParams = context.queryParams && (
176
- // Check for includes on the related resource (e.g., ?include=some.nested.relation)
177
- context.queryParams.include?.length > 0 ||
178
- // Check for field selection on the related resource (e.g., ?fields[countries]=name,code)
179
- context.queryParams.fields?.[targetType] ||
180
- // Note: Filters and sorting don't make sense for a single to-one relationship
181
- // so we don't check for them
182
- false
183
- );
184
-
185
- if (hasRelevantQueryParams) {
186
- // CASE 1: Has queryParams that affect the related resource
187
- // We need to make two calls to properly apply the queryParams
188
-
189
- // First call: Get parent with minimal data (just need the related ID)
190
- const parent = await scope.get({
191
- id: context.id,
192
- queryParams: {
193
- include: [context.relationshipName],
194
- fields: { [scopeName]: vars.idProperty || 'id' } // Only fetch parent ID to minimize data
195
- },
196
- transaction: context.transaction,
197
- simplified: false,
198
- isTransport: params.isTransport
199
- });
200
-
201
- const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id;
202
- if (!relatedId) {
203
- return {
204
- links: { self: `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/${context.relationshipName}` },
205
- data: null
206
- };
207
- }
208
-
209
- // Second call: Get the related resource with all queryParams applied
210
- const related = await apiRef.resources[targetType].get({
211
- id: relatedId,
212
- queryParams: context.queryParams,
213
- transaction: context.transaction,
214
- simplified: false,
215
- isTransport: params.isTransport
216
- });
217
-
218
- return {
219
- links: { self: `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/${context.relationshipName}` },
220
- data: related.data,
221
- included: related.included
222
- };
223
- } else {
224
- // CASE 2: No queryParams that affect the related resource
225
- // We can get everything in one call and extract from included
226
-
227
- // Single call: Get parent with full related resource included
228
- const parent = await scope.get({
229
- id: context.id,
230
- queryParams: {
231
- include: [context.relationshipName],
232
- fields: context.queryParams.fields // Respect any field selections for the parent
233
- },
234
- transaction: context.transaction,
235
- simplified: false,
236
- isTransport: params.isTransport
237
- });
238
-
239
- // Extract the related resource from the parent's relationships
240
- const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id;
241
- if (!relatedId) {
242
- return {
243
- links: { self: `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/${context.relationshipName}` },
244
- data: null
245
- };
246
- }
247
-
248
- // Find the full related resource in the included array
249
- // The include system already fetched it for us!
250
- const relatedResource = parent.included?.find(
251
- r => r.type === targetType && r.id === relatedId
252
- );
253
-
254
- return {
255
- links: { self: `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/${context.relationshipName}` },
256
- data: relatedResource || null
257
- };
258
- }
259
- }
260
-
261
- // Handle simple hasMany (one-to-many)
262
- if (relDef.hasMany && !relDef.through) {
263
- const filters = {
264
- ...context.queryParams.filters,
265
- [relDef.foreignKey]: context.id
266
- };
267
-
268
- const result = await apiRef.resources[targetType].query({
269
- queryParams: { ...context.queryParams, filters },
270
- transaction: context.transaction,
271
- simplified: false,
272
- isTransport: params.isTransport
273
- });
274
-
275
- if (!result.links) {
276
- result.links = {};
277
- }
278
- result.links.self = `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/${context.relationshipName}`;
279
- return result;
280
- }
281
-
282
- // Handle many-to-many relationships (hasMany with through)
283
- // For example: GET /api/authors/1/books (where authors and books are linked via book_authors)
284
- if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
285
- // APPROACH: Query the pivot table directly
286
- //
287
- // Instead of:
288
- // 1. Getting parent with relationship included to find related IDs
289
- // 2. Trying to filter by id:[array] which doesn't work
290
- //
291
- // We query the pivot table (e.g., book_authors) filtered by the parent ID
292
- // and include the target resources. This is simpler and uses existing
293
- // API functionality without needing special array filter support.
294
-
295
- const pivotResource = relDef.through;
296
- const foreignKey = relDef.foreignKey || `${scopeName.slice(0, -1)}_id`;
297
- const otherKey = relDef.otherKey || `${targetType.slice(0, -1)}_id`;
298
-
299
- // We need to find the relationship name on the pivot table that points to the target
300
- // The pivot table should have a belongsTo relationship to the target resource
301
- const pivotScope = scopes[pivotResource];
302
- if (!pivotScope) {
303
- throw new RestApiResourceError(
304
- `Pivot table resource '${pivotResource}' not found`,
305
- { subtype: 'pivot_table_not_found' }
306
- );
307
- }
308
-
309
- // Find the relationship name on the pivot table that points to the target
310
- let targetRelationshipName = null;
311
- const pivotSchema = pivotScope.vars.schemaInfo.schemaStructure;
312
- for (const [fieldName, fieldDef] of Object.entries(pivotSchema)) {
313
- if (fieldDef.belongsTo === targetType && fieldDef.as) {
314
- targetRelationshipName = fieldDef.as;
315
- break;
316
- }
317
- }
318
-
319
- if (!targetRelationshipName) {
320
- throw new RestApiResourceError(
321
- `Cannot find relationship to '${targetType}' in pivot table '${pivotResource}'`,
322
- { subtype: 'pivot_relationship_not_found' }
323
- );
324
- }
325
-
326
- // Build filters for the pivot table
327
- const pivotFilters = {
328
- [foreignKey]: context.id
329
- };
330
-
331
- // Query the pivot table with the target included
332
- const pivotResult = await apiRef.resources[pivotResource].query({
333
- queryParams: {
334
- filters: pivotFilters,
335
- include: [targetRelationshipName],
336
- // Pass through other query params that should apply to the included resources
337
- fields: context.queryParams.fields,
338
- sort: context.queryParams.sort,
339
- page: context.queryParams.page
340
- },
341
- transaction: context.transaction,
342
- simplified: false,
343
- isTransport: params.isTransport
344
- });
345
-
346
- // Extract the included target resources
347
- const includedResources = pivotResult.included?.filter(r => r.type === targetType) || [];
348
-
349
- // Build the response
350
- return {
351
- links: {
352
- self: `${vars.resourceUrlPrefix}/${scopeName}/${context.id}/${context.relationshipName}`
353
- },
354
- data: includedResources,
355
- meta: pivotResult.meta
356
- };
357
- }
358
- });
359
-
360
- /**
361
- * POST RELATIONSHIP
362
- * Adds members to a to-many relationship
363
- * POST /api/articles/1/relationships/tags
364
- *
365
- * @param {string} id - The ID of the resource
366
- * @param {string} relationshipName - The name of the relationship
367
- * @param {array} relationshipData - Array of resource identifiers to add
368
- * @returns {Promise<void>} 204 No Content
369
- */
370
- addScopeMethod('postRelationship', async ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName }) => {
371
- context.method = 'postRelationship';
372
- context.id = params.id;
373
- context.relationshipName = params.relationshipName;
374
- context.schemaInfo = scopes[scopeName].vars.schemaInfo;
375
-
376
- // Transaction handling
377
- context.transaction = params.transaction ||
378
- (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
379
- context.shouldCommit = !params.transaction && !!context.transaction;
380
-
381
- context.db = context.transaction || api.knex.instance
382
-
383
- try {
384
- // Validate
385
- const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
386
- if (!relDef) {
387
- throw new RestApiResourceError(
388
- `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
389
- { subtype: 'relationship_not_found' }
390
- );
391
- }
392
-
393
- if (!relDef.hasMany && !relDef.manyToMany) {
394
- throw new RestApiValidationError(
395
- `Cannot POST to to-one relationship '${context.relationshipName}'. Use PATCH instead.`,
396
- { fields: ['data'] }
397
- );
398
- }
399
-
400
- if (!Array.isArray(params.relationshipData)) {
401
- throw new RestApiPayloadError('POST to relationship requires array of resource identifiers');
402
- }
403
-
404
- // Check permissions
405
- await runHooks('checkPermissions');
406
- await runHooks('checkPermissionsPostRelationship');
407
-
408
- // Verify parent exists
409
- const exists = await helpers.dataExists({
410
- scopeName,
411
- context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo },
412
- });
413
-
414
- if (!exists) {
415
- throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
416
- }
417
-
418
- // Add relationships
419
- if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
420
- const manyToManyDef = relDef.manyToMany || {
421
- through: relDef.through,
422
- foreignKey: relDef.foreignKey,
423
- otherKey: relDef.otherKey
424
- };
425
-
426
- await createPivotRecords(apiRef, context.id, manyToManyDef, params.relationshipData, context.transaction);
427
- } else {
428
- // Update foreign keys for hasMany
429
- const targetType = relDef.hasMany;
430
- for (const identifier of params.relationshipData) {
431
- await apiRef.resources[targetType].patch({
432
- id: identifier.id,
433
- inputRecord: {
434
- data: {
435
- type: targetType,
436
- id: identifier.id,
437
- attributes: { [relDef.hasMany.foreignKey]: context.id }
438
- }
439
- },
440
- transaction: context.transaction,
441
- simplified: false
442
- });
443
- }
444
- }
445
-
446
- await runHooks('finish');
447
- await runHooks('finishPostRelationship');
448
-
449
- if (context.shouldCommit) {
450
- await context.transaction.commit();
451
- }
452
-
453
- return; // 204 No Content
454
-
455
- } catch (error) {
456
- await handleWriteMethodError(error, context, 'POST_RELATIONSHIP', scopeName, log);
457
- }
458
- });
459
-
460
- /**
461
- * PATCH RELATIONSHIP
462
- * Completely replaces a relationship
463
- * PATCH /api/articles/1/relationships/author
464
- *
465
- * @param {string} id - The ID of the resource
466
- * @param {string} relationshipName - The name of the relationship
467
- * @param {object|array} relationshipData - New relationship data
468
- * @returns {Promise<void>} 204 No Content
469
- */
470
- addScopeMethod('patchRelationship', async ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName }) => {
471
- context.method = 'patchRelationship';
472
- context.id = params.id;
473
- context.relationshipName = params.relationshipName;
474
-
475
- // Transaction handling
476
- context.transaction = params.transaction ||
477
- (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
478
- context.shouldCommit = !params.transaction && !!context.transaction;
479
- context.db = context.transaction || api.knex.instance
480
-
481
- try {
482
- // Check permissions
483
- await runHooks('checkPermissions');
484
- await runHooks('checkPermissionsPatchRelationship');
485
-
486
- // Reuse existing patch with relationship data
487
- await scope.patch({
488
- id: context.id,
489
- inputRecord: {
490
- data: {
491
- type: scopeName,
492
- id: context.id,
493
- relationships: {
494
- [params.relationshipName]: { data: params.relationshipData }
495
- }
496
- }
497
- },
498
- transaction: context.transaction,
499
- simplified: false,
500
- isTransport: params.isTransport
501
- });
502
-
503
- await runHooks('finish');
504
- await runHooks('finishPatchRelationship');
505
-
506
- if (context.shouldCommit) {
507
- await context.transaction.commit();
508
- }
509
-
510
- return; // 204 No Content
511
-
512
- } catch (error) {
513
- await handleWriteMethodError(error, context, 'PATCH_RELATIONSHIP', scopeName, log);
514
- }
515
- });
516
-
517
- /**
518
- * DELETE RELATIONSHIP
519
- * Removes specific members from a to-many relationship
520
- * DELETE /api/articles/1/relationships/tags
521
- *
522
- * @param {string} id - The ID of the resource
523
- * @param {string} relationshipName - The name of the relationship
524
- * @param {array} relationshipData - Array of resource identifiers to remove
525
- * @returns {Promise<void>} 204 No Content
526
- */
527
- addScopeMethod('deleteRelationship', async ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName }) => {
528
- context.method = 'deleteRelationship';
529
- context.id = params.id;
530
- context.relationshipName = params.relationshipName;
531
- context.schemaInfo = scopes[scopeName].vars.schemaInfo;
532
-
533
- // Transaction handling
534
- context.transaction = params.transaction ||
535
- (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
536
- context.shouldCommit = !params.transaction && !!context.transaction;
537
- context.db = context.transaction || api.knex.instance
538
-
539
- try {
540
- // Validate
541
- const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
542
- if (!relDef) {
543
- throw new RestApiResourceError(
544
- `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
545
- { subtype: 'relationship_not_found' }
546
- );
547
- }
548
-
549
- if (!relDef.hasMany && !relDef.manyToMany) {
550
- throw new RestApiValidationError(
551
- `Cannot DELETE from to-one relationship '${context.relationshipName}'`,
552
- { fields: ['data'] }
553
- );
554
- }
555
-
556
- if (!Array.isArray(params.relationshipData)) {
557
- throw new RestApiPayloadError('DELETE from relationship requires array of resource identifiers');
558
- }
559
-
560
- // Check permissions
561
- await runHooks('checkPermissions');
562
- await runHooks('checkPermissionsDeleteRelationship');
563
-
564
- // Verify parent exists
565
- const exists = await helpers.dataExists({
566
- scopeName,
567
- context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
568
- });
569
-
570
- if (!exists) {
571
- throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
572
- }
573
-
574
- // Remove relationships
575
- if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
576
- const manyToManyDef = relDef.manyToMany || {
577
- through: relDef.through,
578
- foreignKey: relDef.foreignKey,
579
- otherKey: relDef.otherKey
580
- };
581
-
582
- const knex = apiRef.knex?.instance || helpers.db;
583
- const pivotResource = manyToManyDef.through;
584
- const pivotScope = apiRef.resources[pivotResource];
585
- const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || pivotResource;
586
- const localKey = manyToManyDef.foreignKey;
587
- const foreignKey = manyToManyDef.otherKey;
588
-
589
- for (const identifier of params.relationshipData) {
590
- await knex(pivotTable)
591
- .where(localKey, context.id)
592
- .where(foreignKey, identifier.id)
593
- .delete()
594
- .transacting(context.transaction);
595
- }
596
- } else {
597
- // Null out foreign keys for hasMany
598
- const targetType = relDef.hasMany;
599
- for (const identifier of params.relationshipData) {
600
- await apiRef.resources[targetType].patch({
601
- id: identifier.id,
602
- inputRecord: {
603
- data: {
604
- type: targetType,
605
- id: identifier.id,
606
- attributes: { [relDef.hasMany.foreignKey]: null }
607
- }
608
- },
609
- transaction: context.transaction,
610
- simplified: false
611
- });
612
- }
613
- }
614
-
615
- await runHooks('finish');
616
- await runHooks('finishDeleteRelationship');
617
-
618
- if (context.shouldCommit) {
619
- await context.transaction.commit();
620
- }
621
-
622
- return; // 204 No Content
623
-
624
- } catch (error) {
625
- await handleWriteMethodError(error, context, 'DELETE_RELATIONSHIP', scopeName, log);
626
- }
627
- });
628
-
629
- // Listen for scope additions to register relationship routes
630
- addHook('scope:added', 'registerRelationshipRoutes', {}, async ({ context }) => {
631
- const { scopeName } = context;
632
- const basePath = api.scopes[scopeName].vars.resourceUrlPrefix || '';
633
- const scopePath = `${basePath}/${scopeName}`;
634
-
635
- // Helper to create route handlers
636
- const createRouteHandler = (methodName) => {
637
- return async ({ params, body, queryString }) => {
638
- const scope = api.scopes[scopeName];
639
-
640
- const methodParams = {
641
- id: params.id,
642
- relationshipName: params.relationshipName,
643
- isTransport: true
644
- };
645
-
646
- // Add query params for getRelated
647
- if (methodName === 'getRelated' && queryString) {
648
- methodParams.queryParams = parseJsonApiQuery(queryString);
649
- }
650
-
651
- // Add body data for write operations
652
- if (body && body.data !== undefined) {
653
- methodParams.relationshipData = body.data;
654
- }
655
-
656
- return await scope[methodName](methodParams);
657
- };
658
- };
659
-
660
- // Register relationship routes
661
-
662
- // GET /api/{scope}/{id}/relationships/{relationshipName}
663
- await api.addRoute({
664
- method: 'GET',
665
- path: `${scopePath}/:id/relationships/:relationshipName`,
666
- handler: createRouteHandler('getRelationship')
667
- });
668
-
669
- // GET /api/{scope}/{id}/{relationshipName}
670
- await api.addRoute({
671
- method: 'GET',
672
- path: `${scopePath}/:id/:relationshipName`,
673
- handler: createRouteHandler('getRelated')
674
- });
675
-
676
- // POST /api/{scope}/{id}/relationships/{relationshipName}
677
- await api.addRoute({
678
- method: 'POST',
679
- path: `${scopePath}/:id/relationships/:relationshipName`,
680
- handler: createRouteHandler('postRelationship')
681
- });
682
-
683
- // PATCH /api/{scope}/{id}/relationships/{relationshipName}
684
- await api.addRoute({
685
- method: 'PATCH',
686
- path: `${scopePath}/:id/relationships/:relationshipName`,
687
- handler: createRouteHandler('patchRelationship')
688
- });
689
-
690
- // DELETE /api/{scope}/{id}/relationships/{relationshipName}
691
- await api.addRoute({
692
- method: 'DELETE',
693
- path: `${scopePath}/:id/relationships/:relationshipName`,
694
- handler: createRouteHandler('deleteRelationship')
695
- });
696
-
697
- log.trace(`Registered relationship routes for scope: ${scopeName}`);
698
- });
699
- }
700
- };