json-rest-api 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +5 -9
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -1,5 +1,6 @@
1
1
  import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
2
2
  import { findRelationshipDefinition } from "./common.js";
3
+ import { buildRelationshipUrl } from "../lib/querying/url-helpers.js";
3
4
 
4
5
  /**
5
6
  * GET RELATED
@@ -30,9 +31,15 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
30
31
  );
31
32
  }
32
33
 
33
- // Determine target type - relationships are stored as strings
34
- const targetType = relDef.belongsTo || relDef.hasMany ||
35
- relDef.manyToMany || relDef.hasOne;
34
+ // Determine target type based on relationship type
35
+ let targetType;
36
+ if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
37
+ targetType = relDef.target;
38
+ } else if (relDef.type === 'manyToMany') {
39
+ targetType = context.relationshipName; // For manyToMany, use the relationship name
40
+ } else if (relDef.belongsTo) {
41
+ targetType = relDef.belongsTo; // belongsTo still in schema
42
+ }
36
43
 
37
44
  if (!targetType || !scopes[targetType]) {
38
45
  throw new RestApiResourceError(
@@ -57,7 +64,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
57
64
 
58
65
  // Handle to-one relationships (belongsTo and hasOne)
59
66
  // For example: GET /api/books/1/country or GET /api/books/1/publisher
60
- if (relDef.belongsTo || relDef.hasOne) {
67
+ if (relDef.belongsTo || relDef.type === 'hasOne') {
61
68
  // OPTIMIZATION: Detect if we actually need to make two API calls
62
69
  //
63
70
  // The naive approach always makes 2 calls:
@@ -95,7 +102,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
95
102
  const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id;
96
103
  if (!relatedId) {
97
104
  return {
98
- links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
105
+ links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
99
106
  data: null
100
107
  };
101
108
  }
@@ -110,7 +117,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
110
117
  });
111
118
 
112
119
  return {
113
- links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
120
+ links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
114
121
  data: related.data,
115
122
  included: related.included
116
123
  };
@@ -134,7 +141,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
134
141
  const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id;
135
142
  if (!relatedId) {
136
143
  return {
137
- links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
144
+ links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
138
145
  data: null
139
146
  };
140
147
  }
@@ -146,14 +153,14 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
146
153
  );
147
154
 
148
155
  return {
149
- links: { self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}` },
156
+ links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
150
157
  data: relatedResource || null
151
158
  };
152
159
  }
153
160
  }
154
161
 
155
- // Handle simple hasMany (one-to-many)
156
- if (relDef.hasMany && !relDef.through) {
162
+ // Handle simple hasMany (one-to-many, NOT many-to-many)
163
+ if (relDef.type === 'hasMany') {
157
164
  // Check if this is a polymorphic relationship using 'via'
158
165
  if (relDef.via) {
159
166
  // Polymorphic hasMany relationship
@@ -187,13 +194,28 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
187
194
  if (!result.links) {
188
195
  result.links = {};
189
196
  }
190
- result.links.self = `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}`;
197
+ result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false);
191
198
  return result;
192
199
  } else {
193
200
  // Regular hasMany with foreignKey
201
+ // Need to find the relationship name in the target resource that points back to this resource
202
+ const targetSchema = scopes[targetType].vars.schemaInfo.schemaStructure;
203
+ let relationshipFilterName = null;
204
+
205
+ // Find the field in target schema that has the foreign key and get its relationship name
206
+ for (const [fieldName, fieldDef] of Object.entries(targetSchema)) {
207
+ if (fieldName === relDef.foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
208
+ relationshipFilterName = fieldDef.as;
209
+ break;
210
+ }
211
+ }
212
+
213
+ // Fall back to foreign key if no relationship name found (shouldn't happen with proper schema)
214
+ const filterKey = relationshipFilterName || relDef.foreignKey;
215
+
194
216
  const filters = {
195
217
  ...context.queryParams.filters,
196
- [relDef.foreignKey]: context.id
218
+ [filterKey]: context.id
197
219
  };
198
220
 
199
221
  const result = await api.resources[targetType].query({
@@ -206,35 +228,50 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
206
228
  if (!result.links) {
207
229
  result.links = {};
208
230
  }
209
- result.links.self = `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}`;
231
+ result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false);
210
232
  return result;
211
233
  }
212
234
  }
213
235
 
214
- // Handle many-to-many relationships (hasMany with through)
236
+ // Handle many-to-many relationships
215
237
  // 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
-
238
+ if (relDef?.through) {
239
+ if (api.anyapi?.links?.listMany) {
240
+ const identifiers = await api.anyapi.links.listMany({
241
+ context,
242
+ scopeName,
243
+ relName: context.relationshipName,
244
+ });
245
+
246
+ const results = [];
247
+ for (const identifier of identifiers) {
248
+ const related = await api.resources[targetType].get({
249
+ id: identifier.id,
250
+ queryParams: context.queryParams,
251
+ transaction: context.transaction,
252
+ simplified: false,
253
+ isTransport: params.isTransport,
254
+ });
255
+ if (related?.data) {
256
+ results.push(related.data);
257
+ }
258
+ }
259
+
260
+ return {
261
+ links: {
262
+ self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
263
+ },
264
+ data: results,
265
+ };
266
+ }
267
+
268
+ // Legacy fallback
227
269
  const pivotResource = relDef.through;
228
270
  const foreignKey = relDef.foreignKey;
229
271
  const otherKey = relDef.otherKey;
230
-
231
- // These should already be validated by scope-validations.js
232
272
  if (!foreignKey || !otherKey) {
233
273
  throw new Error(`Missing foreignKey or otherKey in many-to-many relationship`);
234
274
  }
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
275
  const pivotScope = scopes[pivotResource];
239
276
  if (!pivotScope) {
240
277
  throw new RestApiResourceError(
@@ -242,35 +279,22 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
242
279
  { subtype: 'pivot_table_not_found' }
243
280
  );
244
281
  }
245
-
246
- // Find the relationship name on the pivot table that points to the target
247
- let targetRelationshipName = null;
248
282
  const pivotSchema = pivotScope.vars.schemaInfo.schemaStructure;
283
+ let parentRelationshipName = null;
249
284
  for (const [fieldName, fieldDef] of Object.entries(pivotSchema)) {
250
- if (fieldDef.belongsTo === targetType && fieldDef.as) {
251
- targetRelationshipName = fieldDef.as;
285
+ if (fieldName === foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
286
+ parentRelationshipName = fieldDef.as;
252
287
  break;
253
288
  }
254
289
  }
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
290
+ const filterKey = parentRelationshipName || foreignKey;
264
291
  const pivotFilters = {
265
- [foreignKey]: context.id
292
+ [filterKey]: context.id
266
293
  };
267
-
268
- // Query the pivot table with the target included
269
294
  const pivotResult = await api.resources[pivotResource].query({
270
295
  queryParams: {
271
296
  filters: pivotFilters,
272
- include: [targetRelationshipName],
273
- // Pass through other query params that should apply to the included resources
297
+ include: [context.relationshipName],
274
298
  fields: context.queryParams.fields,
275
299
  sort: context.queryParams.sort,
276
300
  page: context.queryParams.page
@@ -279,17 +303,51 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
279
303
  simplified: false,
280
304
  isTransport: params.isTransport
281
305
  });
282
-
283
- // Extract the included target resources
284
- const includedResources = pivotResult.included?.filter(r => r.type === targetType) || [];
285
-
286
- // Build the response
306
+ let includedResources = pivotResult.included?.filter((r) => r.type === targetType) || [];
307
+
308
+ if (includedResources.length === 0) {
309
+ const pivotData = pivotResult.data || [];
310
+ const relatedIds = [...new Set(pivotData
311
+ .map((item) => {
312
+ const attrId = item?.attributes?.[otherKey];
313
+ if (attrId !== null && attrId !== undefined) {
314
+ return String(attrId);
315
+ }
316
+ const relationships = item?.relationships || {};
317
+ for (const rel of Object.values(relationships)) {
318
+ const data = rel?.data;
319
+ if (data?.type === targetType && data?.id != null) {
320
+ return String(data.id);
321
+ }
322
+ }
323
+ return null;
324
+ })
325
+ .filter((id) => id !== null)
326
+ )];
327
+
328
+ if (relatedIds.length > 0) {
329
+ includedResources = [];
330
+ for (const relatedId of relatedIds) {
331
+ const related = await api.resources[targetType].get({
332
+ id: relatedId,
333
+ queryParams: context.queryParams,
334
+ transaction: context.transaction,
335
+ simplified: false,
336
+ isTransport: params.isTransport,
337
+ });
338
+ if (related?.data) {
339
+ includedResources.push(related.data);
340
+ }
341
+ }
342
+ }
343
+ }
344
+
287
345
  return {
288
346
  links: {
289
- self: `${vars.publicBaseUrl || vars.transport?.mountPath || ''}/${scopeName}/${context.id}/${context.relationshipName}`
347
+ self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
290
348
  },
291
349
  data: includedResources,
292
350
  meta: pivotResult.meta
293
351
  };
294
352
  }
295
- }
353
+ }
@@ -1,6 +1,7 @@
1
1
 
2
2
  import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
3
3
  import { findRelationshipDefinition } from "./common.js";
4
+ import { buildRelationshipUrl } from "../lib/querying/url-helpers.js";
4
5
 
5
6
  /**
6
7
  * GET RELATIONSHIP
@@ -53,10 +54,10 @@ export default async function getRelationshipMethod ({ params, context, vars, he
53
54
  // Build response with links
54
55
  return {
55
56
  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}`
57
+ self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, true),
58
+ related: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
58
59
  },
59
- data: relationshipData?.data || (relDef.hasMany || relDef.manyToMany ? [] : null)
60
+ data: relationshipData?.data || (relDef.type === 'hasMany' || relDef.type === 'manyToMany' ? [] : null)
60
61
  };
61
62
  };
62
63
 
@@ -53,7 +53,7 @@ export default async function getMethod({
53
53
  context.scopeName = scopeName;
54
54
 
55
55
  // These are just shortcuts used in this function and will be returned
56
- const schemaStructure = context.schemaInfo.schema.structure;
56
+ const schemaStructure = context.schemaInfo.schemaInstance.structure;
57
57
  const schemaRelationships = context.schemaInfo.schemaRelationships;
58
58
 
59
59
  context.id = params.id;
@@ -88,10 +88,7 @@ export default async function getMethod({
88
88
  // Centralised checkPermissions function
89
89
  await scope.checkPermissions({
90
90
  method: 'get',
91
- auth: context.auth,
92
- id: context.id,
93
- minimalRecord: context.minimalRecord,
94
- transaction: context.transaction
91
+ originalContext: context,
95
92
  });
96
93
 
97
94
  await runHooks('beforeData');
@@ -102,10 +102,7 @@ export default async function patchMethod({
102
102
  // Centralised checkPermissions function
103
103
  await scope.checkPermissions({
104
104
  method: 'patch',
105
- auth: context.auth,
106
- id: context.id,
107
- minimalRecord: context.minimalRecord,
108
- transaction: context.transaction
105
+ originalContext: context,
109
106
  });
110
107
 
111
108
  // Merge belongsTo updates into attributes before patching the record
@@ -142,18 +139,20 @@ export default async function patchMethod({
142
139
  // Process many-to-many relationships after main record update
143
140
  // For PATCH, we only update the relationships that were explicitly provided
144
141
  for (const { relName, relDef, relData } of manyToManyRelationships) {
145
-
146
- // Validate pivot resource exists
142
+ if (relDef?.through && api.anyapi?.links?.syncMany) {
143
+ await api.anyapi.links.syncMany({
144
+ context,
145
+ scopeName,
146
+ relName,
147
+ relDef,
148
+ relData,
149
+ isUpdate: true,
150
+ });
151
+ continue;
152
+ }
153
+
147
154
  validatePivotResource(scopes, relDef, relName);
148
-
149
- // Update many-to-many relationships using intelligent synchronization that preserves pivot data.
150
- // This compares current relationships with desired state: removes records no longer needed,
151
- // adds new relationships, and crucially preserves existing pivot records with their metadata
152
- // (like created_at timestamps or extra pivot fields). This is superior to delete-all-recreate
153
- // because it maintains audit trails and custom pivot data.
154
- // Example: If article has tags [1,2,3] and you update to [2,3,4], it keeps the pivot records
155
- // for tags 2&3 (preserving their created_at), deletes tag 1, and adds new record for tag 4.
156
- // Example: If article has tags [1,2,3] and update sends [2,3,4], tag 1 is removed, tags 2,3 kept, tag 4 added
155
+
157
156
  await updateManyToManyRelationship(null, {
158
157
  api,
159
158
  context: {
@@ -190,4 +189,4 @@ export default async function patchMethod({
190
189
  } catch (error) {
191
190
  await handleWriteMethodError(error, context, 'PATCH', scopeName, log, runHooks);
192
191
  }
193
- }
192
+ }
@@ -1,7 +1,6 @@
1
- import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
1
+ import { RestApiResourceError, RestApiValidationError, RestApiPayloadError } from "../../../lib/rest-api-errors.js";
2
2
  import { findRelationshipDefinition, handleWriteMethodError } from "./common.js";
3
3
  import { createPivotRecords } from "../lib/writing/many-to-many-manipulations.js";
4
- import { RestApiValidationError } from "../../../lib/rest-api-errors.js";
5
4
 
6
5
  /**
7
6
  * POST RELATIONSHIP
@@ -36,7 +35,7 @@ import { RestApiValidationError } from "../../../lib/rest-api-errors.js";
36
35
  );
37
36
  }
38
37
 
39
- if (!relDef.hasMany && !relDef.manyToMany) {
38
+ if (relDef.type !== 'hasMany' && relDef.type !== 'manyToMany') {
40
39
  throw new RestApiValidationError(
41
40
  `Cannot POST to to-one relationship '${context.relationshipName}'. Use PATCH instead.`,
42
41
  { fields: ['data'] }
@@ -62,17 +61,25 @@ import { RestApiValidationError } from "../../../lib/rest-api-errors.js";
62
61
  }
63
62
 
64
63
  // Add relationships
65
- if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
66
- const manyToManyDef = relDef.manyToMany || {
67
- through: relDef.through,
68
- foreignKey: relDef.foreignKey,
69
- otherKey: relDef.otherKey
70
- };
71
-
72
- await createPivotRecords(api, context.id, manyToManyDef, params.relationshipData, context.transaction);
73
- } else {
64
+ if (relDef?.through) {
65
+ if (api.anyapi?.links?.attachMany) {
66
+ await api.anyapi.links.attachMany({
67
+ context,
68
+ scopeName,
69
+ relName: context.relationshipName,
70
+ relDef,
71
+ relData: params.relationshipData,
72
+ });
73
+ } else {
74
+ await createPivotRecords(api, context.id, {
75
+ through: relDef.through,
76
+ foreignKey: relDef.foreignKey,
77
+ otherKey: relDef.otherKey
78
+ }, params.relationshipData, context.transaction);
79
+ }
80
+ } else if (relDef.type === 'hasMany') {
74
81
  // Update foreign keys for hasMany
75
- const targetType = relDef.hasMany;
82
+ const targetType = relDef.target;
76
83
  for (const identifier of params.relationshipData) {
77
84
  await api.resources[targetType].patch({
78
85
  id: identifier.id,
@@ -101,4 +108,4 @@ import { RestApiValidationError } from "../../../lib/rest-api-errors.js";
101
108
  } catch (error) {
102
109
  await handleWriteMethodError(error, context, 'POST_RELATIONSHIP', scopeName, log, runHooks);
103
110
  }
104
- };
111
+ };
@@ -84,11 +84,20 @@ export default async function postMethod({
84
84
  runHooks
85
85
  });
86
86
 
87
+ if (context.inputRecord?.data) {
88
+ const inputData = context.inputRecord.data;
89
+ context.minimalRecord = {
90
+ type: scopeName,
91
+ ...(inputData.id !== undefined && inputData.id !== null ? { id: String(inputData.id) } : {}),
92
+ attributes: structuredClone(inputData.attributes || {}),
93
+ relationships: structuredClone(inputData.relationships || {})
94
+ };
95
+ }
96
+
87
97
  // Centralised checkPermissions function
88
98
  await scope.checkPermissions({
89
99
  method: 'post',
90
- auth: context.auth,
91
- transaction: context.transaction
100
+ originalContext: context,
92
101
  });
93
102
 
94
103
  await runHooks('beforeDataCall');
@@ -116,16 +125,19 @@ export default async function postMethod({
116
125
 
117
126
  // Process many-to-many relationships after main record creation
118
127
  for (const { relName, relDef, relData } of manyToManyRelationships) {
128
+ if (api.anyapi?.links?.attachMany && relDef?.through) {
129
+ await api.anyapi.links.attachMany({
130
+ context,
131
+ scopeName,
132
+ relName,
133
+ relDef,
134
+ relData,
135
+ });
136
+ continue;
137
+ }
138
+
119
139
  // Validate pivot resource exists
120
140
  validatePivotResource(scopes, relDef, relName);
121
-
122
- // Create pivot records in the through table to establish many-to-many relationships.
123
- // This creates records in the intermediary table (like 'article_tags') that link the main
124
- // resource to each related resource. Before creating each pivot record, it validates that
125
- // the related resource actually exists (preventing orphaned relationships). If validateExists
126
- // is false, it skips validation for performance in bulk operations.
127
- // Example: article.tags: [{id: '1'}, {id: '2'}] creates two article_tags records:
128
- // {article_id: 100, tag_id: 1} and {article_id: 100, tag_id: 2}
129
141
  await createPivotRecords(api, context.id, relDef, relData, context.transaction);
130
142
  }
131
143
 
@@ -154,4 +166,4 @@ export default async function postMethod({
154
166
  } catch (error) {
155
167
  await handleWriteMethodError(error, context, 'POST', scopeName, log, runHooks);
156
168
  }
157
- }
169
+ }
@@ -114,11 +114,7 @@ export default async function putMethod({
114
114
 
115
115
  // Collect all defined relationships for this resource
116
116
  for (const [relName, relDef] of Object.entries(schemaRelationships || {})) {
117
- if (relDef.manyToMany) {
118
- allRelationships[relName] = { type: 'manyToMany', relDef: relDef.manyToMany };
119
- }
120
- // Also recognize hasMany with through as many-to-many
121
- else if (relDef.hasMany && relDef.through) {
117
+ if (relDef.type === 'manyToMany') {
122
118
  allRelationships[relName] = {
123
119
  type: 'manyToMany',
124
120
  relDef: {
@@ -174,11 +170,7 @@ export default async function putMethod({
174
170
  // Centralised checkPermissions function
175
171
  await scope.checkPermissions({
176
172
  method: 'put',
177
- auth: context.auth,
178
- id: context.id,
179
- minimalRecord: context.minimalRecord,
180
- isUpdate: context.isUpdate,
181
- transaction: context.transaction
173
+ originalContext: context,
182
174
  });
183
175
 
184
176
  await runHooks('beforeDataCall');
@@ -205,13 +197,21 @@ export default async function putMethod({
205
197
 
206
198
  // Process many-to-many relationships after main record update/creation
207
199
  for (const { relName, relDef, relData } of manyToManyRelationships) {
208
- // Validate pivot resource exists
200
+ if (relDef?.through && api.anyapi?.links?.syncMany) {
201
+ await api.anyapi.links.syncMany({
202
+ context,
203
+ scopeName,
204
+ relName,
205
+ relDef,
206
+ relData,
207
+ isUpdate: context.isUpdate,
208
+ });
209
+ continue;
210
+ }
211
+
209
212
  await validatePivotResource(scopes, relDef, relName);
210
-
211
- // Use smart sync for updates (like industry standard ORMs)
213
+
212
214
  if (context.isUpdate) {
213
- // Update many-to-many relationships using intelligent synchronization
214
- // This preserves existing pivot data while efficiently updating relationships
215
215
  await updateManyToManyRelationship(null, {
216
216
  api,
217
217
  context: {
@@ -221,11 +221,8 @@ export default async function putMethod({
221
221
  transaction: context.transaction
222
222
  }
223
223
  });
224
- } else {
225
- // For new records, just create the pivot records
226
- if (relData.length > 0) {
227
- await createPivotRecords(api, context.id, relDef, relData, context.transaction);
228
- }
224
+ } else if (relData.length > 0) {
225
+ await createPivotRecords(api, context.id, relDef, relData, context.transaction);
229
226
  }
230
227
  }
231
228
 
@@ -254,4 +251,4 @@ export default async function putMethod({
254
251
  } catch (error) {
255
252
  await handleWriteMethodError(error, context, 'PUT', scopeName, log, runHooks);
256
253
  }
257
- }
254
+ }
@@ -61,8 +61,8 @@ export default async function queryMethod({
61
61
  context.scopeName = scopeName;
62
62
 
63
63
  // These are just shortcuts used in this function and will be returned
64
- const searchSchema = context.schemaInfo.searchSchema;
65
- const schemaStructure = context.schemaInfo.schema.structure;
64
+ const searchSchema = context.schemaInfo.searchSchemaInstance;
65
+ const schemaStructure = context.schemaInfo.schemaInstance.structure;
66
66
  const schemaRelationships = context.schemaInfo.schemaRelationships;
67
67
 
68
68
  // Sortable fields and sort (mab)
@@ -125,8 +125,7 @@ export default async function queryMethod({
125
125
  // Centralised checkPermissions function
126
126
  await scope.checkPermissions({
127
127
  method: 'query',
128
- auth: context.auth,
129
- transaction: context.transaction
128
+ originalContext: context,
130
129
  });
131
130
 
132
131
  await runHooks('beforeData');
@@ -0,0 +1,13 @@
1
+ import { ValidationError } from 'hooked-api';
2
+
3
+ /**
4
+ * release
5
+ * Runs the hook that tells plugins to release resources
6
+ */
7
+ export default async ({ runHooks }) => {
8
+
9
+ // Run the addRoute hook to notify transport plugins
10
+ await runHooks('release');
11
+
12
+ return { };
13
+ }
@@ -17,6 +17,7 @@ import deleteMethod from './rest-api-plugin-methods/delete.js';
17
17
  import enrichAttributesMethod from './rest-api-plugin-methods/enrich-attributes.js';
18
18
  import checkPermissionsMethod from './rest-api-plugin-methods/check-permissions.js';
19
19
  import addRouteMethod from './rest-api-plugin-methods/add-route.js';
20
+ import releaseMethod from './rest-api-plugin-methods/release.js';
20
21
  import { defaultDataHelpers } from './lib/querying-writing/default-data-helpers.js';
21
22
  import { DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_INCLUDE_DEPTH_LIMIT } from './lib/querying-writing/knex-constants.js';
22
23
 
@@ -54,7 +55,6 @@ export const RestApiPlugin = {
54
55
  vars.queryDefaultLimit = restApiOptions.queryDefaultLimit || DEFAULT_QUERY_LIMIT
55
56
  vars.queryMaxLimit = restApiOptions.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
56
57
  vars.includeDepthLimit = restApiOptions.includeDepthLimit || DEFAULT_INCLUDE_DEPTH_LIMIT
57
- vars.publicBaseUrl = restApiOptions.publicBaseUrl || ''
58
58
  vars.enablePaginationCounts = restApiOptions.enablePaginationCounts || true
59
59
 
60
60
  // New simplified settings
@@ -127,6 +127,8 @@ export const RestApiPlugin = {
127
127
 
128
128
  addApiMethod('addRoute', addRouteMethod);
129
129
 
130
+ addApiMethod('release', releaseMethod);
131
+
130
132
  // Main REST methods
131
133
  addScopeMethod('query', queryMethod);
132
134
  addScopeMethod('get', getMethod);
@@ -161,8 +163,5 @@ export const RestApiPlugin = {
161
163
  // Add default getLocation helper for generating resource URLs
162
164
  // This can be overridden by storage plugins if needed
163
165
  helpers.getLocation = ({ scopeName, id }) => `/${scopeName}/${id}`
164
-
165
- // Helper to get the URL prefix for generating links
166
- helpers.getUrlPrefix = ({ scope }) => scope?.vars?.publicBaseUrl || vars.publicBaseUrl || vars.transport?.mountPath || ''
167
166
  }
168
167
  };