json-rest-api 1.0.10 → 1.0.13
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.
- package/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +7 -11
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- 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'
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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?.
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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?.
|
|
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 (!
|
|
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?.
|
|
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 (
|
|
264
|
+
if (minimalRecord) {
|
|
203
265
|
// minimalRecord is in JSON:API format, so tenant_id is in attributes
|
|
204
|
-
const recordTenant =
|
|
205
|
-
const userTenant =
|
|
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:
|
|
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
|
-
|
|
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.
|
|
134
|
-
const schemaStructure = context.schemaInfo.
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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 (
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
};
|