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
@@ -0,0 +1,295 @@
1
+ import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
2
+ import { findRelationshipDefinition } from "./common.js";
3
+
4
+ /**
5
+ * GET RELATED
6
+ * Retrieves the actual related resources (full data)
7
+ * GET /api/articles/1/comments
8
+ *
9
+ * @param {string} id - The ID of the parent resource
10
+ * @param {string} relationshipName - The name of the relationship
11
+ * @param {object} queryParams - Standard query parameters
12
+ * @returns {Promise<object>} Related resources with full data
13
+ */
14
+ export default async function getRelatedMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
15
+ context.method = 'getRelated';
16
+ context.id = params.id;
17
+ context.relationshipName = params.relationshipName;
18
+ context.queryParams = params.queryParams || {};
19
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo;
20
+ context.transaction = params.transaction;
21
+ context.db = context.transaction || api.knex.instance
22
+
23
+ // Validate the relationship exists
24
+ const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
25
+
26
+ if (!relDef) {
27
+ throw new RestApiResourceError(
28
+ `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
29
+ { subtype: 'relationship_not_found' }
30
+ );
31
+ }
32
+
33
+ // Determine target type - relationships are stored as strings
34
+ const targetType = relDef.belongsTo || relDef.hasMany ||
35
+ relDef.manyToMany || relDef.hasOne;
36
+
37
+ if (!targetType || !scopes[targetType]) {
38
+ throw new RestApiResourceError(
39
+ `Related resource type '${targetType}' not found`,
40
+ { subtype: 'related_type_not_found' }
41
+ );
42
+ }
43
+
44
+ // Check permissions
45
+ await runHooks('checkPermissions');
46
+ await runHooks('checkPermissionsGetRelated');
47
+
48
+ // Verify parent exists
49
+ const exists = await helpers.dataExists({
50
+ scopeName,
51
+ context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
52
+ });
53
+
54
+ if (!exists) {
55
+ throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
56
+ }
57
+
58
+ // Handle to-one relationships (belongsTo and hasOne)
59
+ // For example: GET /api/books/1/country or GET /api/books/1/publisher
60
+ if (relDef.belongsTo || relDef.hasOne) {
61
+ // OPTIMIZATION: Detect if we actually need to make two API calls
62
+ //
63
+ // The naive approach always makes 2 calls:
64
+ // 1. Get parent with relationship included (fetches FULL related record)
65
+ // 2. Extract just the ID and fetch the same record again with queryParams
66
+ //
67
+ // This optimization checks if there are queryParams that would affect
68
+ // the related resource. If not, we can use the data from the first call.
69
+ const hasRelevantQueryParams = context.queryParams && (
70
+ // Check for includes on the related resource (e.g., ?include=some.nested.relation)
71
+ context.queryParams.include?.length > 0 ||
72
+ // Check for field selection on the related resource (e.g., ?fields[countries]=name,code)
73
+ context.queryParams.fields?.[targetType] ||
74
+ // Note: Filters and sorting don't make sense for a single to-one relationship
75
+ // so we don't check for them
76
+ false
77
+ );
78
+
79
+ if (hasRelevantQueryParams) {
80
+ // CASE 1: Has queryParams that affect the related resource
81
+ // We need to make two calls to properly apply the queryParams
82
+
83
+ // First call: Get parent with minimal data (just need the related ID)
84
+ const parent = await scope.get({
85
+ id: context.id,
86
+ queryParams: {
87
+ include: [context.relationshipName],
88
+ fields: { [scopeName]: vars.idProperty || 'id' } // Only fetch parent ID to minimize data
89
+ },
90
+ transaction: context.transaction,
91
+ simplified: false,
92
+ isTransport: params.isTransport
93
+ });
94
+
95
+ const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id;
96
+ if (!relatedId) {
97
+ return {
98
+ links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
99
+ data: null
100
+ };
101
+ }
102
+
103
+ // Second call: Get the related resource with all queryParams applied
104
+ const related = await api.resources[targetType].get({
105
+ id: relatedId,
106
+ queryParams: context.queryParams,
107
+ transaction: context.transaction,
108
+ simplified: false,
109
+ isTransport: params.isTransport
110
+ });
111
+
112
+ return {
113
+ links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
114
+ data: related.data,
115
+ included: related.included
116
+ };
117
+ } else {
118
+ // CASE 2: No queryParams that affect the related resource
119
+ // We can get everything in one call and extract from included
120
+
121
+ // Single call: Get parent with full related resource included
122
+ const parent = await scope.get({
123
+ id: context.id,
124
+ queryParams: {
125
+ include: [context.relationshipName],
126
+ fields: context.queryParams.fields // Respect any field selections for the parent
127
+ },
128
+ transaction: context.transaction,
129
+ simplified: false,
130
+ isTransport: params.isTransport
131
+ });
132
+
133
+ // Extract the related resource from the parent's relationships
134
+ const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id;
135
+ if (!relatedId) {
136
+ return {
137
+ links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
138
+ data: null
139
+ };
140
+ }
141
+
142
+ // Find the full related resource in the included array
143
+ // The include system already fetched it for us!
144
+ const relatedResource = parent.included?.find(
145
+ r => r.type === targetType && r.id === relatedId
146
+ );
147
+
148
+ return {
149
+ links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
150
+ data: relatedResource || null
151
+ };
152
+ }
153
+ }
154
+
155
+ // Handle simple hasMany (one-to-many)
156
+ if (relDef.hasMany && !relDef.through) {
157
+ // Check if this is a polymorphic relationship using 'via'
158
+ if (relDef.via) {
159
+ // Polymorphic hasMany relationship
160
+ // Example: publishers hasMany reviews via reviewable
161
+ const targetRelationships = scopes[targetType].vars.schemaInfo.schemaRelationships;
162
+ const viaRel = targetRelationships?.[relDef.via];
163
+
164
+ if (!viaRel?.belongsToPolymorphic) {
165
+ throw new RestApiResourceError(
166
+ `Via relationship '${relDef.via}' not found or not polymorphic in '${targetType}'`,
167
+ { subtype: 'invalid_via_relationship' }
168
+ );
169
+ }
170
+
171
+ const { typeField, idField } = viaRel.belongsToPolymorphic;
172
+
173
+ // Add polymorphic filters
174
+ const filters = {
175
+ ...context.queryParams.filters,
176
+ [typeField]: scopeName,
177
+ [idField]: context.id
178
+ };
179
+
180
+ const result = await api.resources[targetType].query({
181
+ queryParams: { ...context.queryParams, filters },
182
+ transaction: context.transaction,
183
+ simplified: false,
184
+ isTransport: params.isTransport
185
+ });
186
+
187
+ if (!result.links) {
188
+ result.links = {};
189
+ }
190
+ result.links.self = `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}`;
191
+ return result;
192
+ } else {
193
+ // Regular hasMany with foreignKey
194
+ const filters = {
195
+ ...context.queryParams.filters,
196
+ [relDef.foreignKey]: context.id
197
+ };
198
+
199
+ const result = await api.resources[targetType].query({
200
+ queryParams: { ...context.queryParams, filters },
201
+ transaction: context.transaction,
202
+ simplified: false,
203
+ isTransport: params.isTransport
204
+ });
205
+
206
+ if (!result.links) {
207
+ result.links = {};
208
+ }
209
+ result.links.self = `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}`;
210
+ return result;
211
+ }
212
+ }
213
+
214
+ // Handle many-to-many relationships (hasMany with through)
215
+ // For example: GET /api/authors/1/books (where authors and books are linked via book_authors)
216
+ if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
217
+ // APPROACH: Query the pivot table directly
218
+ //
219
+ // Instead of:
220
+ // 1. Getting parent with relationship included to find related IDs
221
+ // 2. Trying to filter by id:[array] which doesn't work
222
+ //
223
+ // We query the pivot table (e.g., book_authors) filtered by the parent ID
224
+ // and include the target resources. This is simpler and uses existing
225
+ // API functionality without needing special array filter support.
226
+
227
+ const pivotResource = relDef.through;
228
+ const foreignKey = relDef.foreignKey;
229
+ const otherKey = relDef.otherKey;
230
+
231
+ // These should already be validated by scope-validations.js
232
+ if (!foreignKey || !otherKey) {
233
+ throw new Error(`Missing foreignKey or otherKey in many-to-many relationship`);
234
+ }
235
+
236
+ // We need to find the relationship name on the pivot table that points to the target
237
+ // The pivot table should have a belongsTo relationship to the target resource
238
+ const pivotScope = scopes[pivotResource];
239
+ if (!pivotScope) {
240
+ throw new RestApiResourceError(
241
+ `Pivot table resource '${pivotResource}' not found`,
242
+ { subtype: 'pivot_table_not_found' }
243
+ );
244
+ }
245
+
246
+ // Find the relationship name on the pivot table that points to the target
247
+ let targetRelationshipName = null;
248
+ const pivotSchema = pivotScope.vars.schemaInfo.schemaStructure;
249
+ for (const [fieldName, fieldDef] of Object.entries(pivotSchema)) {
250
+ if (fieldDef.belongsTo === targetType && fieldDef.as) {
251
+ targetRelationshipName = fieldDef.as;
252
+ break;
253
+ }
254
+ }
255
+
256
+ if (!targetRelationshipName) {
257
+ throw new RestApiResourceError(
258
+ `Cannot find relationship to '${targetType}' in pivot table '${pivotResource}'`,
259
+ { subtype: 'pivot_relationship_not_found' }
260
+ );
261
+ }
262
+
263
+ // Build filters for the pivot table
264
+ const pivotFilters = {
265
+ [foreignKey]: context.id
266
+ };
267
+
268
+ // Query the pivot table with the target included
269
+ const pivotResult = await api.resources[pivotResource].query({
270
+ queryParams: {
271
+ filters: pivotFilters,
272
+ include: [targetRelationshipName],
273
+ // Pass through other query params that should apply to the included resources
274
+ fields: context.queryParams.fields,
275
+ sort: context.queryParams.sort,
276
+ page: context.queryParams.page
277
+ },
278
+ transaction: context.transaction,
279
+ simplified: false,
280
+ isTransport: params.isTransport
281
+ });
282
+
283
+ // Extract the included target resources
284
+ const includedResources = pivotResult.included?.filter(r => r.type === targetType) || [];
285
+
286
+ // Build the response
287
+ return {
288
+ links: {
289
+ self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}`
290
+ },
291
+ data: includedResources,
292
+ meta: pivotResult.meta
293
+ };
294
+ }
295
+ }
@@ -0,0 +1,62 @@
1
+
2
+ import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
3
+ import { findRelationshipDefinition } from "./common.js";
4
+
5
+ /**
6
+ * GET RELATIONSHIP
7
+ * Retrieves relationship linkage data (just resource identifiers)
8
+ * GET /api/articles/1/relationships/author
9
+ *
10
+ * @param {string} id - The ID of the resource
11
+ * @param {string} relationshipName - The name of the relationship
12
+ * @returns {Promise<object>} Relationship linkage with links
13
+ */
14
+ export default async function getRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
15
+ context.method = 'getRelationship';
16
+ context.id = params.id;
17
+ context.relationshipName = params.relationshipName;
18
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo;
19
+
20
+ // Validate the relationship exists
21
+ const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
22
+
23
+ if (!relDef) {
24
+ throw new RestApiResourceError(
25
+ `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
26
+ { subtype: 'relationship_not_found' }
27
+ );
28
+ }
29
+
30
+ // Check permissions
31
+ await runHooks('checkPermissions');
32
+ await runHooks('checkPermissionsGetRelationship');
33
+
34
+ // Reuse existing get method with minimal fields
35
+ const fullRecord = await scope.get({
36
+ id: context.id,
37
+ queryParams: {
38
+ include: [context.relationshipName],
39
+ fields: { [scopeName]: vars.idProperty || 'id' }
40
+ },
41
+ transaction: context.transaction,
42
+ simplified: false,
43
+ isTransport: params.isTransport
44
+ });
45
+
46
+ if (!fullRecord || !fullRecord.data) {
47
+ throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
48
+ }
49
+
50
+ // Extract just the relationship data
51
+ const relationshipData = fullRecord.data.relationships?.[context.relationshipName];
52
+
53
+ // Build response with links
54
+ return {
55
+ links: {
56
+ self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/relationships/${context.relationshipName}`,
57
+ related: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}`
58
+ },
59
+ data: relationshipData?.data || (relDef.hasMany || relDef.manyToMany ? [] : null)
60
+ };
61
+ };
62
+
@@ -0,0 +1,187 @@
1
+ import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
2
+ import { validateGetPayload } from '../lib/querying-writing/payload-validators.js';
3
+ import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js';
4
+ import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js';
5
+ import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js';
6
+ import { cascadeConfig } from './common.js';
7
+
8
+ /**
9
+ * GET
10
+ * Retrieves a single resource by its type and ID.
11
+ * @param {(string|number)} id - The unique ID of the resource to fetch.
12
+ * @param {object} [queryParams={}] - Optional. An object to customize the query.
13
+ * @param {string} [queryParams.include] - A comma-separated string of relationship paths to sideload (e.g., "authors,publisher").
14
+ * @param {object} [queryParams.fields] - An object to request only specific fields (sparse fieldsets). Keys are resource types, values are comma-separated field names.
15
+ * @returns {Promise<object>} A Promise that resolves to the JSON:API response document.
16
+ */
17
+ export default async function getMethod({
18
+ params,
19
+ context,
20
+ vars,
21
+ helpers,
22
+ scope,
23
+ scopes,
24
+ runHooks,
25
+ apiOptions,
26
+ pluginOptions,
27
+ scopeOptions,
28
+ scopeName,
29
+ api
30
+ }) {
31
+ context.method = 'get';
32
+
33
+ // Determine which simplified setting to use based on transport
34
+ const isTransport = params.isTransport === true;
35
+
36
+ // Use vars which automatically cascade from scope to global
37
+ const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi;
38
+
39
+ // Get simplified setting - from params only (per-call override) or use default
40
+ context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified;
41
+
42
+ // Assign common context properties
43
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo;
44
+ context.queryParams = params.queryParams || {};
45
+
46
+ // These only make sense as parameter per query
47
+ context.queryParams.fields = cascadeConfig('fields', [context.queryParams], {});
48
+ context.queryParams.include = cascadeConfig('include', [context.queryParams], []);
49
+
50
+ context.transaction = params.transaction;
51
+ context.db = context.transaction || api.knex.instance;
52
+
53
+ context.scopeName = scopeName;
54
+
55
+ // These are just shortcuts used in this function and will be returned
56
+ const schemaStructure = context.schemaInfo.schema.structure;
57
+ const schemaRelationships = context.schemaInfo.schemaRelationships;
58
+
59
+ context.id = params.id;
60
+
61
+ // Validate GET request to ensure required parameters are present and properly formatted.
62
+ // This checks that 'id' parameter exists and is not empty (you can't GET without an ID),
63
+ // validates 'include' contains valid relationship names (not arbitrary fields), and ensures
64
+ // 'fields' for sparse fieldsets follow the format fields[type]=comma,separated,list.
65
+ // Example: validates id: '123' exists, include: ['author', 'tags'] are real relationships.
66
+ validateGetPayload({ id: context.id, queryParams: context.queryParams }, vars.includeDepthLimit);
67
+
68
+ // Fetch minimal record for authorization checks
69
+ const minimalRecord = await helpers.dataGetMinimal({
70
+ scopeName,
71
+ context,
72
+ runHooks
73
+ });
74
+
75
+ if (!minimalRecord) {
76
+ throw new RestApiResourceError(
77
+ `Resource not found`,
78
+ {
79
+ subtype: 'not_found',
80
+ resourceType: scopeName,
81
+ resourceId: context.id
82
+ }
83
+ );
84
+ }
85
+
86
+ context.minimalRecord = minimalRecord;
87
+
88
+ // Centralised checkPermissions function
89
+ await scope.checkPermissions({
90
+ method: 'get',
91
+ auth: context.auth,
92
+ id: context.id,
93
+ minimalRecord: context.minimalRecord,
94
+ transaction: context.transaction
95
+ });
96
+
97
+ await runHooks('beforeData');
98
+ await runHooks('beforeDataGet');
99
+
100
+ context.record = await helpers.dataGet({
101
+ scopeName,
102
+ context,
103
+ runHooks
104
+ });
105
+
106
+ // Check if record was found - storage layer returns null/undefined for non-existent records.
107
+ // This generates a proper 404 error with JSON:API error format instead of returning empty data.
108
+ if (!context.record || !context.record.data) {
109
+ throw new RestApiResourceError(
110
+ `Resource not found`,
111
+ {
112
+ subtype: 'not_found',
113
+ resourceType: scopeName,
114
+ resourceId: context.id
115
+ }
116
+ );
117
+ }
118
+
119
+ // Normalize database values (e.g., convert 1/0 to true/false for booleans)
120
+ context.record = normalizeRecordAttributes(context.record, scopes);
121
+
122
+ await runHooks('checkDataPermissions');
123
+ await runHooks('checkDataPermissionsGet');
124
+
125
+ // Make a backup
126
+ context.originalRecord = structuredClone(context.record);
127
+
128
+ // This will enhance record, which is the WHOLE JSON:API record
129
+ await runHooks('enrichRecord');
130
+
131
+ // Get computed field information for main resource
132
+ const computedFields = scope.vars.schemaInfo?.computed || {};
133
+ const requestedFields = context.queryParams.fields?.[scopeName];
134
+ const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields);
135
+
136
+ // Enrich attributes for the main resource
137
+ // Pass computedDependencies from context (set by dataGet in Knex plugin)
138
+ // This tells enrichAttributes which fields to remove after computation
139
+ context.record.data.attributes = await scope.enrichAttributes({
140
+ attributes: context.record.data.attributes,
141
+ parentContext: context,
142
+ requestedComputedFields: requestedComputedFields,
143
+ isMainResource: true,
144
+ computedDependencies: context.computedDependencies // Fields to remove if not requested
145
+ });
146
+
147
+ for (const entry of (context.record.included || [])) {
148
+ const entryScope = scopes[entry.type];
149
+ const entryComputed = entryScope.vars.schemaInfo?.computed || {};
150
+ const entryRequestedFields = context.queryParams.fields?.[entry.type];
151
+ const entryRequestedComputed = getRequestedComputedFields(
152
+ entry.type,
153
+ entryRequestedFields,
154
+ entryComputed
155
+ );
156
+
157
+ entry.attributes = await entryScope.enrichAttributes({
158
+ attributes: entry.attributes,
159
+ parentContext: context,
160
+ requestedComputedFields: entryRequestedComputed,
161
+ isMainResource: false,
162
+ computedDependencies: entry.__$jsonrestapi_computed_deps$__
163
+ });
164
+ }
165
+
166
+ await runHooks('enrichRecordWithRelationships');
167
+
168
+ // The called hooks should NOT change context.record
169
+ await runHooks('finish');
170
+ await runHooks('finishGet');
171
+
172
+ // Get schema info for transformation
173
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo;
174
+
175
+ // Transform output if in simplified mode
176
+ if (context.simplified) {
177
+ // Convert JSON:API response back to simplified format
178
+ // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
179
+ // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
180
+ return transformJsonApiToSimplified(
181
+ { record: context.record },
182
+ { context: { schemaStructure, schemaRelationships, scopes } }
183
+ );
184
+ }
185
+
186
+ return context.record;
187
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * PATCH RELATIONSHIP
3
+ * Completely replaces a relationship
4
+ * PATCH /api/articles/1/relationships/author
5
+ *
6
+ * @param {string} id - The ID of the resource
7
+ * @param {string} relationshipName - The name of the relationship
8
+ * @param {object|array} relationshipData - New relationship data
9
+ * @returns {Promise<void>} 204 No Content
10
+ */
11
+ export default async function patchRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
12
+ context.method = 'patchRelationship';
13
+ context.id = params.id;
14
+ context.relationshipName = params.relationshipName;
15
+
16
+ // Transaction handling
17
+ context.transaction = params.transaction ||
18
+ (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
19
+ context.shouldCommit = !params.transaction && !!context.transaction;
20
+ context.db = context.transaction || api.knex.instance
21
+
22
+ try {
23
+ // Check permissions
24
+ await runHooks('checkPermissions');
25
+ await runHooks('checkPermissionsPatchRelationship');
26
+
27
+ // Reuse existing patch with relationship data
28
+ await scope.patch({
29
+ id: context.id,
30
+ inputRecord: {
31
+ data: {
32
+ type: scopeName,
33
+ id: context.id,
34
+ relationships: {
35
+ [params.relationshipName]: { data: params.relationshipData }
36
+ }
37
+ }
38
+ },
39
+ transaction: context.transaction,
40
+ simplified: false,
41
+ isTransport: params.isTransport
42
+ });
43
+
44
+ await runHooks('finish');
45
+ await runHooks('finishPatchRelationship');
46
+
47
+ if (context.shouldCommit) {
48
+ await context.transaction.commit();
49
+ }
50
+
51
+ return; // 204 No Content
52
+
53
+ } catch (error) {
54
+ await handleWriteMethodError(error, context, 'PATCH_RELATIONSHIP', scopeName, log);
55
+ }
56
+ }