json-rest-api 1.0.13 → 1.0.15
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +13 -0
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +9 -5
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import { RestApiResourceError } from
|
|
2
|
-
import { findRelationshipDefinition } from
|
|
3
|
-
import { buildRelationshipUrl } from
|
|
1
|
+
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
import { findRelationshipDefinition } from './common.js'
|
|
3
|
+
import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* GET RELATED
|
|
7
7
|
* Retrieves the actual related resources (full data)
|
|
8
8
|
* GET /api/articles/1/comments
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* @param {string} id - The ID of the parent resource
|
|
11
11
|
* @param {string} relationshipName - The name of the relationship
|
|
12
12
|
* @param {object} queryParams - Standard query parameters
|
|
13
13
|
* @returns {Promise<object>} Related resources with full data
|
|
14
14
|
*/
|
|
15
|
-
export default async function getRelatedMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api })
|
|
16
|
-
context.method = 'getRelated'
|
|
17
|
-
context.id = params.id
|
|
18
|
-
context.relationshipName = params.relationshipName
|
|
19
|
-
context.queryParams = params.queryParams || {}
|
|
20
|
-
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
21
|
-
context.transaction = params.transaction
|
|
15
|
+
export default async function getRelatedMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
|
|
16
|
+
context.method = 'getRelated'
|
|
17
|
+
context.id = params.id
|
|
18
|
+
context.relationshipName = params.relationshipName
|
|
19
|
+
context.queryParams = params.queryParams || {}
|
|
20
|
+
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
21
|
+
context.transaction = params.transaction
|
|
22
22
|
context.db = context.transaction || api.knex.instance
|
|
23
23
|
|
|
24
24
|
// Validate the relationship exists
|
|
25
|
-
const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
|
|
25
|
+
const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
|
|
26
26
|
|
|
27
27
|
if (!relDef) {
|
|
28
28
|
throw new RestApiResourceError(
|
|
29
29
|
`Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
|
|
30
30
|
{ subtype: 'relationship_not_found' }
|
|
31
|
-
)
|
|
31
|
+
)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Determine target type based on relationship type
|
|
35
|
-
let targetType
|
|
35
|
+
let targetType
|
|
36
36
|
if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
37
|
-
targetType = relDef.target
|
|
37
|
+
targetType = relDef.target
|
|
38
38
|
} else if (relDef.type === 'manyToMany') {
|
|
39
|
-
targetType = context.relationshipName
|
|
39
|
+
targetType = context.relationshipName // For manyToMany, use the relationship name
|
|
40
40
|
} else if (relDef.belongsTo) {
|
|
41
|
-
targetType = relDef.belongsTo
|
|
41
|
+
targetType = relDef.belongsTo // belongsTo still in schema
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (!targetType || !scopes[targetType]) {
|
|
45
45
|
throw new RestApiResourceError(
|
|
46
46
|
`Related resource type '${targetType}' not found`,
|
|
47
47
|
{ subtype: 'related_type_not_found' }
|
|
48
|
-
)
|
|
48
|
+
)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Check permissions
|
|
52
|
-
await runHooks('checkPermissions')
|
|
53
|
-
await runHooks('checkPermissionsGetRelated')
|
|
52
|
+
await runHooks('checkPermissions')
|
|
53
|
+
await runHooks('checkPermissionsGetRelated')
|
|
54
54
|
|
|
55
55
|
// Verify parent exists
|
|
56
56
|
const exists = await helpers.dataExists({
|
|
57
57
|
scopeName,
|
|
58
58
|
context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
|
|
59
|
-
})
|
|
59
|
+
})
|
|
60
60
|
|
|
61
61
|
if (!exists) {
|
|
62
|
-
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
62
|
+
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Handle to-one relationships (belongsTo and hasOne)
|
|
66
66
|
// For example: GET /api/books/1/country or GET /api/books/1/publisher
|
|
67
67
|
if (relDef.belongsTo || relDef.type === 'hasOne') {
|
|
68
68
|
// OPTIMIZATION: Detect if we actually need to make two API calls
|
|
69
|
-
//
|
|
69
|
+
//
|
|
70
70
|
// The naive approach always makes 2 calls:
|
|
71
71
|
// 1. Get parent with relationship included (fetches FULL related record)
|
|
72
72
|
// 2. Extract just the ID and fetch the same record again with queryParams
|
|
@@ -81,30 +81,30 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
81
81
|
// Note: Filters and sorting don't make sense for a single to-one relationship
|
|
82
82
|
// so we don't check for them
|
|
83
83
|
false
|
|
84
|
-
)
|
|
84
|
+
)
|
|
85
85
|
|
|
86
86
|
if (hasRelevantQueryParams) {
|
|
87
87
|
// CASE 1: Has queryParams that affect the related resource
|
|
88
88
|
// We need to make two calls to properly apply the queryParams
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
// First call: Get parent with minimal data (just need the related ID)
|
|
91
91
|
const parent = await scope.get({
|
|
92
92
|
id: context.id,
|
|
93
|
-
queryParams: {
|
|
93
|
+
queryParams: {
|
|
94
94
|
include: [context.relationshipName],
|
|
95
95
|
fields: { [scopeName]: vars.idProperty || 'id' } // Only fetch parent ID to minimize data
|
|
96
96
|
},
|
|
97
97
|
transaction: context.transaction,
|
|
98
98
|
simplified: false,
|
|
99
99
|
isTransport: params.isTransport
|
|
100
|
-
})
|
|
100
|
+
})
|
|
101
101
|
|
|
102
|
-
const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
|
|
102
|
+
const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
|
|
103
103
|
if (!relatedId) {
|
|
104
104
|
return {
|
|
105
105
|
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
106
106
|
data: null
|
|
107
|
-
}
|
|
107
|
+
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Second call: Get the related resource with all queryParams applied
|
|
@@ -114,48 +114,48 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
114
114
|
transaction: context.transaction,
|
|
115
115
|
simplified: false,
|
|
116
116
|
isTransport: params.isTransport
|
|
117
|
-
})
|
|
117
|
+
})
|
|
118
118
|
|
|
119
119
|
return {
|
|
120
120
|
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
121
121
|
data: related.data,
|
|
122
122
|
included: related.included
|
|
123
|
-
}
|
|
123
|
+
}
|
|
124
124
|
} else {
|
|
125
125
|
// CASE 2: No queryParams that affect the related resource
|
|
126
126
|
// We can get everything in one call and extract from included
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
// Single call: Get parent with full related resource included
|
|
129
129
|
const parent = await scope.get({
|
|
130
130
|
id: context.id,
|
|
131
|
-
queryParams: {
|
|
131
|
+
queryParams: {
|
|
132
132
|
include: [context.relationshipName],
|
|
133
133
|
fields: context.queryParams.fields // Respect any field selections for the parent
|
|
134
134
|
},
|
|
135
135
|
transaction: context.transaction,
|
|
136
136
|
simplified: false,
|
|
137
137
|
isTransport: params.isTransport
|
|
138
|
-
})
|
|
138
|
+
})
|
|
139
139
|
|
|
140
140
|
// Extract the related resource from the parent's relationships
|
|
141
|
-
const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
|
|
141
|
+
const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
|
|
142
142
|
if (!relatedId) {
|
|
143
143
|
return {
|
|
144
144
|
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
145
145
|
data: null
|
|
146
|
-
}
|
|
146
|
+
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// Find the full related resource in the included array
|
|
150
150
|
// The include system already fetched it for us!
|
|
151
151
|
const relatedResource = parent.included?.find(
|
|
152
152
|
r => r.type === targetType && r.id === relatedId
|
|
153
|
-
)
|
|
153
|
+
)
|
|
154
154
|
|
|
155
155
|
return {
|
|
156
156
|
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
157
157
|
data: relatedResource || null
|
|
158
|
-
}
|
|
158
|
+
}
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -165,71 +165,71 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
165
165
|
if (relDef.via) {
|
|
166
166
|
// Polymorphic hasMany relationship
|
|
167
167
|
// Example: publishers hasMany reviews via reviewable
|
|
168
|
-
const targetRelationships = scopes[targetType].vars.schemaInfo.schemaRelationships
|
|
169
|
-
const viaRel = targetRelationships?.[relDef.via]
|
|
170
|
-
|
|
168
|
+
const targetRelationships = scopes[targetType].vars.schemaInfo.schemaRelationships
|
|
169
|
+
const viaRel = targetRelationships?.[relDef.via]
|
|
170
|
+
|
|
171
171
|
if (!viaRel?.belongsToPolymorphic) {
|
|
172
172
|
throw new RestApiResourceError(
|
|
173
173
|
`Via relationship '${relDef.via}' not found or not polymorphic in '${targetType}'`,
|
|
174
174
|
{ subtype: 'invalid_via_relationship' }
|
|
175
|
-
)
|
|
175
|
+
)
|
|
176
176
|
}
|
|
177
|
-
|
|
178
|
-
const { typeField, idField } = viaRel.belongsToPolymorphic
|
|
179
|
-
|
|
177
|
+
|
|
178
|
+
const { typeField, idField } = viaRel.belongsToPolymorphic
|
|
179
|
+
|
|
180
180
|
// Add polymorphic filters
|
|
181
181
|
const filters = {
|
|
182
182
|
...context.queryParams.filters,
|
|
183
183
|
[typeField]: scopeName,
|
|
184
184
|
[idField]: context.id
|
|
185
|
-
}
|
|
186
|
-
|
|
185
|
+
}
|
|
186
|
+
|
|
187
187
|
const result = await api.resources[targetType].query({
|
|
188
188
|
queryParams: { ...context.queryParams, filters },
|
|
189
189
|
transaction: context.transaction,
|
|
190
190
|
simplified: false,
|
|
191
191
|
isTransport: params.isTransport
|
|
192
|
-
})
|
|
193
|
-
|
|
192
|
+
})
|
|
193
|
+
|
|
194
194
|
if (!result.links) {
|
|
195
|
-
result.links = {}
|
|
195
|
+
result.links = {}
|
|
196
196
|
}
|
|
197
|
-
result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
198
|
-
return result
|
|
197
|
+
result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
198
|
+
return result
|
|
199
199
|
} else {
|
|
200
200
|
// Regular hasMany with foreignKey
|
|
201
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
|
-
|
|
202
|
+
const targetSchema = scopes[targetType].vars.schemaInfo.schemaStructure
|
|
203
|
+
let relationshipFilterName = null
|
|
204
|
+
|
|
205
205
|
// Find the field in target schema that has the foreign key and get its relationship name
|
|
206
206
|
for (const [fieldName, fieldDef] of Object.entries(targetSchema)) {
|
|
207
207
|
if (fieldName === relDef.foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
|
|
208
|
-
relationshipFilterName = fieldDef.as
|
|
209
|
-
break
|
|
208
|
+
relationshipFilterName = fieldDef.as
|
|
209
|
+
break
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
// Fall back to foreign key if no relationship name found (shouldn't happen with proper schema)
|
|
214
|
-
const filterKey = relationshipFilterName || relDef.foreignKey
|
|
215
|
-
|
|
214
|
+
const filterKey = relationshipFilterName || relDef.foreignKey
|
|
215
|
+
|
|
216
216
|
const filters = {
|
|
217
217
|
...context.queryParams.filters,
|
|
218
218
|
[filterKey]: context.id
|
|
219
|
-
}
|
|
219
|
+
}
|
|
220
220
|
|
|
221
221
|
const result = await api.resources[targetType].query({
|
|
222
222
|
queryParams: { ...context.queryParams, filters },
|
|
223
223
|
transaction: context.transaction,
|
|
224
224
|
simplified: false,
|
|
225
225
|
isTransport: params.isTransport
|
|
226
|
-
})
|
|
226
|
+
})
|
|
227
227
|
|
|
228
228
|
if (!result.links) {
|
|
229
|
-
result.links = {}
|
|
229
|
+
result.links = {}
|
|
230
230
|
}
|
|
231
|
-
result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
232
|
-
return result
|
|
231
|
+
result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
232
|
+
return result
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
@@ -241,9 +241,9 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
241
241
|
context,
|
|
242
242
|
scopeName,
|
|
243
243
|
relName: context.relationshipName,
|
|
244
|
-
})
|
|
244
|
+
})
|
|
245
245
|
|
|
246
|
-
const results = []
|
|
246
|
+
const results = []
|
|
247
247
|
for (const identifier of identifiers) {
|
|
248
248
|
const related = await api.resources[targetType].get({
|
|
249
249
|
id: identifier.id,
|
|
@@ -251,9 +251,9 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
251
251
|
transaction: context.transaction,
|
|
252
252
|
simplified: false,
|
|
253
253
|
isTransport: params.isTransport,
|
|
254
|
-
})
|
|
254
|
+
})
|
|
255
255
|
if (related?.data) {
|
|
256
|
-
results.push(related.data)
|
|
256
|
+
results.push(related.data)
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -262,35 +262,35 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
262
262
|
self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
263
263
|
},
|
|
264
264
|
data: results,
|
|
265
|
-
}
|
|
265
|
+
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
// Legacy fallback
|
|
269
|
-
const pivotResource = relDef.through
|
|
270
|
-
const foreignKey = relDef.foreignKey
|
|
271
|
-
const otherKey = relDef.otherKey
|
|
269
|
+
const pivotResource = relDef.through
|
|
270
|
+
const foreignKey = relDef.foreignKey
|
|
271
|
+
const otherKey = relDef.otherKey
|
|
272
272
|
if (!foreignKey || !otherKey) {
|
|
273
|
-
throw new Error(
|
|
273
|
+
throw new Error('Missing foreignKey or otherKey in many-to-many relationship')
|
|
274
274
|
}
|
|
275
|
-
const pivotScope = scopes[pivotResource]
|
|
275
|
+
const pivotScope = scopes[pivotResource]
|
|
276
276
|
if (!pivotScope) {
|
|
277
277
|
throw new RestApiResourceError(
|
|
278
278
|
`Pivot table resource '${pivotResource}' not found`,
|
|
279
279
|
{ subtype: 'pivot_table_not_found' }
|
|
280
|
-
)
|
|
280
|
+
)
|
|
281
281
|
}
|
|
282
|
-
const pivotSchema = pivotScope.vars.schemaInfo.schemaStructure
|
|
283
|
-
let parentRelationshipName = null
|
|
282
|
+
const pivotSchema = pivotScope.vars.schemaInfo.schemaStructure
|
|
283
|
+
let parentRelationshipName = null
|
|
284
284
|
for (const [fieldName, fieldDef] of Object.entries(pivotSchema)) {
|
|
285
285
|
if (fieldName === foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
|
|
286
|
-
parentRelationshipName = fieldDef.as
|
|
287
|
-
break
|
|
286
|
+
parentRelationshipName = fieldDef.as
|
|
287
|
+
break
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
const filterKey = parentRelationshipName || foreignKey
|
|
290
|
+
const filterKey = parentRelationshipName || foreignKey
|
|
291
291
|
const pivotFilters = {
|
|
292
292
|
[filterKey]: context.id
|
|
293
|
-
}
|
|
293
|
+
}
|
|
294
294
|
const pivotResult = await api.resources[pivotResource].query({
|
|
295
295
|
queryParams: {
|
|
296
296
|
filters: pivotFilters,
|
|
@@ -302,31 +302,31 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
302
302
|
transaction: context.transaction,
|
|
303
303
|
simplified: false,
|
|
304
304
|
isTransport: params.isTransport
|
|
305
|
-
})
|
|
306
|
-
let includedResources = pivotResult.included?.filter((r) => r.type === targetType) || []
|
|
305
|
+
})
|
|
306
|
+
let includedResources = pivotResult.included?.filter((r) => r.type === targetType) || []
|
|
307
307
|
|
|
308
308
|
if (includedResources.length === 0) {
|
|
309
|
-
const pivotData = pivotResult.data || []
|
|
309
|
+
const pivotData = pivotResult.data || []
|
|
310
310
|
const relatedIds = [...new Set(pivotData
|
|
311
311
|
.map((item) => {
|
|
312
|
-
const attrId = item?.attributes?.[otherKey]
|
|
312
|
+
const attrId = item?.attributes?.[otherKey]
|
|
313
313
|
if (attrId !== null && attrId !== undefined) {
|
|
314
|
-
return String(attrId)
|
|
314
|
+
return String(attrId)
|
|
315
315
|
}
|
|
316
|
-
const relationships = item?.relationships || {}
|
|
316
|
+
const relationships = item?.relationships || {}
|
|
317
317
|
for (const rel of Object.values(relationships)) {
|
|
318
|
-
const data = rel?.data
|
|
318
|
+
const data = rel?.data
|
|
319
319
|
if (data?.type === targetType && data?.id != null) {
|
|
320
|
-
return String(data.id)
|
|
320
|
+
return String(data.id)
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
-
return null
|
|
323
|
+
return null
|
|
324
324
|
})
|
|
325
325
|
.filter((id) => id !== null)
|
|
326
|
-
)]
|
|
326
|
+
)]
|
|
327
327
|
|
|
328
328
|
if (relatedIds.length > 0) {
|
|
329
|
-
includedResources = []
|
|
329
|
+
includedResources = []
|
|
330
330
|
for (const relatedId of relatedIds) {
|
|
331
331
|
const related = await api.resources[targetType].get({
|
|
332
332
|
id: relatedId,
|
|
@@ -334,9 +334,9 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
334
334
|
transaction: context.transaction,
|
|
335
335
|
simplified: false,
|
|
336
336
|
isTransport: params.isTransport,
|
|
337
|
-
})
|
|
337
|
+
})
|
|
338
338
|
if (related?.data) {
|
|
339
|
-
includedResources.push(related.data)
|
|
339
|
+
includedResources.push(related.data)
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
}
|
|
@@ -348,6 +348,6 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
348
348
|
},
|
|
349
349
|
data: includedResources,
|
|
350
350
|
meta: pivotResult.meta
|
|
351
|
-
}
|
|
351
|
+
}
|
|
352
352
|
}
|
|
353
353
|
}
|
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { buildRelationshipUrl } from "../lib/querying/url-helpers.js";
|
|
1
|
+
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
import { findRelationshipDefinition } from './common.js'
|
|
3
|
+
import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* GET RELATIONSHIP
|
|
8
7
|
* Retrieves relationship linkage data (just resource identifiers)
|
|
9
8
|
* GET /api/articles/1/relationships/author
|
|
10
|
-
*
|
|
9
|
+
*
|
|
11
10
|
* @param {string} id - The ID of the resource
|
|
12
11
|
* @param {string} relationshipName - The name of the relationship
|
|
13
12
|
* @returns {Promise<object>} Relationship linkage with links
|
|
14
13
|
*/
|
|
15
14
|
export default async function getRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
|
|
16
|
-
context.method = 'getRelationship'
|
|
17
|
-
context.id = params.id
|
|
18
|
-
context.relationshipName = params.relationshipName
|
|
19
|
-
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
15
|
+
context.method = 'getRelationship'
|
|
16
|
+
context.id = params.id
|
|
17
|
+
context.relationshipName = params.relationshipName
|
|
18
|
+
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
20
19
|
|
|
21
20
|
// Validate the relationship exists
|
|
22
|
-
const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
|
|
21
|
+
const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
|
|
23
22
|
|
|
24
23
|
if (!relDef) {
|
|
25
24
|
throw new RestApiResourceError(
|
|
26
25
|
`Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
|
|
27
26
|
{ subtype: 'relationship_not_found' }
|
|
28
|
-
)
|
|
27
|
+
)
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
// Check permissions
|
|
32
|
-
await runHooks('checkPermissions')
|
|
33
|
-
await runHooks('checkPermissionsGetRelationship')
|
|
31
|
+
await runHooks('checkPermissions')
|
|
32
|
+
await runHooks('checkPermissionsGetRelationship')
|
|
34
33
|
|
|
35
34
|
// Reuse existing get method with minimal fields
|
|
36
35
|
const fullRecord = await scope.get({
|
|
@@ -42,14 +41,14 @@ export default async function getRelationshipMethod ({ params, context, vars, he
|
|
|
42
41
|
transaction: context.transaction,
|
|
43
42
|
simplified: false,
|
|
44
43
|
isTransport: params.isTransport
|
|
45
|
-
})
|
|
44
|
+
})
|
|
46
45
|
|
|
47
46
|
if (!fullRecord || !fullRecord.data) {
|
|
48
|
-
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
47
|
+
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
// Extract just the relationship data
|
|
52
|
-
const relationshipData = fullRecord.data.relationships?.[context.relationshipName]
|
|
51
|
+
const relationshipData = fullRecord.data.relationships?.[context.relationshipName]
|
|
53
52
|
|
|
54
53
|
// Build response with links
|
|
55
54
|
return {
|
|
@@ -58,6 +57,5 @@ export default async function getRelationshipMethod ({ params, context, vars, he
|
|
|
58
57
|
related: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
59
58
|
},
|
|
60
59
|
data: relationshipData?.data || (relDef.type === 'hasMany' || relDef.type === 'manyToMany' ? [] : null)
|
|
61
|
-
}
|
|
60
|
+
}
|
|
62
61
|
};
|
|
63
|
-
|