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,9 +1,13 @@
1
1
  export const MultiHomePlugin = {
2
2
  name: 'multihome',
3
- dependencies: ['rest-api', 'rest-api-knex'],
3
+ dependencies: ['rest-api'],
4
4
 
5
5
  install({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
6
6
 
7
+ if (!api.knex?.instance) {
8
+ throw new Error('Multihome plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)');
9
+ }
10
+
7
11
  // Get configuration - hooked-api namespaces options by plugin name
8
12
  const multihomeOptions = pluginOptions || {};
9
13
 
@@ -15,6 +19,31 @@ export const MultiHomePlugin = {
15
19
  allowMissing: multihomeOptions.allowMissing || false
16
20
  };
17
21
 
22
+ const resolveStorageAdapter = (scopeName, hookContext = {}) => {
23
+ if (!scopeName) return null;
24
+ if (hookContext.storageAdapter) return hookContext.storageAdapter;
25
+ if (hookContext.knexQuery?.storageAdapter) return hookContext.knexQuery.storageAdapter;
26
+ if (helpers.getStorageAdapter) {
27
+ return helpers.getStorageAdapter(scopeName);
28
+ }
29
+ return null;
30
+ };
31
+
32
+ const translateColumnReference = ({ field, tableName, scopeName, hookContext }) => {
33
+ const storageAdapter = resolveStorageAdapter(scopeName, hookContext);
34
+ const translated = storageAdapter?.translateColumn?.(field) ?? field;
35
+ if (tableName) {
36
+ return `${tableName}.${translated}`;
37
+ }
38
+ return translated;
39
+ };
40
+
41
+ const translateFilterValue = ({ field, value, scopeName, hookContext }) => {
42
+ const storageAdapter = resolveStorageAdapter(scopeName, hookContext);
43
+ if (!storageAdapter?.translateFilterValue) return value;
44
+ return storageAdapter.translateFilterValue(field, value);
45
+ };
46
+
18
47
  // Default extractor function
19
48
  function defaultSubdomainExtractor(request) {
20
49
  // Extract from subdomain (e.g., 'mobily' from 'mobily.app.com')
@@ -80,7 +109,8 @@ export const MultiHomePlugin = {
80
109
  });
81
110
 
82
111
  // Main filtering hook - adds WHERE clause to all queries
83
- addHook('knexQueryFiltering', 'multihome-filter', { beforePlugin: 'rest-api-knex' }, async ({ context, vars }) => {
112
+ log.debug('Registering multihome filter hook');
113
+ addHook('knexQueryFiltering', 'multihome-filter', {}, async ({ context, vars }) => {
84
114
  // Get query info from context
85
115
  const { query, tableName, scopeName } = context.knexQuery;
86
116
 
@@ -90,17 +120,23 @@ export const MultiHomePlugin = {
90
120
  return;
91
121
  }
92
122
 
93
- // Skip if no multihome context
94
- if (!context.auth?.multihome_id) {
95
- if (vars.multihome.requireAuth) {
96
- throw new Error('No multihome context available - cannot execute query');
97
- }
98
- return;
123
+ // Skip if no multihome context
124
+ if (!context.auth?.multihome_id) {
125
+ if (vars.multihome.requireAuth) {
126
+ throw new Error('No multihome context available - cannot execute query');
99
127
  }
128
+ return;
129
+ }
100
130
 
101
- // Check if this resource has multihome field
131
+ log.debug('Applying multihome tenant filter', {
132
+ scopeName,
133
+ tableName,
134
+ tenant: context.auth.multihome_id,
135
+ });
136
+
137
+ // Check if this resource has multihome field
102
138
  const scope = scopes[scopeName];
103
- const hasMultihomeField = scope?.vars?.schemaInfo?.schema?.structure?.[vars.multihome.field];
139
+ const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field];
104
140
 
105
141
  if (!hasMultihomeField) {
106
142
  if (vars.multihome.allowMissing) {
@@ -111,10 +147,31 @@ export const MultiHomePlugin = {
111
147
  }
112
148
  }
113
149
 
114
- // Add WHERE condition wrapped in function for proper grouping
115
- query.where(function() {
116
- this.where(`${tableName}.${vars.multihome.field}`, context.auth.multihome_id);
117
- });
150
+ const storageAdapter = resolveStorageAdapter(scopeName, context);
151
+ if (storageAdapter && !context.storageAdapter) {
152
+ context.storageAdapter = storageAdapter;
153
+ }
154
+
155
+ const columnRef = translateColumnReference({
156
+ field: vars.multihome.field,
157
+ tableName,
158
+ scopeName,
159
+ hookContext: context,
160
+ });
161
+
162
+ const tenantValue = translateFilterValue({
163
+ field: vars.multihome.field,
164
+ value: context.auth.multihome_id,
165
+ scopeName,
166
+ hookContext: context,
167
+ });
168
+
169
+
170
+ if (tenantValue === null) {
171
+ query.whereNull(columnRef);
172
+ } else {
173
+ query.where(columnRef, tenantValue);
174
+ }
118
175
 
119
176
  log.trace('Added multihome filter', {
120
177
  scopeName,
@@ -141,7 +198,7 @@ export const MultiHomePlugin = {
141
198
 
142
199
  // Check if this resource has multihome field
143
200
  const scope = scopes[scopeName];
144
- const hasMultihomeField = scope?.vars?.schemaInfo?.schema?.structure?.[vars.multihome.field];
201
+ const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field];
145
202
 
146
203
  if (!hasMultihomeField) {
147
204
  if (!vars.multihome.allowMissing) {
@@ -177,42 +234,47 @@ export const MultiHomePlugin = {
177
234
  // Add checkPermissions hook to enforce tenant isolation
178
235
  // This is the single source of truth for single-record access control
179
236
  addHook('checkPermissions', 'multihome-check-permissions', {}, async ({ context, scopeName }) => {
237
+ // Extract the needed values from originalContext
238
+ const auth = context.originalContext?.auth;
239
+ const minimalRecord = context.originalContext?.minimalRecord;
240
+ const id = context.originalContext?.id;
241
+
180
242
  // Skip excluded resources
181
243
  if (vars.multihome.excludeResources.includes(scopeName)) {
182
244
  return;
183
245
  }
184
-
246
+
185
247
  // Skip if no multihome context
186
- if (!context.auth?.multihome_id) {
248
+ if (!auth?.multihome_id) {
187
249
  if (vars.multihome.requireAuth) {
188
250
  throw new Error('No multihome context available');
189
251
  }
190
252
  return;
191
253
  }
192
-
254
+
193
255
  // Get the scope to check if it has multihome field
194
256
  const scope = scopes[scopeName];
195
- const hasMultihomeField = scope?.vars?.schemaInfo?.schema?.structure?.[vars.multihome.field];
196
-
257
+ const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field];
258
+
197
259
  if (!hasMultihomeField) {
198
260
  return; // Resource doesn't support multihome
199
261
  }
200
-
262
+
201
263
  // For operations on existing records, verify tenant ownership
202
- if (context.minimalRecord) {
264
+ if (minimalRecord) {
203
265
  // minimalRecord is in JSON:API format, so tenant_id is in attributes
204
- const recordTenant = context.minimalRecord.attributes?.[vars.multihome.field];
205
- const userTenant = context.auth.multihome_id;
206
-
266
+ const recordTenant = minimalRecord.attributes?.[vars.multihome.field];
267
+ const userTenant = auth.multihome_id;
268
+
207
269
  if (recordTenant !== userTenant) {
208
270
  log.error('Multihome permission violation', {
209
271
  scopeName,
210
- recordId: context.id,
272
+ recordId: id,
211
273
  recordTenant: recordTenant,
212
274
  userTenant: userTenant,
213
275
  method: context.method
214
276
  });
215
-
277
+
216
278
  // Return 404 for GET to prevent information leakage
217
279
  // Return 403 for other operations
218
280
  if (context.method === 'get') {
@@ -12,7 +12,6 @@ export default async function turnScopeInitIntoVars({ context, scopes, vars: api
12
12
  if (typeof scopeOptions.queryDefaultLimit !== 'undefined') vars.queryDefaultLimit = scopeOptions.queryDefaultLimit;
13
13
  if (typeof scopeOptions.queryMaxLimit !== 'undefined') vars.queryMaxLimit = scopeOptions.queryMaxLimit;
14
14
  if (typeof scopeOptions.includeDepthLimit !== 'undefined') vars.includeDepthLimit = scopeOptions.includeDepthLimit;
15
- if (typeof scopeOptions.publicBaseUrl !== 'undefined') vars.publicBaseUrl = scopeOptions.publicBaseUrl;
16
15
  if (typeof scopeOptions.enablePaginationCounts !== 'undefined') vars.enablePaginationCounts = scopeOptions.enablePaginationCounts;
17
16
 
18
17
  // Set simplified settings as scope vars
@@ -7,11 +7,7 @@ export default async function checkPermissionsMethod({ context, params, runHooks
7
7
 
8
8
  Object.assign(context, {
9
9
  method: params.method,
10
- isUpdate: params.isUpdate,
11
- id: params.id,
12
- auth: params.auth,
13
- transaction: params.transaction,
14
- minimalRecord: params.minimalRecord
10
+ originalContext: params.originalContext,
15
11
  })
16
12
 
17
13
  await runHooks('checkPermissions');
@@ -130,8 +130,8 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
130
130
  context.db = context.transaction || api.knex.instance;
131
131
 
132
132
  // These are just shortcuts used in this function and will be returned
133
- const schema = context.schemaInfo.schema;
134
- const schemaStructure = context.schemaInfo.schema.structure;
133
+ const schema = context.schemaInfo.schemaInstance;
134
+ const schemaStructure = context.schemaInfo.schemaInstance.structure;
135
135
  const schemaRelationships = context.schemaInfo.schemaRelationships;
136
136
 
137
137
  // Transform input if in simplified mode
@@ -297,8 +297,9 @@ export const validateResourceAttributesBeforeWrite = async ({
297
297
  ...belongsToUpdates
298
298
  };
299
299
 
300
- // Extract computed fields before validation (they should be rejected)
300
+ // Extract computed fields and foreign key fields before validation
301
301
  const computedFields = {};
302
+ const foreignKeyFields = {};
302
303
  const schemaStructure = context.schemaInfo.schemaStructure || {};
303
304
  Object.entries(attributesToValidate).forEach(([key, value]) => {
304
305
  const fieldDef = schemaStructure[key];
@@ -306,6 +307,12 @@ export const validateResourceAttributesBeforeWrite = async ({
306
307
  if (fieldDef && fieldDef.computed === true && value !== undefined) {
307
308
  computedFields[key] = value;
308
309
  }
310
+ // Foreign key fields (belongsTo with 'as' property) should NOT be in attributes
311
+ // They should only come through relationships or belongsToUpdates
312
+ // Check if key exists in belongsToUpdates (not just truthy value)
313
+ if (fieldDef && fieldDef.belongsTo && fieldDef.as && value !== undefined && !(key in belongsToUpdates)) {
314
+ foreignKeyFields[key] = value;
315
+ }
309
316
  });
310
317
 
311
318
  // Warn if computed fields were sent in the input
@@ -314,12 +321,40 @@ export const validateResourceAttributesBeforeWrite = async ({
314
321
  console.warn(`Computed fields [${fieldNames}] were sent in input for resource '${context.scopeName}' but will be ignored as they are output-only`);
315
322
  }
316
323
 
317
- // Filter out ONLY computed fields from validation
324
+ // Reject if foreign key fields were sent directly in attributes
325
+ if (Object.keys(foreignKeyFields).length > 0) {
326
+ const violations = Object.keys(foreignKeyFields).map(field => {
327
+ const fieldDef = schemaStructure[field];
328
+ return {
329
+ field: `data.attributes.${field}`,
330
+ rule: 'foreign_key_in_attributes',
331
+ message: `Foreign key field '${field}' should not be in attributes. Use 'data.relationships.${fieldDef.as}' instead.`
332
+ };
333
+ });
334
+
335
+ throw new RestApiValidationError(
336
+ 'Foreign key fields cannot be set directly in attributes',
337
+ {
338
+ fields: violations.map(v => v.field),
339
+ violations
340
+ }
341
+ );
342
+ }
343
+
344
+ // Filter out computed fields and foreign key fields from validation
318
345
  // Virtual fields MUST go through validation
319
346
  const attributesForValidation = Object.entries(attributesToValidate)
320
347
  .filter(([key, _]) => {
321
348
  const fieldDef = schemaStructure[key];
322
- return !fieldDef || fieldDef.computed !== true;
349
+ // Exclude computed fields
350
+ if (fieldDef && fieldDef.computed === true) {
351
+ return false;
352
+ }
353
+ // Exclude foreign key fields that weren't provided via belongsToUpdates
354
+ if (fieldDef && fieldDef.belongsTo && fieldDef.as && !belongsToUpdates[key]) {
355
+ return false;
356
+ }
357
+ return true;
323
358
  })
324
359
  .reduce((acc, [key, value]) => {
325
360
  acc[key] = value;
@@ -332,7 +367,7 @@ export const validateResourceAttributesBeforeWrite = async ({
332
367
 
333
368
  if (Object.keys(errors).length > 0) {
334
369
  // --- START OF MODIFICATION ---
335
- const schemaStructure = context.schemaInfo.schema.structure; // Get the schema structure for lookup
370
+ const schemaStructure = context.schemaInfo.schemaInstance.structure; // Get the schema structure for lookup
336
371
 
337
372
  const violations = Object.entries(errors).map(([field, error]) => {
338
373
  let fieldPath = `data.attributes.${field}`; // Default path for attributes
@@ -426,10 +461,11 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
426
461
  // Check permissions using the related scope's checkPermissions
427
462
  await relatedScope.checkPermissions({
428
463
  method: 'get',
429
- auth: context.auth,
430
- id: item.id,
431
- minimalRecord: record,
432
- transaction: context.transaction
464
+ originalContext: {
465
+ ...context,
466
+ id: item.id,
467
+ minimalRecord: record
468
+ }
433
469
  });
434
470
  }
435
471
  }
@@ -1,6 +1,6 @@
1
1
 
2
- import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
3
- import { findRelationshipDefinition } from "./common.js";
2
+ import { RestApiResourceError, RestApiValidationError, RestApiPayloadError } from "../../../lib/rest-api-errors.js";
3
+ import { findRelationshipDefinition, handleWriteMethodError } from "./common.js";
4
4
 
5
5
  /**
6
6
  * DELETE RELATIONSHIP
@@ -12,7 +12,7 @@ import { findRelationshipDefinition } from "./common.js";
12
12
  * @param {array} relationshipData - Array of resource identifiers to remove
13
13
  * @returns {Promise<void>} 204 No Content
14
14
  */
15
- export default async function deleteRelationshipMethod({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
15
+ export default async function deleteRelationshipMethod({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api, log }) {
16
16
  context.method = 'deleteRelationship';
17
17
  context.id = params.id;
18
18
  context.relationshipName = params.relationshipName;
@@ -34,7 +34,7 @@ export default async function deleteRelationshipMethod({ params, context, vars,
34
34
  );
35
35
  }
36
36
 
37
- if (!relDef.hasMany && !relDef.manyToMany) {
37
+ if (relDef.type !== 'hasMany' && relDef.type !== 'manyToMany') {
38
38
  throw new RestApiValidationError(
39
39
  `Cannot DELETE from to-one relationship '${context.relationshipName}'`,
40
40
  { fields: ['data'] }
@@ -60,30 +60,34 @@ export default async function deleteRelationshipMethod({ params, context, vars,
60
60
  }
61
61
 
62
62
  // Remove relationships
63
- if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
64
- const manyToManyDef = relDef.manyToMany || {
65
- through: relDef.through,
66
- foreignKey: relDef.foreignKey,
67
- otherKey: relDef.otherKey
68
- };
69
-
70
- const knex = api.knex?.instance || helpers.db;
71
- const pivotResource = manyToManyDef.through;
72
- const pivotScope = api.resources[pivotResource];
73
- const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || pivotResource;
74
- const localKey = manyToManyDef.foreignKey;
75
- const foreignKey = manyToManyDef.otherKey;
76
-
77
- for (const identifier of params.relationshipData) {
78
- await knex(pivotTable)
79
- .where(localKey, context.id)
80
- .where(foreignKey, identifier.id)
81
- .delete()
82
- .transacting(context.transaction);
63
+ if (relDef?.through) {
64
+ if (api.anyapi?.links?.removeMany) {
65
+ await api.anyapi.links.removeMany({
66
+ context,
67
+ scopeName,
68
+ relName: context.relationshipName,
69
+ relDef,
70
+ relData: params.relationshipData,
71
+ });
72
+ } else {
73
+ const knex = api.knex?.instance || helpers.db;
74
+ const pivotResource = relDef.through;
75
+ const pivotScope = api.resources[pivotResource];
76
+ const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || pivotResource;
77
+ const localKey = relDef.foreignKey;
78
+ const foreignKey = relDef.otherKey;
79
+
80
+ for (const identifier of params.relationshipData) {
81
+ await knex(pivotTable)
82
+ .where(localKey, context.id)
83
+ .where(foreignKey, identifier.id)
84
+ .delete()
85
+ .transacting(context.transaction);
86
+ }
83
87
  }
84
88
  } else {
85
89
  // Null out foreign keys for hasMany
86
- const targetType = relDef.hasMany;
90
+ const targetType = relDef.target;
87
91
  for (const identifier of params.relationshipData) {
88
92
  await api.resources[targetType].patch({
89
93
  id: identifier.id,
@@ -110,6 +114,6 @@ export default async function deleteRelationshipMethod({ params, context, vars,
110
114
  return; // 204 No Content
111
115
 
112
116
  } catch (error) {
113
- await handleWriteMethodError(error, context, 'DELETE_RELATIONSHIP', scopeName, log);
117
+ await handleWriteMethodError(error, context, 'DELETE_RELATIONSHIP', scopeName, log, runHooks);
114
118
  }
115
- }
119
+ }
@@ -67,10 +67,7 @@ export default async function deleteMethod({
67
67
  // Centralised checkPermissions function
68
68
  await scope.checkPermissions({
69
69
  method: 'delete',
70
- auth: context.auth,
71
- id: context.id,
72
- minimalRecord: context.minimalRecord,
73
- transaction: context.transaction
70
+ originalContext: context,
74
71
  });
75
72
 
76
73
  // Before data operations
@@ -167,6 +167,18 @@ export default async function enrichAttributesMethod({ context, params, runHooks
167
167
  }
168
168
  }
169
169
 
170
+ if (requestedFields && requestedFields.length > 0) {
171
+ const requestedList = typeof requestedFields === 'string'
172
+ ? requestedFields.split(',').map((field) => field.trim()).filter((field) => field)
173
+ : requestedFields;
174
+ const allowed = new Set(requestedList);
175
+ Object.keys(finalAttributes).forEach((key) => {
176
+ if (!allowed.has(key)) {
177
+ delete finalAttributes[key];
178
+ }
179
+ });
180
+ }
181
+
170
182
  // Create context for enrichAttributes hooks
171
183
  Object.assign(context, {
172
184
  parentContext,
@@ -183,4 +195,4 @@ export default async function enrichAttributesMethod({ context, params, runHooks
183
195
 
184
196
  // Return the attributes from context, which hooks may have modified
185
197
  return context.attributes;
186
- };
198
+ };