json-rest-api 1.0.14 → 1.0.16
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 +11 -2
- 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 +1 -1
- 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 +99 -99
- 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 +11 -15
- 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,17 +1,17 @@
|
|
|
1
|
-
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { validatePutPayload } from '../lib/querying-writing/payload-validators.js'
|
|
3
|
-
import { processRelationships } from '../lib/writing/relationship-processor.js'
|
|
4
|
-
import { updateManyToManyRelationship, createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
5
|
-
import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
|
|
6
|
-
import {
|
|
7
|
-
setupCommonRequest,
|
|
1
|
+
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
import { validatePutPayload } from '../lib/querying-writing/payload-validators.js'
|
|
3
|
+
import { processRelationships } from '../lib/writing/relationship-processor.js'
|
|
4
|
+
import { updateManyToManyRelationship, createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
5
|
+
import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
|
|
6
|
+
import {
|
|
7
|
+
setupCommonRequest,
|
|
8
8
|
validateResourceAttributesBeforeWrite,
|
|
9
9
|
validateRelationshipAccess,
|
|
10
10
|
applyFieldSetters,
|
|
11
11
|
validatePivotResource,
|
|
12
12
|
handleRecordReturnAfterWrite,
|
|
13
13
|
handleWriteMethodError
|
|
14
|
-
} from './common.js'
|
|
14
|
+
} from './common.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* PUT
|
|
@@ -20,39 +20,39 @@ import {
|
|
|
20
20
|
* PUT is a complete replacement - any relationships not provided will be removed/nulled.
|
|
21
21
|
* This method does NOT support creating new related resources via an `included` array.
|
|
22
22
|
*/
|
|
23
|
-
export default async function putMethod({
|
|
24
|
-
params,
|
|
25
|
-
context,
|
|
26
|
-
vars,
|
|
27
|
-
helpers,
|
|
28
|
-
scope,
|
|
29
|
-
scopes,
|
|
30
|
-
runHooks,
|
|
31
|
-
apiOptions,
|
|
32
|
-
pluginOptions,
|
|
33
|
-
scopeOptions,
|
|
23
|
+
export default async function putMethod ({
|
|
24
|
+
params,
|
|
25
|
+
context,
|
|
26
|
+
vars,
|
|
27
|
+
helpers,
|
|
28
|
+
scope,
|
|
29
|
+
scopes,
|
|
30
|
+
runHooks,
|
|
31
|
+
apiOptions,
|
|
32
|
+
pluginOptions,
|
|
33
|
+
scopeOptions,
|
|
34
34
|
scopeName,
|
|
35
35
|
api,
|
|
36
36
|
log
|
|
37
37
|
}) {
|
|
38
|
-
context.method = 'put'
|
|
39
|
-
|
|
38
|
+
context.method = 'put'
|
|
39
|
+
|
|
40
40
|
try {
|
|
41
41
|
const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
|
|
42
|
-
params,
|
|
43
|
-
context,
|
|
44
|
-
vars,
|
|
45
|
-
scopes,
|
|
46
|
-
scopeOptions,
|
|
42
|
+
params,
|
|
43
|
+
context,
|
|
44
|
+
vars,
|
|
45
|
+
scopes,
|
|
46
|
+
scopeOptions,
|
|
47
47
|
scopeName,
|
|
48
|
-
api,
|
|
48
|
+
api,
|
|
49
49
|
helpers
|
|
50
|
-
})
|
|
51
|
-
context.id = context.inputRecord.data.id
|
|
52
|
-
|
|
50
|
+
})
|
|
51
|
+
context.id = context.inputRecord.data.id
|
|
52
|
+
|
|
53
53
|
// Run early hooks for pre-processing (e.g., file handling)
|
|
54
|
-
await runHooks('beforeProcessing')
|
|
55
|
-
await runHooks('beforeProcessingPut')
|
|
54
|
+
await runHooks('beforeProcessing')
|
|
55
|
+
await runHooks('beforeProcessingPut')
|
|
56
56
|
|
|
57
57
|
// Validate PUT payload to ensure it's a complete resource replacement operation.
|
|
58
58
|
// PUT requires the full resource representation including ID (unlike POST which generates ID).
|
|
@@ -60,35 +60,35 @@ export default async function putMethod({
|
|
|
60
60
|
// read-only), and ensures the payload represents a complete replacement. Any fields not
|
|
61
61
|
// provided will be removed or reset to defaults - this is the key difference from PATCH.
|
|
62
62
|
// Example: PUT to /articles/123 must have data.id: '123' and all required fields.
|
|
63
|
-
validatePutPayload(context.inputRecord, scopes)
|
|
64
|
-
|
|
63
|
+
validatePutPayload(context.inputRecord, scopes)
|
|
64
|
+
|
|
65
65
|
// Validate that user has read access to all related resources
|
|
66
66
|
// This ensures users can only create relationships to resources they can access
|
|
67
|
-
await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api)
|
|
68
|
-
|
|
67
|
+
await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api)
|
|
68
|
+
|
|
69
69
|
// Extract foreign keys from JSON:API relationships and prepare many-to-many operations
|
|
70
70
|
// Example: relationships.author -> author_id: '123' for storage
|
|
71
71
|
// Example: relationships.tags -> array of pivot records to create later
|
|
72
72
|
const { belongsToUpdates, manyToManyRelationships } = processRelationships(
|
|
73
73
|
scope,
|
|
74
74
|
{ context }
|
|
75
|
-
)
|
|
75
|
+
)
|
|
76
76
|
|
|
77
|
-
await validateResourceAttributesBeforeWrite({
|
|
78
|
-
context,
|
|
79
|
-
schema,
|
|
80
|
-
belongsToUpdates,
|
|
77
|
+
await validateResourceAttributesBeforeWrite({
|
|
78
|
+
context,
|
|
79
|
+
schema,
|
|
80
|
+
belongsToUpdates,
|
|
81
81
|
runHooks
|
|
82
|
-
})
|
|
82
|
+
})
|
|
83
83
|
|
|
84
84
|
// Check existence first
|
|
85
85
|
context.exists = await helpers.dataExists({
|
|
86
86
|
scopeName,
|
|
87
87
|
context
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
context.isCreate = !context.exists
|
|
91
|
-
context.isUpdate = context.exists
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
context.isCreate = !context.exists
|
|
91
|
+
context.isUpdate = context.exists
|
|
92
92
|
|
|
93
93
|
// Fetch minimal record for authorization checks (only for updates)
|
|
94
94
|
if (context.isUpdate) {
|
|
@@ -96,64 +96,64 @@ export default async function putMethod({
|
|
|
96
96
|
scopeName,
|
|
97
97
|
context,
|
|
98
98
|
runHooks
|
|
99
|
-
})
|
|
99
|
+
})
|
|
100
100
|
|
|
101
101
|
if (!minimalRecord) {
|
|
102
102
|
throw new RestApiResourceError(
|
|
103
103
|
`Resource not found: ${scopeName}/${context.id}`,
|
|
104
104
|
ERROR_SUBTYPES.NOT_FOUND
|
|
105
|
-
)
|
|
105
|
+
)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
context.minimalRecord = minimalRecord
|
|
108
|
+
context.minimalRecord = minimalRecord
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// For PUT, we also need to handle relationships that are NOT provided
|
|
112
112
|
// (they should be set to null/empty as PUT is a complete replacement)
|
|
113
|
-
const allRelationships = {}
|
|
114
|
-
|
|
113
|
+
const allRelationships = {}
|
|
114
|
+
|
|
115
115
|
// Collect all defined relationships for this resource
|
|
116
116
|
for (const [relName, relDef] of Object.entries(schemaRelationships || {})) {
|
|
117
117
|
if (relDef.type === 'manyToMany') {
|
|
118
|
-
allRelationships[relName] = {
|
|
119
|
-
type: 'manyToMany',
|
|
118
|
+
allRelationships[relName] = {
|
|
119
|
+
type: 'manyToMany',
|
|
120
120
|
relDef: {
|
|
121
121
|
through: relDef.through,
|
|
122
122
|
foreignKey: relDef.foreignKey,
|
|
123
123
|
otherKey: relDef.otherKey
|
|
124
124
|
}
|
|
125
|
-
}
|
|
125
|
+
}
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
// Also check schema fields for belongsTo relationships
|
|
130
130
|
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
131
131
|
if (fieldDef.as && fieldDef.belongsTo) {
|
|
132
|
-
allRelationships[fieldDef.as] = {
|
|
132
|
+
allRelationships[fieldDef.as] = {
|
|
133
133
|
type: 'belongsTo',
|
|
134
134
|
fieldName,
|
|
135
|
-
fieldDef
|
|
136
|
-
}
|
|
135
|
+
fieldDef
|
|
136
|
+
}
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
// Process missing relationships (PUT should null them out only if relationships object exists)
|
|
141
|
-
const hasRelationshipsObject = context.inputRecord.data.relationships !== undefined
|
|
142
|
-
const providedRelationships = new Set(Object.keys(context.inputRecord.data.relationships || {}))
|
|
143
|
-
|
|
141
|
+
const hasRelationshipsObject = context.inputRecord.data.relationships !== undefined
|
|
142
|
+
const providedRelationships = new Set(Object.keys(context.inputRecord.data.relationships || {}))
|
|
143
|
+
|
|
144
144
|
// Only null out missing relationships if a relationships object was provided
|
|
145
145
|
if (hasRelationshipsObject) {
|
|
146
146
|
for (const [relName, relInfo] of Object.entries(allRelationships)) {
|
|
147
147
|
if (!providedRelationships.has(relName)) {
|
|
148
148
|
if (relInfo.type === 'belongsTo') {
|
|
149
|
-
belongsToUpdates[relInfo.fieldName] = null
|
|
149
|
+
belongsToUpdates[relInfo.fieldName] = null
|
|
150
150
|
} else if (relInfo.type === 'manyToMany') {
|
|
151
151
|
// Add to manyToManyRelationships with empty array
|
|
152
152
|
manyToManyRelationships.push({
|
|
153
153
|
relName,
|
|
154
154
|
relDef: relInfo.relDef,
|
|
155
155
|
relData: [] // Empty array means delete all
|
|
156
|
-
})
|
|
156
|
+
})
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
}
|
|
@@ -164,18 +164,18 @@ export default async function putMethod({
|
|
|
164
164
|
context.inputRecord.data.attributes = {
|
|
165
165
|
...context.inputRecord.data.attributes,
|
|
166
166
|
...belongsToUpdates
|
|
167
|
-
}
|
|
167
|
+
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// Centralised checkPermissions function
|
|
171
171
|
await scope.checkPermissions({
|
|
172
172
|
method: 'put',
|
|
173
173
|
originalContext: context,
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
await runHooks('beforeDataCall')
|
|
177
|
-
await runHooks('beforeDataCallPut')
|
|
178
|
-
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
await runHooks('beforeDataCall')
|
|
177
|
+
await runHooks('beforeDataCallPut')
|
|
178
|
+
|
|
179
179
|
// Apply field setters after validation and before storage
|
|
180
180
|
if (context.inputRecord?.data?.attributes) {
|
|
181
181
|
context.inputRecord.data.attributes = await applyFieldSetters(
|
|
@@ -184,16 +184,16 @@ export default async function putMethod({
|
|
|
184
184
|
context,
|
|
185
185
|
api,
|
|
186
186
|
helpers
|
|
187
|
-
)
|
|
187
|
+
)
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
// Pass the operation type to the helper
|
|
191
191
|
await helpers.dataPut({
|
|
192
192
|
scopeName,
|
|
193
193
|
context
|
|
194
|
-
})
|
|
195
|
-
await runHooks('afterDataCallPut')
|
|
196
|
-
await runHooks('afterDataCall')
|
|
194
|
+
})
|
|
195
|
+
await runHooks('afterDataCallPut')
|
|
196
|
+
await runHooks('afterDataCall')
|
|
197
197
|
|
|
198
198
|
// Process many-to-many relationships after main record update/creation
|
|
199
199
|
for (const { relName, relDef, relData } of manyToManyRelationships) {
|
|
@@ -205,11 +205,11 @@ export default async function putMethod({
|
|
|
205
205
|
relDef,
|
|
206
206
|
relData,
|
|
207
207
|
isUpdate: context.isUpdate,
|
|
208
|
-
})
|
|
209
|
-
continue
|
|
208
|
+
})
|
|
209
|
+
continue
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
await validatePivotResource(scopes, relDef, relName)
|
|
212
|
+
await validatePivotResource(scopes, relDef, relName)
|
|
213
213
|
|
|
214
214
|
if (context.isUpdate) {
|
|
215
215
|
await updateManyToManyRelationship(null, {
|
|
@@ -220,12 +220,12 @@ export default async function putMethod({
|
|
|
220
220
|
relData,
|
|
221
221
|
transaction: context.transaction
|
|
222
222
|
}
|
|
223
|
-
})
|
|
223
|
+
})
|
|
224
224
|
} else if (relData.length > 0) {
|
|
225
|
-
await createPivotRecords(api, context.id, relDef, relData, context.transaction)
|
|
225
|
+
await createPivotRecords(api, context.id, relDef, relData, context.transaction)
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
const ret = await handleRecordReturnAfterWrite({
|
|
230
230
|
context,
|
|
231
231
|
scopeName,
|
|
@@ -238,17 +238,16 @@ export default async function putMethod({
|
|
|
238
238
|
runHooks,
|
|
239
239
|
helpers,
|
|
240
240
|
log
|
|
241
|
-
})
|
|
241
|
+
})
|
|
242
242
|
|
|
243
243
|
// Commit transaction if we created it
|
|
244
244
|
if (context.shouldCommit) {
|
|
245
|
-
await context.transaction.commit()
|
|
246
|
-
await runHooks('afterCommit')
|
|
245
|
+
await context.transaction.commit()
|
|
246
|
+
await runHooks('afterCommit')
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
return ret
|
|
250
|
-
|
|
249
|
+
return ret
|
|
251
250
|
} catch (error) {
|
|
252
|
-
await handleWriteMethodError(error, context, 'PUT', scopeName, log, runHooks)
|
|
251
|
+
await handleWriteMethodError(error, context, 'PUT', scopeName, log, runHooks)
|
|
253
252
|
}
|
|
254
253
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { validateQueryPayload } from '../lib/querying-writing/payload-validators.js'
|
|
3
|
-
import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
|
|
4
|
-
import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
|
|
5
|
-
import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
|
|
6
|
-
import { cascadeConfig } from './common.js'
|
|
1
|
+
import { RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
import { validateQueryPayload } from '../lib/querying-writing/payload-validators.js'
|
|
3
|
+
import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
|
|
4
|
+
import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
|
|
5
|
+
import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
|
|
6
|
+
import { cascadeConfig } from './common.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* QUERY
|
|
@@ -19,57 +19,57 @@ import { cascadeConfig } from './common.js';
|
|
|
19
19
|
* @param {object} [queryParams.page] - An object for pagination. Typically includes `number` (page number) and `size` (items per page). E.g., `{ number: 1, size: 10 }`.
|
|
20
20
|
* @returns {Promise<object>} A Promise that resolves to the JSON:API response document containing the resource collection.
|
|
21
21
|
*/
|
|
22
|
-
export default async function queryMethod({
|
|
23
|
-
params,
|
|
24
|
-
context,
|
|
25
|
-
vars,
|
|
26
|
-
helpers,
|
|
27
|
-
scope,
|
|
28
|
-
scopes,
|
|
29
|
-
runHooks,
|
|
30
|
-
apiOptions,
|
|
31
|
-
pluginOptions,
|
|
32
|
-
scopeOptions,
|
|
33
|
-
scopeName,
|
|
22
|
+
export default async function queryMethod ({
|
|
23
|
+
params,
|
|
24
|
+
context,
|
|
25
|
+
vars,
|
|
26
|
+
helpers,
|
|
27
|
+
scope,
|
|
28
|
+
scopes,
|
|
29
|
+
runHooks,
|
|
30
|
+
apiOptions,
|
|
31
|
+
pluginOptions,
|
|
32
|
+
scopeOptions,
|
|
33
|
+
scopeName,
|
|
34
34
|
log,
|
|
35
|
-
api
|
|
35
|
+
api
|
|
36
36
|
}) {
|
|
37
|
-
context.method = 'query'
|
|
38
|
-
|
|
37
|
+
context.method = 'query'
|
|
38
|
+
|
|
39
39
|
// Determine which simplified setting to use based on transport
|
|
40
|
-
const isTransport = params.isTransport === true
|
|
41
|
-
|
|
40
|
+
const isTransport = params.isTransport === true
|
|
41
|
+
|
|
42
42
|
// Use vars which automatically cascade from scope to global
|
|
43
|
-
const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
|
|
44
|
-
|
|
43
|
+
const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
|
|
44
|
+
|
|
45
45
|
// Get simplified setting - from params only (per-call override) or use default
|
|
46
|
-
context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
|
|
46
|
+
context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
|
|
47
47
|
|
|
48
48
|
// Assign common context properties
|
|
49
|
-
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
50
|
-
context.queryParams = params.queryParams || {}
|
|
49
|
+
context.schemaInfo = scopes[scopeName].vars.schemaInfo // This is the object variable created by compileSchemas
|
|
50
|
+
context.queryParams = params.queryParams || {}
|
|
51
51
|
|
|
52
52
|
// These only make sense as parameter per query
|
|
53
|
-
context.queryParams.fields = cascadeConfig('fields', [params.queryParams], {})
|
|
54
|
-
context.queryParams.include = cascadeConfig('include', [params.queryParams], [])
|
|
55
|
-
context.queryParams.sort = cascadeConfig('sort', [params.queryParams], [])
|
|
56
|
-
context.queryParams.page = cascadeConfig('page', [params.queryParams], {})
|
|
53
|
+
context.queryParams.fields = cascadeConfig('fields', [params.queryParams], {})
|
|
54
|
+
context.queryParams.include = cascadeConfig('include', [params.queryParams], [])
|
|
55
|
+
context.queryParams.sort = cascadeConfig('sort', [params.queryParams], [])
|
|
56
|
+
context.queryParams.page = cascadeConfig('page', [params.queryParams], {})
|
|
57
57
|
|
|
58
|
-
context.transaction = params.transaction
|
|
59
|
-
context.db = context.transaction || api.knex.instance
|
|
60
|
-
|
|
61
|
-
context.scopeName = scopeName
|
|
58
|
+
context.transaction = params.transaction
|
|
59
|
+
context.db = context.transaction || api.knex.instance
|
|
60
|
+
|
|
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.searchSchemaInstance
|
|
65
|
-
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
66
|
-
const schemaRelationships = context.schemaInfo.schemaRelationships
|
|
64
|
+
const searchSchema = context.schemaInfo.searchSchemaInstance
|
|
65
|
+
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
66
|
+
const schemaRelationships = context.schemaInfo.schemaRelationships
|
|
67
67
|
|
|
68
68
|
// Sortable fields and sort (mab)
|
|
69
|
-
context.sortableFields = vars.sortableFields
|
|
69
|
+
context.sortableFields = vars.sortableFields
|
|
70
70
|
// Apply default sort if no sort specified
|
|
71
71
|
if (context.queryParams.sort.length === 0 && vars.defaultSort) {
|
|
72
|
-
context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort]
|
|
72
|
+
context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort]
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Validate query parameters to ensure they follow JSON:API specification and security rules.
|
|
@@ -77,7 +77,7 @@ export default async function queryMethod({
|
|
|
77
77
|
// (preventing SQL injection), pagination uses valid page[size]/page[number] format, and include
|
|
78
78
|
// paths reference real relationships. Example: sort: ['-createdAt', 'title'] is checked against
|
|
79
79
|
// sortableFields to ensure users can't sort by sensitive fields like 'password_hash'.
|
|
80
|
-
validateQueryPayload({ queryParams: context.queryParams }, context.sortableFields, vars.includeDepthLimit)
|
|
80
|
+
validateQueryPayload({ queryParams: context.queryParams }, context.sortableFields, vars.includeDepthLimit)
|
|
81
81
|
|
|
82
82
|
// Validate search/filter parameters against searchSchema
|
|
83
83
|
if (context.queryParams.filters && Object.keys(context.queryParams.filters).length > 0) {
|
|
@@ -85,7 +85,7 @@ export default async function queryMethod({
|
|
|
85
85
|
if (!searchSchema) {
|
|
86
86
|
throw new RestApiValidationError(
|
|
87
87
|
`Filtering is not enabled for resource '${scopeName}'. To enable filtering, add 'search: true' to schema fields or define a searchSchema.`,
|
|
88
|
-
{
|
|
88
|
+
{
|
|
89
89
|
fields: Object.keys(context.queryParams.filters).map(field => `filters.${field}`),
|
|
90
90
|
violations: [{
|
|
91
91
|
field: 'filters',
|
|
@@ -93,13 +93,13 @@ export default async function queryMethod({
|
|
|
93
93
|
message: 'Resource does not have searchable fields defined'
|
|
94
94
|
}]
|
|
95
95
|
}
|
|
96
|
-
)
|
|
96
|
+
)
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Validate the filter parameters searchSchema
|
|
100
|
-
const { validatedObject, errors } = await searchSchema.validate(context.queryParams.filters, {
|
|
100
|
+
const { validatedObject, errors } = await searchSchema.validate(context.queryParams.filters, {
|
|
101
101
|
onlyObjectValues: true // Partial validation for filters
|
|
102
|
-
})
|
|
102
|
+
})
|
|
103
103
|
|
|
104
104
|
// If there are validation errors, throw an error
|
|
105
105
|
if (Object.keys(errors).length > 0) {
|
|
@@ -107,101 +107,101 @@ export default async function queryMethod({
|
|
|
107
107
|
field: `filters.${field}`,
|
|
108
108
|
rule: error.code || 'invalid_value',
|
|
109
109
|
message: error.message
|
|
110
|
-
}))
|
|
111
|
-
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
112
|
throw new RestApiValidationError(
|
|
113
113
|
'Invalid filter parameters',
|
|
114
|
-
{
|
|
114
|
+
{
|
|
115
115
|
fields: Object.keys(errors).map(field => `filters.${field}`),
|
|
116
|
-
violations
|
|
116
|
+
violations
|
|
117
117
|
}
|
|
118
|
-
)
|
|
118
|
+
)
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
// Replace filter with validated/transformed values
|
|
122
|
-
context.queryParams.filters = validatedObject
|
|
122
|
+
context.queryParams.filters = validatedObject
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Centralised checkPermissions function
|
|
126
126
|
await scope.checkPermissions({
|
|
127
127
|
method: 'query',
|
|
128
128
|
originalContext: context,
|
|
129
|
-
})
|
|
129
|
+
})
|
|
130
130
|
|
|
131
|
-
await runHooks('beforeData')
|
|
132
|
-
await runHooks('beforeDataQuery')
|
|
131
|
+
await runHooks('beforeData')
|
|
132
|
+
await runHooks('beforeDataQuery')
|
|
133
133
|
context.record = await helpers.dataQuery({
|
|
134
134
|
scopeName,
|
|
135
135
|
context,
|
|
136
136
|
runHooks
|
|
137
|
-
})
|
|
137
|
+
})
|
|
138
138
|
|
|
139
139
|
// Normalize database values (e.g., convert 1/0 to true/false for booleans)
|
|
140
|
-
context.record = normalizeRecordAttributes(context.record, scopes)
|
|
140
|
+
context.record = normalizeRecordAttributes(context.record, scopes)
|
|
141
141
|
|
|
142
142
|
// Make a backup
|
|
143
143
|
try {
|
|
144
|
-
context.originalRecord = structuredClone(context.record)
|
|
144
|
+
context.originalRecord = structuredClone(context.record)
|
|
145
145
|
} catch (e) {
|
|
146
146
|
log.error('Failed to clone record:', {
|
|
147
147
|
error: e.message,
|
|
148
148
|
recordKeys: Object.keys(context.record || {}),
|
|
149
149
|
hasHttpRequest: !!context.raw?.req
|
|
150
|
-
})
|
|
151
|
-
throw e
|
|
150
|
+
})
|
|
151
|
+
throw e
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// This will enhance record, which is the WHOLE JSON:API record
|
|
155
|
-
await runHooks('enrichRecord')
|
|
155
|
+
await runHooks('enrichRecord')
|
|
156
156
|
|
|
157
157
|
// Get computed field information for main resource
|
|
158
|
-
const computedFields = scope.vars.schemaInfo?.computed || {}
|
|
159
|
-
const requestedFields = context.queryParams.fields?.[scopeName]
|
|
160
|
-
const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
|
|
158
|
+
const computedFields = scope.vars.schemaInfo?.computed || {}
|
|
159
|
+
const requestedFields = context.queryParams.fields?.[scopeName]
|
|
160
|
+
const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
|
|
161
161
|
|
|
162
162
|
// Run enrichAttributes for every single set of attribute, calling it from the right scope
|
|
163
163
|
for (const entry of context.record.data) {
|
|
164
|
-
entry.attributes = await scope.enrichAttributes({
|
|
165
|
-
attributes: entry.attributes,
|
|
164
|
+
entry.attributes = await scope.enrichAttributes({
|
|
165
|
+
attributes: entry.attributes,
|
|
166
166
|
parentContext: context,
|
|
167
|
-
requestedComputedFields
|
|
167
|
+
requestedComputedFields,
|
|
168
168
|
isMainResource: true,
|
|
169
169
|
computedDependencies: context.computedDependencies
|
|
170
|
-
})
|
|
170
|
+
})
|
|
171
171
|
}
|
|
172
172
|
for (const entry of (context.record.included || [])) {
|
|
173
|
-
const entryScope = scopes[entry.type]
|
|
174
|
-
const entryComputed = entryScope.vars.schemaInfo?.computed || {}
|
|
175
|
-
const entryRequestedFields = context.queryParams.fields?.[entry.type]
|
|
173
|
+
const entryScope = scopes[entry.type]
|
|
174
|
+
const entryComputed = entryScope.vars.schemaInfo?.computed || {}
|
|
175
|
+
const entryRequestedFields = context.queryParams.fields?.[entry.type]
|
|
176
176
|
const entryRequestedComputed = getRequestedComputedFields(
|
|
177
|
-
entry.type,
|
|
178
|
-
entryRequestedFields,
|
|
177
|
+
entry.type,
|
|
178
|
+
entryRequestedFields,
|
|
179
179
|
entryComputed
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
entry.attributes = await entryScope.enrichAttributes({
|
|
183
|
-
attributes: entry.attributes,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
entry.attributes = await entryScope.enrichAttributes({
|
|
183
|
+
attributes: entry.attributes,
|
|
184
184
|
parentContext: context,
|
|
185
185
|
requestedComputedFields: entryRequestedComputed,
|
|
186
186
|
isMainResource: false,
|
|
187
187
|
computedDependencies: entry.__$jsonrestapi_computed_deps$__
|
|
188
|
-
})
|
|
188
|
+
})
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// The called hooks should NOT change context.record
|
|
192
|
-
await runHooks('finish')
|
|
193
|
-
await runHooks('finishQuery')
|
|
194
|
-
|
|
192
|
+
await runHooks('finish')
|
|
193
|
+
await runHooks('finishQuery')
|
|
194
|
+
|
|
195
195
|
// Transform output if in simplified mode
|
|
196
196
|
if (context.simplified) {
|
|
197
197
|
// Convert JSON:API response back to simplified format
|
|
198
|
-
// Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
|
|
198
|
+
// Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
|
|
199
199
|
// becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
|
|
200
200
|
return transformJsonApiToSimplified(
|
|
201
201
|
{ record: context.record },
|
|
202
202
|
{ context: { schemaStructure, schemaRelationships, scopes } }
|
|
203
|
-
)
|
|
203
|
+
)
|
|
204
204
|
}
|
|
205
|
-
|
|
206
|
-
return context.record
|
|
207
|
-
}
|
|
205
|
+
|
|
206
|
+
return context.record
|
|
207
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { ValidationError } from 'hooked-api'
|
|
1
|
+
import { ValidationError } from 'hooked-api'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* release
|
|
5
5
|
* Runs the hook that tells plugins to release resources
|
|
6
6
|
*/
|
|
7
7
|
export default async ({ runHooks }) => {
|
|
8
|
-
|
|
9
8
|
// Run the addRoute hook to notify transport plugins
|
|
10
|
-
await runHooks('release')
|
|
11
|
-
|
|
12
|
-
return { }
|
|
13
|
-
}
|
|
9
|
+
await runHooks('release')
|
|
10
|
+
|
|
11
|
+
return { }
|
|
12
|
+
}
|