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.
Files changed (84) hide show
  1. package/README.md +2 -1
  2. package/docs/API.md +12 -9
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Row_Policies.md +605 -0
  10. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  11. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  12. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  13. package/docs/GUIDE/index.md +4 -1
  14. package/fixing_plan.md +112 -0
  15. package/index.js +2 -0
  16. package/package.json +1 -1
  17. package/plugins/core/bulk-operations-plugin.js +5 -4
  18. package/plugins/core/connectors/express-plugin.js +6 -1
  19. package/plugins/core/connectors/fastify-plugin.js +5 -1
  20. package/plugins/core/connectors/lib/connector-core.js +17 -11
  21. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  22. package/plugins/core/file-handling-plugin.js +126 -96
  23. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  24. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +36 -26
  25. package/plugins/core/lib/anyapi/schema-utils.js +1 -0
  26. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
  27. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
  28. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  29. package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
  30. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  31. package/plugins/core/lib/querying/storage-adapter-utils.js +8 -8
  32. package/plugins/core/lib/querying/url-helpers.js +9 -21
  33. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  34. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  35. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  36. package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
  37. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  38. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  39. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  40. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
  41. package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
  42. package/plugins/core/lib/storage/storage-adapter.js +74 -13
  43. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
  44. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  45. package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
  46. package/plugins/core/rest-api-knex-plugin.js +70 -60
  47. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
  48. package/plugins/core/rest-api-plugin-methods/common.js +85 -63
  49. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
  50. package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
  51. package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
  52. package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
  53. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
  54. package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
  55. package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
  56. package/plugins/core/rest-api-plugin-methods/post.js +4 -7
  57. package/plugins/core/rest-api-plugin-methods/put.js +4 -7
  58. package/plugins/core/rest-api-plugin.js +6 -36
  59. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  60. package/plugins/storage/local-storage.js +44 -19
  61. package/plugins/storage/s3-storage.js +21 -55
  62. package/tests/README.md +1 -1
  63. package/tests/anyapi-cursor-pagination.test.js +7 -1
  64. package/tests/anyapi-custom-idproperty.test.js +163 -0
  65. package/tests/fastify-plugin.test.js +28 -0
  66. package/tests/field-setters.test.js +10 -10
  67. package/tests/file-handling.test.js +118 -0
  68. package/tests/fixtures/api-configs.js +383 -4
  69. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  70. package/tests/http-connectors-parity.test.js +165 -5
  71. package/tests/local-storage.test.js +96 -0
  72. package/tests/pagination-cursor-multifield.test.js +121 -0
  73. package/tests/pagination-enhanced.test.js +66 -15
  74. package/tests/post-commit-hooks.test.js +120 -0
  75. package/tests/public-exports.test.js +15 -0
  76. package/tests/put-create-parity.test.js +216 -0
  77. package/tests/relationship-endpoints.test.js +259 -0
  78. package/tests/return-record-settings.test.js +184 -0
  79. package/tests/row-policy.test.js +489 -0
  80. package/tests/s3-storage.test.js +38 -0
  81. package/tests/searchschema-merge.test.js +17 -0
  82. package/tests/socketio.test.js +62 -0
  83. package/tests/storage-mapping.test.js +1 -0
  84. 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.urlPrefix || mountPath
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: getConnectorLocationHeader({
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 (routeMeta.kind === 'relationship') {
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
- // Process each file field defined in schema
158
- for (const fieldConfig of fileFields) {
159
- const file = files[fieldConfig.field]
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
- if (!file) {
162
- if (fieldConfig.required) {
163
- throw new RestApiValidationError(
164
- `Required file field '${fieldConfig.field}' is missing`,
165
- {
166
- fields: [fieldConfig.field],
167
- violations: [{
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
- // Validate mime type
178
- if (fieldConfig.accepts[0] !== '*') {
179
- const acceptable = fieldConfig.accepts.some(pattern => {
180
- if (pattern.endsWith('/*')) {
181
- // e.g., 'image/*'
182
- const prefix = pattern.slice(0, -2)
183
- return file.mimetype.startsWith(prefix + '/')
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
- return file.mimetype === pattern
186
- })
255
+ }
187
256
 
188
- if (!acceptable) {
189
- throw new RestApiValidationError(
190
- `Invalid file type for field '${fieldConfig.field}'`,
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
- // Validate file size
203
- if (fieldConfig.maxSize) {
204
- const maxBytes = parseSize(fieldConfig.maxSize)
205
- if (file.size > maxBytes) {
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
- `File too large for field '${fieldConfig.field}'`,
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: `Maximum size is ${fieldConfig.maxSize}, got ${formatSize(file.size)}`
278
+ message: error.message
213
279
  }]
214
280
  }
215
281
  )
216
282
  }
217
283
  }
218
284
 
219
- // Upload to storage
220
- if (!fieldConfig.storage) {
221
- throw new Error(`No storage configured for file field '${fieldConfig.field}'`)
285
+ // Replace inputRecord with processed data
286
+ if (!params.inputRecord) {
287
+ params.inputRecord = { data: { attributes: {} } }
222
288
  }
223
-
224
- try {
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
- #translateWhereArgs (method, args) {
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
- return [maybeWrapCallback(this, first), ...rest]
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] = maybeWrapCallback(this, rest[rest.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, maybeWrapCallback(this, values), ...rest]
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 [maybeWrapCallback(this, first), ...rest]
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 maybeWrapCallback(this, value)
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, maybeWrapCallback(this, values), ...rest]
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(prop, args)
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(prop, args)
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)
@@ -210,6 +210,7 @@ export const TYPE_TO_POOL = new Map([
210
210
  ['text', 'string'],
211
211
  ['uuid', 'string'],
212
212
  ['email', 'string'],
213
+ ['file', 'string'],
213
214
  ['number', 'number'],
214
215
  ['integer', 'number'],
215
216
  ['float', 'number'],
@@ -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
- if (field === 'id') {
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