json-rest-api 1.0.24 → 1.0.25

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 (82) 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 +603 -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/schema-utils.js +1 -0
  25. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
  26. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
  27. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
  29. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  30. package/plugins/core/lib/querying/url-helpers.js +9 -21
  31. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  32. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  33. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  34. package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
  35. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  36. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  37. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  38. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
  39. package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
  40. package/plugins/core/lib/storage/storage-adapter.js +74 -13
  41. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
  42. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  43. package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
  44. package/plugins/core/rest-api-knex-plugin.js +70 -60
  45. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
  46. package/plugins/core/rest-api-plugin-methods/common.js +85 -63
  47. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
  48. package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
  49. package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
  50. package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
  51. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
  52. package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
  53. package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
  54. package/plugins/core/rest-api-plugin-methods/post.js +4 -7
  55. package/plugins/core/rest-api-plugin-methods/put.js +4 -7
  56. package/plugins/core/rest-api-plugin.js +6 -36
  57. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  58. package/plugins/storage/local-storage.js +44 -19
  59. package/plugins/storage/s3-storage.js +21 -55
  60. package/tests/README.md +1 -1
  61. package/tests/anyapi-cursor-pagination.test.js +7 -1
  62. package/tests/anyapi-custom-idproperty.test.js +163 -0
  63. package/tests/fastify-plugin.test.js +28 -0
  64. package/tests/field-setters.test.js +10 -10
  65. package/tests/file-handling.test.js +118 -0
  66. package/tests/fixtures/api-configs.js +362 -4
  67. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  68. package/tests/http-connectors-parity.test.js +165 -5
  69. package/tests/local-storage.test.js +96 -0
  70. package/tests/pagination-cursor-multifield.test.js +121 -0
  71. package/tests/pagination-enhanced.test.js +66 -15
  72. package/tests/post-commit-hooks.test.js +120 -0
  73. package/tests/public-exports.test.js +15 -0
  74. package/tests/put-create-parity.test.js +216 -0
  75. package/tests/relationship-endpoints.test.js +259 -0
  76. package/tests/return-record-settings.test.js +184 -0
  77. package/tests/row-policy.test.js +433 -0
  78. package/tests/s3-storage.test.js +38 -0
  79. package/tests/searchschema-merge.test.js +17 -0
  80. package/tests/socketio.test.js +62 -0
  81. package/tests/storage-mapping.test.js +1 -0
  82. 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,
@@ -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
 
@@ -350,37 +350,42 @@ export const toJsonApiRecord = (scope, record, scopeName) => {
350
350
  export const buildJsonApiResponse = async (scope, records, included = [], isSingle = false, scopeName, context) => {
351
351
  const {
352
352
  vars: {
353
- schemaInfo: { schemaInstance, schemaRelationships: relationships, idProperty }
353
+ schemaInfo
354
354
  }
355
355
  } = scope
356
356
 
357
- const idField = idProperty || 'id'
357
+ const {
358
+ schemaInstance,
359
+ schemaRelationships: relationships
360
+ } = schemaInfo
358
361
 
359
362
  const schemaStructure = getSchemaStructure(schemaInstance)
360
363
 
361
364
  const processedRecords = records.map(record => {
362
365
  const { [RELATIONSHIPS_KEY]: _relationships, ...cleanRecord } = record
366
+ const logicalRecord = translateRecordFromStorage(cleanRecord, schemaInfo)
363
367
  const jsonApiRecord = toJsonApiRecord(scope, cleanRecord, scopeName)
368
+ const resourceId = jsonApiRecord.id
364
369
 
365
370
  if (_relationships) {
366
371
  jsonApiRecord.relationships = _relationships
367
372
  }
368
373
 
369
374
  for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
370
- if (fieldDef.belongsTo && fieldDef.as && fieldName in cleanRecord) {
375
+ if (fieldDef.belongsTo && fieldDef.as && fieldName in logicalRecord) {
371
376
  jsonApiRecord.relationships = jsonApiRecord.relationships || {}
372
377
  if (!jsonApiRecord.relationships[fieldDef.as]) {
373
- if (cleanRecord[fieldName] !== null && cleanRecord[fieldName] !== undefined) {
378
+ if (logicalRecord[fieldName] !== null && logicalRecord[fieldName] !== undefined) {
374
379
  const relationshipObject = {
375
380
  data: {
376
381
  type: fieldDef.belongsTo,
377
- id: String(cleanRecord[fieldName])
382
+ id: String(logicalRecord[fieldName])
378
383
  }
379
384
  }
380
385
 
381
386
  relationshipObject.links = {
382
- self: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, true),
383
- related: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, false)
387
+ self: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, true),
388
+ related: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, false)
384
389
  }
385
390
 
386
391
  jsonApiRecord.relationships[fieldDef.as] = relationshipObject
@@ -390,8 +395,8 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
390
395
  }
391
396
 
392
397
  relationshipObject.links = {
393
- self: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, true),
394
- related: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, false)
398
+ self: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, true),
399
+ related: buildRelationshipUrl(context, scope, scopeName, resourceId, fieldDef.as, false)
395
400
  }
396
401
 
397
402
  jsonApiRecord.relationships[fieldDef.as] = relationshipObject
@@ -402,8 +407,8 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
402
407
 
403
408
  Object.entries(relationships || {}).forEach(([relName, relDef]) => {
404
409
  if (relDef.belongsToPolymorphic) {
405
- const typeValue = cleanRecord[relDef.belongsToPolymorphic.typeField]
406
- const idValue = cleanRecord[relDef.belongsToPolymorphic.idField]
410
+ const typeValue = logicalRecord[relDef.belongsToPolymorphic.typeField]
411
+ const idValue = logicalRecord[relDef.belongsToPolymorphic.idField]
407
412
 
408
413
  if (typeValue && idValue) {
409
414
  jsonApiRecord.relationships = jsonApiRecord.relationships || {}
@@ -415,8 +420,8 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
415
420
  }
416
421
 
417
422
  relationshipObject.links = {
418
- self: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, true),
419
- related: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, false)
423
+ self: buildRelationshipUrl(context, scope, scopeName, resourceId, relName, true),
424
+ related: buildRelationshipUrl(context, scope, scopeName, resourceId, relName, false)
420
425
  }
421
426
 
422
427
  jsonApiRecord.relationships[relName] = relationshipObject
@@ -427,8 +432,8 @@ export const buildJsonApiResponse = async (scope, records, included = [], isSing
427
432
  }
428
433
 
429
434
  relationshipObject.links = {
430
- self: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, true),
431
- related: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, false)
435
+ self: buildRelationshipUrl(context, scope, scopeName, resourceId, relName, true),
436
+ related: buildRelationshipUrl(context, scope, scopeName, resourceId, relName, false)
432
437
  }
433
438
 
434
439
  jsonApiRecord.relationships[relName] = relationshipObject