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,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
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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 =
|
|
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
|
-
[
|
|
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 =
|
|
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
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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 ===
|
|
251
|
-
|
|
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
|
-
[
|
|
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: [
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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:
|
|
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:
|
|
57
|
-
related:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
65
|
-
const schemaStructure = context.schemaInfo.
|
|
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
|
-
|
|
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
|
};
|