json-rest-api 1.0.24 → 1.0.26
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/README.md +2 -1
- package/docs/API.md +12 -9
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +605 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +5 -4
- package/plugins/core/connectors/express-plugin.js +6 -1
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +36 -26
- package/plugins/core/lib/anyapi/schema-utils.js +1 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/storage-adapter-utils.js +8 -8
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
- package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
- package/plugins/core/lib/storage/storage-adapter.js +74 -13
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
- package/plugins/core/rest-api-knex-plugin.js +70 -60
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
- package/plugins/core/rest-api-plugin-methods/common.js +85 -63
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
- package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
- package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
- package/plugins/core/rest-api-plugin-methods/post.js +4 -7
- package/plugins/core/rest-api-plugin-methods/put.js +4 -7
- package/plugins/core/rest-api-plugin.js +6 -36
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/storage/local-storage.js +44 -19
- package/plugins/storage/s3-storage.js +21 -55
- package/tests/README.md +1 -1
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/fastify-plugin.test.js +28 -0
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +383 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/local-storage.test.js +96 -0
- package/tests/pagination-cursor-multifield.test.js +121 -0
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +489 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +1 -0
- package/tests/test-development-guide.md +5 -5
|
@@ -43,6 +43,7 @@ export function createConnectorContext ({
|
|
|
43
43
|
reply,
|
|
44
44
|
source,
|
|
45
45
|
mountPath = '',
|
|
46
|
+
publicBaseUrl = '',
|
|
46
47
|
requestData,
|
|
47
48
|
createContext,
|
|
48
49
|
urlPrefixOverride
|
|
@@ -55,8 +56,7 @@ export function createConnectorContext ({
|
|
|
55
56
|
|
|
56
57
|
context.urlPrefix = getUrlPrefix(
|
|
57
58
|
context,
|
|
58
|
-
{ vars: { transport: { mountPath } } }
|
|
59
|
-
request
|
|
59
|
+
{ vars: { transport: { mountPath, publicBaseUrl } } }
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
const transportData = buildTransportData(requestData)
|
|
@@ -113,7 +113,8 @@ export function getConnectorLocationHeader ({
|
|
|
113
113
|
context,
|
|
114
114
|
routeMeta,
|
|
115
115
|
helpers,
|
|
116
|
-
mountPath = ''
|
|
116
|
+
mountPath = '',
|
|
117
|
+
publicBaseUrl = ''
|
|
117
118
|
}) {
|
|
118
119
|
if (String(method || '').toUpperCase() !== 'POST') return null
|
|
119
120
|
if (!context?.id || !routeMeta?.scopeName || !helpers?.getLocation) return null
|
|
@@ -123,7 +124,9 @@ export function getConnectorLocationHeader ({
|
|
|
123
124
|
id: context.id
|
|
124
125
|
})
|
|
125
126
|
|
|
126
|
-
const baseUrl = context
|
|
127
|
+
const baseUrl = getUrlPrefix(context, {
|
|
128
|
+
vars: { transport: { mountPath, publicBaseUrl } }
|
|
129
|
+
})
|
|
127
130
|
return `${baseUrl}${location}`
|
|
128
131
|
}
|
|
129
132
|
|
|
@@ -139,6 +142,7 @@ export async function executeConnectorRoute ({
|
|
|
139
142
|
routeMeta,
|
|
140
143
|
helpers,
|
|
141
144
|
mountPath = '',
|
|
145
|
+
publicBaseUrl = '',
|
|
142
146
|
runHooks
|
|
143
147
|
}) {
|
|
144
148
|
const result = await handler({
|
|
@@ -150,6 +154,14 @@ export async function executeConnectorRoute ({
|
|
|
150
154
|
})
|
|
151
155
|
|
|
152
156
|
const status = determineResponseStatus(method, result)
|
|
157
|
+
const location = getConnectorLocationHeader({
|
|
158
|
+
method,
|
|
159
|
+
context,
|
|
160
|
+
routeMeta,
|
|
161
|
+
helpers,
|
|
162
|
+
mountPath,
|
|
163
|
+
publicBaseUrl
|
|
164
|
+
})
|
|
153
165
|
const transportHeaders = await applyTransportResponseLifecycle({
|
|
154
166
|
context,
|
|
155
167
|
transportData,
|
|
@@ -165,13 +177,7 @@ export async function executeConnectorRoute ({
|
|
|
165
177
|
...(result?.headers || {}),
|
|
166
178
|
...transportHeaders
|
|
167
179
|
},
|
|
168
|
-
location
|
|
169
|
-
method,
|
|
170
|
-
context,
|
|
171
|
-
routeMeta,
|
|
172
|
-
helpers,
|
|
173
|
-
mountPath
|
|
174
|
-
}),
|
|
180
|
+
location,
|
|
175
181
|
result
|
|
176
182
|
}
|
|
177
183
|
}
|
|
@@ -1,70 +1,11 @@
|
|
|
1
1
|
import { getRequestContracts } from '../../lib/querying-writing/request-contracts.js'
|
|
2
2
|
|
|
3
|
-
const STRING_OR_NUMBER_SCHEMA = {
|
|
4
|
-
anyOf: [
|
|
5
|
-
{ type: 'string' },
|
|
6
|
-
{ type: 'number' }
|
|
7
|
-
]
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function buildResourceIdentifierSchema () {
|
|
11
|
-
return {
|
|
12
|
-
type: 'object',
|
|
13
|
-
additionalProperties: false,
|
|
14
|
-
required: ['type', 'id'],
|
|
15
|
-
properties: {
|
|
16
|
-
type: { type: 'string', minLength: 1 },
|
|
17
|
-
id: STRING_OR_NUMBER_SCHEMA
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildRelationshipValueSchema (operation) {
|
|
23
|
-
if (operation === 'patchRelationship') {
|
|
24
|
-
return {
|
|
25
|
-
anyOf: [
|
|
26
|
-
{ type: 'null' },
|
|
27
|
-
buildResourceIdentifierSchema(),
|
|
28
|
-
{
|
|
29
|
-
type: 'array',
|
|
30
|
-
items: buildResourceIdentifierSchema()
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
type: 'array',
|
|
38
|
-
items: buildResourceIdentifierSchema()
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function buildRelationshipRouteBodySchema (operation) {
|
|
43
|
-
if (!['postRelationship', 'patchRelationship', 'deleteRelationship'].includes(operation)) {
|
|
44
|
-
return null
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
type: 'object',
|
|
49
|
-
additionalProperties: false,
|
|
50
|
-
required: ['data'],
|
|
51
|
-
properties: {
|
|
52
|
-
data: buildRelationshipValueSchema(operation)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
3
|
export function buildTransportRouteSchema ({ routeMeta, api }) {
|
|
58
4
|
if (!routeMeta) {
|
|
59
5
|
return null
|
|
60
6
|
}
|
|
61
7
|
|
|
62
|
-
if (
|
|
63
|
-
const relationshipBodySchema = buildRelationshipRouteBodySchema(routeMeta.operation)
|
|
64
|
-
return relationshipBodySchema ? { body: relationshipBodySchema } : null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (routeMeta.kind !== 'resource' || !['post', 'put', 'patch'].includes(routeMeta.operation)) {
|
|
8
|
+
if (!['resource', 'relationship'].includes(routeMeta.kind)) {
|
|
68
9
|
return null
|
|
69
10
|
}
|
|
70
11
|
|
|
@@ -86,6 +27,17 @@ export function buildTransportRouteSchema ({ routeMeta, api }) {
|
|
|
86
27
|
return null
|
|
87
28
|
}
|
|
88
29
|
|
|
30
|
+
if (routeMeta.kind === 'resource' && !['post', 'put', 'patch'].includes(routeMeta.operation)) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
routeMeta.kind === 'relationship' &&
|
|
36
|
+
!['postRelationship', 'patchRelationship', 'deleteRelationship'].includes(routeMeta.operation)
|
|
37
|
+
) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
89
41
|
return {
|
|
90
42
|
body: contract.schema.toJsonSchema({ mode: contract.mode, additionalProperties: false })
|
|
91
43
|
}
|
|
@@ -65,6 +65,43 @@ export const FileHandlingPlugin = {
|
|
|
65
65
|
// Store detectors array for inspection
|
|
66
66
|
api.rest.fileDetectors = detectorRegistry
|
|
67
67
|
|
|
68
|
+
const trackUploadedFile = (context, upload) => {
|
|
69
|
+
if (!context.fileHandlingUploads) {
|
|
70
|
+
context.fileHandlingUploads = []
|
|
71
|
+
}
|
|
72
|
+
context.fileHandlingUploads.push(upload)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cleanupParsedFiles = async (files = {}) => {
|
|
76
|
+
for (const file of Object.values(files)) {
|
|
77
|
+
if (!file?.cleanup) continue
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await file.cleanup()
|
|
81
|
+
} catch (error) {
|
|
82
|
+
log.warn('Failed to cleanup temp file:', error)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cleanupUploadedFiles = async (context) => {
|
|
88
|
+
const uploads = context.fileHandlingUploads
|
|
89
|
+
if (!uploads || uploads.length === 0) return
|
|
90
|
+
|
|
91
|
+
context.fileHandlingUploads = []
|
|
92
|
+
for (const upload of uploads) {
|
|
93
|
+
if (!upload?.url || typeof upload.storage?.delete !== 'function') {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await upload.storage.delete(upload.url)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
log.warn(`Failed to cleanup uploaded file '${upload.url}' after rollback:`, error)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
68
105
|
/**
|
|
69
106
|
* Analyze a scope's schema to find file fields
|
|
70
107
|
*/
|
|
@@ -152,125 +189,114 @@ export const FileHandlingPlugin = {
|
|
|
152
189
|
}
|
|
153
190
|
|
|
154
191
|
log.debug(`Processing files with detector '${detectorUsed}'`)
|
|
155
|
-
const { fields, files } = parsed
|
|
192
|
+
const { fields = {}, files = {} } = parsed
|
|
156
193
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
194
|
+
try {
|
|
195
|
+
// Process each file field defined in schema
|
|
196
|
+
for (const fieldConfig of fileFields) {
|
|
197
|
+
const file = files[fieldConfig.field]
|
|
198
|
+
|
|
199
|
+
if (!file) {
|
|
200
|
+
if (fieldConfig.required) {
|
|
201
|
+
throw new RestApiValidationError(
|
|
202
|
+
`Required file field '${fieldConfig.field}' is missing`,
|
|
203
|
+
{
|
|
204
|
+
fields: [fieldConfig.field],
|
|
205
|
+
violations: [{
|
|
206
|
+
field: fieldConfig.field,
|
|
207
|
+
message: 'This field is required'
|
|
208
|
+
}]
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
160
214
|
|
|
161
|
-
|
|
162
|
-
if (fieldConfig.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
field: fieldConfig.field,
|
|
169
|
-
message: 'This field is required'
|
|
170
|
-
}]
|
|
215
|
+
// Validate mime type
|
|
216
|
+
if (fieldConfig.accepts[0] !== '*') {
|
|
217
|
+
const acceptable = fieldConfig.accepts.some(pattern => {
|
|
218
|
+
if (pattern.endsWith('/*')) {
|
|
219
|
+
// e.g., 'image/*'
|
|
220
|
+
const prefix = pattern.slice(0, -2)
|
|
221
|
+
return file.mimetype.startsWith(prefix + '/')
|
|
171
222
|
}
|
|
172
|
-
|
|
223
|
+
return file.mimetype === pattern
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if (!acceptable) {
|
|
227
|
+
throw new RestApiValidationError(
|
|
228
|
+
`Invalid file type for field '${fieldConfig.field}'`,
|
|
229
|
+
{
|
|
230
|
+
fields: [fieldConfig.field],
|
|
231
|
+
violations: [{
|
|
232
|
+
field: fieldConfig.field,
|
|
233
|
+
message: `Expected ${fieldConfig.accepts.join(' or ')}, got ${file.mimetype}`
|
|
234
|
+
}]
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
}
|
|
173
238
|
}
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
239
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
240
|
+
// Validate file size
|
|
241
|
+
if (fieldConfig.maxSize) {
|
|
242
|
+
const maxBytes = parseSize(fieldConfig.maxSize)
|
|
243
|
+
if (file.size > maxBytes) {
|
|
244
|
+
throw new RestApiValidationError(
|
|
245
|
+
`File too large for field '${fieldConfig.field}'`,
|
|
246
|
+
{
|
|
247
|
+
fields: [fieldConfig.field],
|
|
248
|
+
violations: [{
|
|
249
|
+
field: fieldConfig.field,
|
|
250
|
+
message: `Maximum size is ${fieldConfig.maxSize}, got ${formatSize(file.size)}`
|
|
251
|
+
}]
|
|
252
|
+
}
|
|
253
|
+
)
|
|
184
254
|
}
|
|
185
|
-
|
|
186
|
-
})
|
|
255
|
+
}
|
|
187
256
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
fields: [fieldConfig.field],
|
|
193
|
-
violations: [{
|
|
194
|
-
field: fieldConfig.field,
|
|
195
|
-
message: `Expected ${fieldConfig.accepts.join(' or ')}, got ${file.mimetype}`
|
|
196
|
-
}]
|
|
197
|
-
}
|
|
198
|
-
)
|
|
257
|
+
// Upload to storage
|
|
258
|
+
if (!fieldConfig.storage) {
|
|
259
|
+
throw new Error(`No storage configured for file field '${fieldConfig.field}'`)
|
|
199
260
|
}
|
|
200
|
-
}
|
|
201
261
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
262
|
+
try {
|
|
263
|
+
const storedUrl = await fieldConfig.storage.upload(file)
|
|
264
|
+
fields[fieldConfig.field] = storedUrl
|
|
265
|
+
trackUploadedFile(context, {
|
|
266
|
+
field: fieldConfig.field,
|
|
267
|
+
storage: fieldConfig.storage,
|
|
268
|
+
url: storedUrl
|
|
269
|
+
})
|
|
270
|
+
log.debug(`Uploaded file for field '${fieldConfig.field}' to: ${storedUrl}`)
|
|
271
|
+
} catch (error) {
|
|
206
272
|
throw new RestApiValidationError(
|
|
207
|
-
`
|
|
273
|
+
`Failed to upload file for field '${fieldConfig.field}': ${error.message}`,
|
|
208
274
|
{
|
|
209
275
|
fields: [fieldConfig.field],
|
|
210
276
|
violations: [{
|
|
211
277
|
field: fieldConfig.field,
|
|
212
|
-
message:
|
|
278
|
+
message: error.message
|
|
213
279
|
}]
|
|
214
280
|
}
|
|
215
281
|
)
|
|
216
282
|
}
|
|
217
283
|
}
|
|
218
284
|
|
|
219
|
-
//
|
|
220
|
-
if (!
|
|
221
|
-
|
|
285
|
+
// Replace inputRecord with processed data
|
|
286
|
+
if (!params.inputRecord) {
|
|
287
|
+
params.inputRecord = { data: { attributes: {} } }
|
|
222
288
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const storedUrl = await fieldConfig.storage.upload(file)
|
|
226
|
-
fields[fieldConfig.field] = storedUrl
|
|
227
|
-
log.debug(`Uploaded file for field '${fieldConfig.field}' to: ${storedUrl}`)
|
|
228
|
-
} catch (error) {
|
|
229
|
-
// Cleanup if file has cleanup function
|
|
230
|
-
if (file.cleanup) {
|
|
231
|
-
try {
|
|
232
|
-
await file.cleanup()
|
|
233
|
-
} catch (cleanupError) {
|
|
234
|
-
log.warn('Failed to cleanup file after upload error:', cleanupError)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
throw new RestApiValidationError(
|
|
239
|
-
`Failed to upload file for field '${fieldConfig.field}': ${error.message}`,
|
|
240
|
-
{
|
|
241
|
-
fields: [fieldConfig.field],
|
|
242
|
-
violations: [{
|
|
243
|
-
field: fieldConfig.field,
|
|
244
|
-
message: error.message
|
|
245
|
-
}]
|
|
246
|
-
}
|
|
247
|
-
)
|
|
289
|
+
if (!params.inputRecord.data) {
|
|
290
|
+
params.inputRecord.data = { attributes: {} }
|
|
248
291
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// Replace inputRecord with processed data
|
|
252
|
-
if (!params.inputRecord) {
|
|
253
|
-
params.inputRecord = { data: { attributes: {} } }
|
|
254
|
-
}
|
|
255
|
-
if (!params.inputRecord.data) {
|
|
256
|
-
params.inputRecord.data = { attributes: {} }
|
|
257
|
-
}
|
|
258
|
-
if (!params.inputRecord.data.attributes) {
|
|
259
|
-
params.inputRecord.data.attributes = {}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Merge fields into attributes
|
|
263
|
-
Object.assign(params.inputRecord.data.attributes, fields)
|
|
264
|
-
|
|
265
|
-
// Cleanup any remaining temp files
|
|
266
|
-
for (const file of Object.values(files)) {
|
|
267
|
-
if (file.cleanup) {
|
|
268
|
-
try {
|
|
269
|
-
await file.cleanup()
|
|
270
|
-
} catch (error) {
|
|
271
|
-
log.warn('Failed to cleanup temp file:', error)
|
|
272
|
-
}
|
|
292
|
+
if (!params.inputRecord.data.attributes) {
|
|
293
|
+
params.inputRecord.data.attributes = {}
|
|
273
294
|
}
|
|
295
|
+
|
|
296
|
+
// Merge fields into attributes
|
|
297
|
+
Object.assign(params.inputRecord.data.attributes, fields)
|
|
298
|
+
} finally {
|
|
299
|
+
await cleanupParsedFiles(files)
|
|
274
300
|
}
|
|
275
301
|
}
|
|
276
302
|
|
|
@@ -291,6 +317,10 @@ export const FileHandlingPlugin = {
|
|
|
291
317
|
await processFiles(scopeName, params, context)
|
|
292
318
|
})
|
|
293
319
|
|
|
320
|
+
addHook('afterRollback', 'cleanupUploadedFiles', {}, async ({ context }) => {
|
|
321
|
+
await cleanupUploadedFiles(context)
|
|
322
|
+
})
|
|
323
|
+
|
|
294
324
|
log.info('File handling plugin initialized successfully')
|
|
295
325
|
}
|
|
296
326
|
}
|
|
@@ -9,6 +9,21 @@ const clone = (value) => {
|
|
|
9
9
|
|
|
10
10
|
const SUPPORTED_TYPES = new Set([...TYPE_TO_POOL.keys(), 'id'])
|
|
11
11
|
|
|
12
|
+
const resolveIdProperty = (schema = {}, explicitIdProperty = null) => {
|
|
13
|
+
if (explicitIdProperty) return explicitIdProperty
|
|
14
|
+
if (schema.id) return 'id'
|
|
15
|
+
|
|
16
|
+
const idFields = Object.entries(schema)
|
|
17
|
+
.filter(([, fieldDef]) => (
|
|
18
|
+
fieldDef?.type === 'id' &&
|
|
19
|
+
!fieldDef.belongsTo &&
|
|
20
|
+
!fieldDef.belongsToPolymorphic
|
|
21
|
+
))
|
|
22
|
+
.map(([fieldName]) => fieldName)
|
|
23
|
+
|
|
24
|
+
return idFields.length === 1 ? idFields[0] : 'id'
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
const BELONGS_TO_ALIAS = (fieldName, fieldDef) => {
|
|
13
28
|
if (fieldDef.as) return fieldDef.as
|
|
14
29
|
if (fieldName.endsWith('_id')) {
|
|
@@ -305,6 +320,7 @@ export class AnyapiRegistry {
|
|
|
305
320
|
|
|
306
321
|
async #register (definition, externalTrx) {
|
|
307
322
|
const { tenant, resource, schema, relationships = {}, canonicalFieldMap = null } = definition
|
|
323
|
+
const idProperty = resolveIdProperty(schema, definition.idProperty)
|
|
308
324
|
const trx = externalTrx || await this.knex.transaction()
|
|
309
325
|
const managed = !externalTrx
|
|
310
326
|
|
|
@@ -520,7 +536,7 @@ export class AnyapiRegistry {
|
|
|
520
536
|
await trx('any_relationship_configs').insert(relationshipInserts)
|
|
521
537
|
}
|
|
522
538
|
|
|
523
|
-
const descriptor = await this.#loadDescriptor(tenant, resource, trx)
|
|
539
|
+
const descriptor = await this.#loadDescriptor(tenant, resource, trx, { idProperty })
|
|
524
540
|
|
|
525
541
|
if (managed) {
|
|
526
542
|
await trx.commit()
|
|
@@ -535,7 +551,7 @@ export class AnyapiRegistry {
|
|
|
535
551
|
}
|
|
536
552
|
}
|
|
537
553
|
|
|
538
|
-
async #loadDescriptor (tenant, resource, trx) {
|
|
554
|
+
async #loadDescriptor (tenant, resource, trx, options = {}) {
|
|
539
555
|
const query = (trx || this.knex)('any_resource_configs')
|
|
540
556
|
.where({ tenant_id: tenant, resource })
|
|
541
557
|
.first()
|
|
@@ -568,6 +584,7 @@ export class AnyapiRegistry {
|
|
|
568
584
|
slotState,
|
|
569
585
|
fieldRows,
|
|
570
586
|
relationshipRows,
|
|
587
|
+
idProperty: options.idProperty,
|
|
571
588
|
})
|
|
572
589
|
|
|
573
590
|
const canonicalFieldMap = {}
|
|
@@ -589,7 +606,7 @@ export class AnyapiRegistry {
|
|
|
589
606
|
return descriptor
|
|
590
607
|
}
|
|
591
608
|
|
|
592
|
-
#buildDescriptor ({ tenant, resource, schema, relationships, slotState, fieldRows, relationshipRows }) {
|
|
609
|
+
#buildDescriptor ({ tenant, resource, schema, relationships, slotState, fieldRows, relationshipRows, idProperty }) {
|
|
593
610
|
const fields = {}
|
|
594
611
|
const reverseAttributes = {}
|
|
595
612
|
const belongsTo = {}
|
|
@@ -671,6 +688,7 @@ export class AnyapiRegistry {
|
|
|
671
688
|
return {
|
|
672
689
|
tenant,
|
|
673
690
|
resource,
|
|
691
|
+
idProperty: resolveIdProperty(schema, idProperty),
|
|
674
692
|
schema,
|
|
675
693
|
relationships,
|
|
676
694
|
canonical: DEFAULT_CANONICAL_CONFIG,
|
|
@@ -71,14 +71,6 @@ const SELECT_METHODS = new Set(['select', 'columns', 'distinct'])
|
|
|
71
71
|
|
|
72
72
|
const AGGREGATE_METHODS = new Set(['count', 'min', 'max', 'sum', 'avg'])
|
|
73
73
|
|
|
74
|
-
const CALLBACK_FIRST_METHODS = new Set([
|
|
75
|
-
'where',
|
|
76
|
-
'andWhere',
|
|
77
|
-
'orWhere',
|
|
78
|
-
'having',
|
|
79
|
-
'orHaving',
|
|
80
|
-
])
|
|
81
|
-
|
|
82
74
|
const isFunction = (value) => typeof value === 'function'
|
|
83
75
|
const isObject = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
84
76
|
|
|
@@ -149,13 +141,6 @@ const translateJoinObject = (adapter, obj) => {
|
|
|
149
141
|
return result
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
const maybeWrapCallback = (adapter, callback) => {
|
|
153
|
-
if (!isFunction(callback)) return callback
|
|
154
|
-
return function wrappedCallback (...args) {
|
|
155
|
-
return callback.apply(adapter.proxy, args)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
144
|
const isRaw = (value) => Boolean(value && typeof value === 'object' && typeof value.toSQL === 'function' && value.toSQL().method)
|
|
160
145
|
|
|
161
146
|
const ensureArray = (value) => (Array.isArray(value) ? value : [value])
|
|
@@ -317,12 +302,39 @@ export class AnyapiQueryAdapter {
|
|
|
317
302
|
this.aliasActualNames.set(alias, alias)
|
|
318
303
|
}
|
|
319
304
|
|
|
320
|
-
#
|
|
305
|
+
#wrapQueryCallback (callback) {
|
|
306
|
+
if (!isFunction(callback)) return callback
|
|
307
|
+
const adapter = this
|
|
308
|
+
|
|
309
|
+
return function wrappedCallback (...args) {
|
|
310
|
+
const callbackTarget = this
|
|
311
|
+
const callbackProxy = adapter.#createProxy(callbackTarget)
|
|
312
|
+
const callbackArgs = args.map((arg) => arg === callbackTarget ? callbackProxy : arg)
|
|
313
|
+
return callback.apply(callbackProxy, callbackArgs)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#wrapJoinCallback (callback) {
|
|
318
|
+
if (!isFunction(callback)) return callback
|
|
319
|
+
const adapter = this
|
|
320
|
+
|
|
321
|
+
return function wrappedCallback (...args) {
|
|
322
|
+
const callbackTarget = this
|
|
323
|
+
const callbackProxy = adapter.#createJoinProxy(callbackTarget)
|
|
324
|
+
const callbackArgs = args.map((arg) => arg === callbackTarget ? callbackProxy : arg)
|
|
325
|
+
return callback.apply(callbackProxy, callbackArgs)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#translateWhereArgs (args, { joinClause = false } = {}) {
|
|
321
330
|
if (args.length === 0) return args
|
|
322
331
|
const [first, ...rest] = args
|
|
323
332
|
|
|
324
333
|
if (isFunction(first)) {
|
|
325
|
-
|
|
334
|
+
const wrappedGroup = joinClause
|
|
335
|
+
? this.#wrapJoinCallback(first)
|
|
336
|
+
: this.#wrapQueryCallback(first)
|
|
337
|
+
return [wrappedGroup, ...rest]
|
|
326
338
|
}
|
|
327
339
|
|
|
328
340
|
if (isObject(first) && !isRaw(first)) {
|
|
@@ -337,7 +349,7 @@ export class AnyapiQueryAdapter {
|
|
|
337
349
|
|
|
338
350
|
if (rest.length > 0 && isFunction(rest[rest.length - 1])) {
|
|
339
351
|
const newRest = [...rest]
|
|
340
|
-
newRest[newRest.length - 1] =
|
|
352
|
+
newRest[newRest.length - 1] = this.#wrapQueryCallback(rest[rest.length - 1])
|
|
341
353
|
return [translatedFirst, ...newRest]
|
|
342
354
|
}
|
|
343
355
|
|
|
@@ -350,7 +362,7 @@ export class AnyapiQueryAdapter {
|
|
|
350
362
|
const translatedColumn = this.translateColumn(column)
|
|
351
363
|
|
|
352
364
|
if (isFunction(values)) {
|
|
353
|
-
return [translatedColumn,
|
|
365
|
+
return [translatedColumn, this.#wrapQueryCallback(values), ...rest]
|
|
354
366
|
}
|
|
355
367
|
|
|
356
368
|
return [translatedColumn, values, ...rest]
|
|
@@ -439,7 +451,7 @@ export class AnyapiQueryAdapter {
|
|
|
439
451
|
const [first, ...rest] = args
|
|
440
452
|
|
|
441
453
|
if (isFunction(first)) {
|
|
442
|
-
return [
|
|
454
|
+
return [this.#wrapJoinCallback(first), ...rest]
|
|
443
455
|
}
|
|
444
456
|
|
|
445
457
|
if (isObject(first) && !isRaw(first)) {
|
|
@@ -454,7 +466,7 @@ export class AnyapiQueryAdapter {
|
|
|
454
466
|
|
|
455
467
|
const translatedRest = rest.map((value, index) => {
|
|
456
468
|
if (isFunction(value)) {
|
|
457
|
-
return
|
|
469
|
+
return this.#wrapJoinCallback(value)
|
|
458
470
|
}
|
|
459
471
|
if (typeof value !== 'string') {
|
|
460
472
|
return value
|
|
@@ -475,7 +487,7 @@ export class AnyapiQueryAdapter {
|
|
|
475
487
|
const translatedColumn = typeof column === 'string' ? this.translateColumn(column) : column
|
|
476
488
|
|
|
477
489
|
if (isFunction(values)) {
|
|
478
|
-
return [translatedColumn,
|
|
490
|
+
return [translatedColumn, this.#wrapQueryCallback(values), ...rest]
|
|
479
491
|
}
|
|
480
492
|
|
|
481
493
|
return [translatedColumn, values, ...rest]
|
|
@@ -510,7 +522,7 @@ export class AnyapiQueryAdapter {
|
|
|
510
522
|
let translatedArgs = args
|
|
511
523
|
|
|
512
524
|
if (WHERE_METHODS.has(prop)) {
|
|
513
|
-
translatedArgs = adapter.#translateWhereArgs(
|
|
525
|
+
translatedArgs = adapter.#translateWhereArgs(args, { joinClause: true })
|
|
514
526
|
} else if (WHERE_IN_METHODS.has(prop)) {
|
|
515
527
|
translatedArgs = adapter.#translateWhereInArgs(args)
|
|
516
528
|
} else if (BETWEEN_METHODS.has(prop)) {
|
|
@@ -607,7 +619,7 @@ export class AnyapiQueryAdapter {
|
|
|
607
619
|
let translatedArgs = args
|
|
608
620
|
|
|
609
621
|
if (WHERE_METHODS.has(prop)) {
|
|
610
|
-
translatedArgs = adapter.#translateWhereArgs(
|
|
622
|
+
translatedArgs = adapter.#translateWhereArgs(args)
|
|
611
623
|
} else if (WHERE_IN_METHODS.has(prop)) {
|
|
612
624
|
translatedArgs = adapter.#translateWhereInArgs(args)
|
|
613
625
|
} else if (BETWEEN_METHODS.has(prop)) {
|
|
@@ -630,8 +642,6 @@ export class AnyapiQueryAdapter {
|
|
|
630
642
|
translatedArgs = adapter.#translateSelectArgs(args)
|
|
631
643
|
} else if (AGGREGATE_METHODS.has(prop)) {
|
|
632
644
|
translatedArgs = adapter.#translateAggregateArgs(args)
|
|
633
|
-
} else if (CALLBACK_FIRST_METHODS.has(prop) && args.length && isFunction(args[0])) {
|
|
634
|
-
translatedArgs = [maybeWrapCallback(adapter, args[0]), ...args.slice(1)]
|
|
635
645
|
}
|
|
636
646
|
|
|
637
647
|
const result = value.apply(target, translatedArgs)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RestApiValidationError } from '../../../../../lib/rest-api-errors.js'
|
|
2
|
+
import { normalizeDateValue } from '../../querying-writing/database-value-normalizers.js'
|
|
2
3
|
|
|
3
4
|
export const normalizeId = (value) => (value === null || value === undefined ? null : String(value))
|
|
4
5
|
|
|
@@ -21,7 +22,8 @@ export const findSchemaFieldByAlias = (descriptor, alias) => {
|
|
|
21
22
|
|
|
22
23
|
export const resolveFieldInfo = (descriptor, field) => {
|
|
23
24
|
if (!descriptor) return null
|
|
24
|
-
|
|
25
|
+
const idProperty = descriptor.idProperty || 'id'
|
|
26
|
+
if (field === 'id' || field === idProperty) {
|
|
25
27
|
return {
|
|
26
28
|
column: descriptor?.canonical?.logicalIdColumn || 'logical_id',
|
|
27
29
|
definition: { type: 'id' }
|
|
@@ -90,6 +92,10 @@ export const coerceValueForDefinition = (value, definition, { isRelationship } =
|
|
|
90
92
|
return String(value)
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
if (['date', 'dateTime', 'time'].includes(type)) {
|
|
96
|
+
return normalizeDateValue(value, type)
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
return value
|
|
94
100
|
}
|
|
95
101
|
|