json-rest-api 1.0.26 → 1.0.28
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/docs/API.md +5 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -2
- package/docs/GUIDE/GUIDE_Y_Appendices.md +42 -29
- package/index.js +2 -0
- package/lib/error-context.js +21 -0
- package/lib/rest-api-errors.js +70 -0
- package/package.json +2 -2
- package/plugins/core/connectors/lib/transport-http-helpers.js +21 -0
- package/plugins/core/lib/anyapi/schema-utils.js +2 -1
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -2
- package/plugins/core/lib/dbTablesOperations.js +20 -10
- package/plugins/core/lib/querying/knex-pagination-helpers.js +11 -1
- package/plugins/core/lib/querying/knex-process-includes.js +11 -5
- package/plugins/core/lib/querying/knex-relationship-includes.js +63 -46
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +231 -143
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -1
- package/plugins/core/lib/storage/canonical-storage-mapping.js +11 -1
- package/plugins/core/lib/storage/storage-adapter.js +4 -2
- package/plugins/core/lib/storage/storage-mapping.js +7 -1
- package/plugins/core/rest-api-plugin-methods/common.js +5 -0
- package/plugins/core/rest-api-plugin-methods/get.js +6 -0
- package/plugins/core/rest-api-plugin-methods/query.js +6 -0
- package/tests/computed-fields.test.js +90 -2
- package/tests/db-table-operations.test.js +13 -1
- package/tests/fixtures/api-configs.js +83 -3
- package/tests/includes.test.js +49 -0
- package/tests/pagination-cursor-multifield.test.js +4 -0
- package/tests/relationship-include-storage-adapter.test.js +56 -0
- package/tests/storage-mapping.test.js +112 -10
|
@@ -1,132 +1,214 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* @param {*} value - The date value from the database
|
|
5
|
-
* @param {string} type - The field type ('date', 'dateTime', or 'time')
|
|
6
|
-
* @returns {Date|string|null} Date object for date/dateTime, string for time, or null
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* // Input: Already a Date object
|
|
10
|
-
* normalizeDateValue(new Date('2024-01-15T10:30:00Z'), 'dateTime');
|
|
11
|
-
* // Output: Date object (unchanged)
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* // Input: MySQL datetime string without timezone
|
|
15
|
-
* normalizeDateValue('2024-01-15 10:30:00', 'dateTime');
|
|
16
|
-
* // Output: Date object parsed as UTC (2024-01-15T10:30:00Z)
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* // Input: Date-only string
|
|
20
|
-
* normalizeDateValue('2024-01-15', 'date');
|
|
21
|
-
* // Output: Date object at UTC midnight (2024-01-15T00:00:00Z)
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* // Input: Time field (always returns string)
|
|
25
|
-
* normalizeDateValue('14:30:45', 'time');
|
|
26
|
-
* // Output: "14:30:45" (string)
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* // Input: Date object for time field
|
|
30
|
-
* normalizeDateValue(new Date('2024-01-15T14:30:45Z'), 'time');
|
|
31
|
-
* // Output: "14:30:45" (extracts time portion)
|
|
32
|
-
*
|
|
33
|
-
* @description
|
|
34
|
-
* Used by:
|
|
35
|
-
* - normalizeAttributes to process individual field values
|
|
36
|
-
* - Applied to all date/time fields fetched from database
|
|
37
|
-
*
|
|
38
|
-
* Purpose:
|
|
39
|
-
* - Handles database-specific date formats (especially MySQL)
|
|
40
|
-
* - Prevents timezone shifts for date-only fields
|
|
41
|
-
* - Ensures consistent Date objects across databases
|
|
42
|
-
* - Keeps time fields as HH:MM:SS strings
|
|
43
|
-
*
|
|
44
|
-
* Data flow:
|
|
45
|
-
* 1. Checks for null/undefined (returns null)
|
|
46
|
-
* 2. For time fields: ensures HH:MM:SS string format
|
|
47
|
-
* 3. For date/dateTime: converts to Date objects
|
|
48
|
-
* 4. Handles MySQL datetime format (no T separator)
|
|
49
|
-
* 5. Forces UTC interpretation to prevent timezone issues
|
|
50
|
-
*/
|
|
51
|
-
export function normalizeDateValue (value, type) {
|
|
52
|
-
// Handle null/undefined
|
|
53
|
-
if (value === null || value === undefined) {
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
1
|
+
import { createSchema } from 'json-rest-schema'
|
|
2
|
+
import { RestApiTemporalDataError } from '../../../../lib/rest-api-errors.js'
|
|
56
3
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
4
|
+
const DATE_VALUE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
|
5
|
+
const RFC3339_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/
|
|
6
|
+
const SQL_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2}) ((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?)$/
|
|
7
|
+
const INTEGER_VALUE_PATTERN = /^-?\d+$/
|
|
8
|
+
const temporalContractCache = new Map()
|
|
9
|
+
|
|
10
|
+
const limitFractionalSecondPrecision = (value, temporalPrecision) => {
|
|
11
|
+
if (!Number.isInteger(temporalPrecision)) return value
|
|
12
|
+
|
|
13
|
+
return value.replace(
|
|
14
|
+
/(\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z)?$/,
|
|
15
|
+
(_match, wholeSeconds, fraction = '', utcSuffix = '') => {
|
|
16
|
+
const limitedFraction = fraction.slice(0, temporalPrecision)
|
|
17
|
+
return `${wholeSeconds}${limitedFraction ? `.${limitedFraction}` : ''}${utcSuffix}`
|
|
66
18
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getTemporalContract = (type, temporalPrecision) => {
|
|
23
|
+
const cacheKey = `${type}:${Number.isInteger(temporalPrecision) ? temporalPrecision : 'default'}`
|
|
24
|
+
if (!temporalContractCache.has(cacheKey)) {
|
|
25
|
+
temporalContractCache.set(cacheKey, createSchema({
|
|
26
|
+
value: {
|
|
27
|
+
type,
|
|
28
|
+
...(Number.isInteger(temporalPrecision) ? { temporalPrecision } : {})
|
|
72
29
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
30
|
+
}))
|
|
31
|
+
}
|
|
32
|
+
return temporalContractCache.get(cacheKey)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const throwTemporalDataError = ({ fieldName, resourceType, type, source }) => {
|
|
36
|
+
throw new RestApiTemporalDataError({
|
|
37
|
+
field: fieldName,
|
|
38
|
+
resourceType,
|
|
39
|
+
fieldType: type,
|
|
40
|
+
source
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const validateTemporalJsonValue = (value, type, {
|
|
45
|
+
temporalPrecision,
|
|
46
|
+
fieldName,
|
|
47
|
+
resourceType,
|
|
48
|
+
source
|
|
49
|
+
} = {}) => {
|
|
50
|
+
const { validatedObject, errors } = getTemporalContract(type, temporalPrecision).patch({ value })
|
|
51
|
+
if (Object.keys(errors).length > 0) {
|
|
52
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
53
|
+
}
|
|
54
|
+
return validatedObject.value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parseEpochDatabaseValue = (value) => {
|
|
58
|
+
let epochValue = null
|
|
59
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
60
|
+
epochValue = value
|
|
61
|
+
} else if (typeof value === 'bigint') {
|
|
62
|
+
epochValue = Number(value)
|
|
63
|
+
} else if (typeof value === 'string' && INTEGER_VALUE_PATTERN.test(value.trim())) {
|
|
64
|
+
epochValue = Number(value.trim())
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
|
|
67
|
+
if (!Number.isSafeInteger(epochValue)) return null
|
|
68
|
+
const parsed = new Date(epochValue)
|
|
69
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parseKnownDatabaseDate = (value, { fieldName, resourceType, type, source }) => {
|
|
78
73
|
if (value instanceof Date) {
|
|
79
|
-
return value
|
|
74
|
+
if (!Number.isNaN(value.getTime())) return value
|
|
75
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
const trimmed = value.trim()
|
|
78
|
+
const epochDate = parseEpochDatabaseValue(value)
|
|
79
|
+
if (epochDate) return epochDate
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return numericDate
|
|
90
|
-
}
|
|
91
|
-
}
|
|
81
|
+
if (typeof value !== 'string') {
|
|
82
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
83
|
+
}
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
const trimmed = value.trim()
|
|
86
|
+
const sqlMatch = SQL_DATETIME_PATTERN.exec(trimmed)
|
|
87
|
+
const rfc3339Match = RFC3339_DATETIME_PATTERN.exec(trimmed)
|
|
88
|
+
let dateTimeValue = null
|
|
89
|
+
|
|
90
|
+
if (sqlMatch) {
|
|
91
|
+
dateTimeValue = `${sqlMatch[1]}T${sqlMatch[2]}Z`
|
|
92
|
+
} else if (rfc3339Match) {
|
|
93
|
+
dateTimeValue = trimmed
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!dateTimeValue) {
|
|
97
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
98
|
+
}
|
|
107
99
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
validateTemporalJsonValue(dateTimeValue, 'dateTime', {
|
|
101
|
+
fieldName,
|
|
102
|
+
resourceType,
|
|
103
|
+
source
|
|
104
|
+
})
|
|
105
|
+
const parsed = new Date(dateTimeValue)
|
|
106
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
107
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
108
|
+
}
|
|
109
|
+
return parsed
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Converts a database temporal value to the public JSON representation used by
|
|
114
|
+
* json-rest-schema: YYYY-MM-DD for date, RFC 3339 for dateTime, and an
|
|
115
|
+
* offset-free string for time.
|
|
116
|
+
*/
|
|
117
|
+
export function normalizeDateValue (value, type, {
|
|
118
|
+
temporalPrecision,
|
|
119
|
+
fieldName,
|
|
120
|
+
resourceType,
|
|
121
|
+
source = 'database'
|
|
122
|
+
} = {}) {
|
|
123
|
+
if (value === null || value === undefined) return null
|
|
124
|
+
|
|
125
|
+
if (type === 'time') {
|
|
126
|
+
let timeValue = null
|
|
127
|
+
if (typeof value === 'string') {
|
|
128
|
+
timeValue = limitFractionalSecondPrecision(value.trim(), temporalPrecision)
|
|
129
|
+
}
|
|
130
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
131
|
+
timeValue = limitFractionalSecondPrecision(value.toISOString().slice(11, -1), temporalPrecision)
|
|
112
132
|
}
|
|
133
|
+
return validateTemporalJsonValue(timeValue, type, {
|
|
134
|
+
temporalPrecision,
|
|
135
|
+
fieldName,
|
|
136
|
+
resourceType,
|
|
137
|
+
source
|
|
138
|
+
})
|
|
113
139
|
}
|
|
114
140
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
if (type === 'date' && typeof value === 'string' && DATE_VALUE_PATTERN.test(value.trim())) {
|
|
142
|
+
return validateTemporalJsonValue(value.trim(), type, {
|
|
143
|
+
fieldName,
|
|
144
|
+
resourceType,
|
|
145
|
+
source
|
|
146
|
+
})
|
|
118
147
|
}
|
|
119
148
|
|
|
120
|
-
|
|
121
|
-
const
|
|
149
|
+
const dateValue = parseKnownDatabaseDate(value, { fieldName, resourceType, type, source })
|
|
150
|
+
const isoValue = dateValue.toISOString()
|
|
151
|
+
const normalized = type === 'date'
|
|
152
|
+
? isoValue.slice(0, 10)
|
|
153
|
+
: limitFractionalSecondPrecision(isoValue, temporalPrecision)
|
|
154
|
+
return validateTemporalJsonValue(normalized, type, {
|
|
155
|
+
temporalPrecision,
|
|
156
|
+
fieldName,
|
|
157
|
+
resourceType,
|
|
158
|
+
source
|
|
159
|
+
})
|
|
160
|
+
}
|
|
122
161
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Converts validated JSON temporal values to the native values expected by
|
|
164
|
+
* database drivers. JSON schema validation keeps date and dateTime values as
|
|
165
|
+
* strings; database writes and comparisons must normalize them consistently.
|
|
166
|
+
* Time values remain strings because database drivers accept their wire shape.
|
|
167
|
+
*/
|
|
168
|
+
export function normalizeValueForDatabaseStorage (value, type, {
|
|
169
|
+
temporalPrecision,
|
|
170
|
+
fieldName,
|
|
171
|
+
resourceType,
|
|
172
|
+
source = 'storage'
|
|
173
|
+
} = {}) {
|
|
174
|
+
if (!['date', 'dateTime', 'time'].includes(type)) return value
|
|
175
|
+
if (value === null || value === undefined) return null
|
|
176
|
+
|
|
177
|
+
if (type === 'time') {
|
|
178
|
+
if (typeof value !== 'string') {
|
|
179
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
180
|
+
}
|
|
181
|
+
return validateTemporalJsonValue(value, type, {
|
|
182
|
+
temporalPrecision,
|
|
183
|
+
fieldName,
|
|
184
|
+
resourceType,
|
|
185
|
+
source
|
|
186
|
+
})
|
|
127
187
|
}
|
|
128
188
|
|
|
129
|
-
|
|
189
|
+
let publicValue = value
|
|
190
|
+
if (value instanceof Date) {
|
|
191
|
+
if (Number.isNaN(value.getTime())) {
|
|
192
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
193
|
+
}
|
|
194
|
+
publicValue = type === 'date'
|
|
195
|
+
? value.toISOString().slice(0, 10)
|
|
196
|
+
: limitFractionalSecondPrecision(value.toISOString(), temporalPrecision)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const validatedValue = validateTemporalJsonValue(publicValue, type, {
|
|
200
|
+
temporalPrecision,
|
|
201
|
+
fieldName,
|
|
202
|
+
resourceType,
|
|
203
|
+
source
|
|
204
|
+
})
|
|
205
|
+
const databaseValue = new Date(
|
|
206
|
+
type === 'date' ? `${validatedValue}T00:00:00Z` : validatedValue
|
|
207
|
+
)
|
|
208
|
+
if (Number.isNaN(databaseValue.getTime())) {
|
|
209
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
210
|
+
}
|
|
211
|
+
return databaseValue
|
|
130
212
|
}
|
|
131
213
|
|
|
132
214
|
/**
|
|
@@ -155,13 +237,13 @@ export function normalizeDateValue (value, type) {
|
|
|
155
237
|
*
|
|
156
238
|
* const normalized = normalizeAttributes(attributes, schema);
|
|
157
239
|
*
|
|
158
|
-
* // Output:
|
|
240
|
+
* // Output: JSON-safe values
|
|
159
241
|
* // {
|
|
160
242
|
* // id: 1,
|
|
161
243
|
* // is_active: true, // 1 → true
|
|
162
|
-
* // created_at:
|
|
163
|
-
* // birth_date:
|
|
164
|
-
* // shift_time: '09:00:00'
|
|
244
|
+
* // created_at: '2024-01-15T10:30:00.000Z',
|
|
245
|
+
* // birth_date: '1990-05-20',
|
|
246
|
+
* // shift_time: '09:00:00'
|
|
165
247
|
* // }
|
|
166
248
|
*
|
|
167
249
|
* @example
|
|
@@ -206,7 +288,10 @@ export function normalizeDateValue (value, type) {
|
|
|
206
288
|
* 4. Normalizes dates using normalizeDateValue
|
|
207
289
|
* 5. Returns new object with normalized values
|
|
208
290
|
*/
|
|
209
|
-
export function normalizeAttributes (attributes, schemaStructure
|
|
291
|
+
export function normalizeAttributes (attributes, schemaStructure, {
|
|
292
|
+
resourceType,
|
|
293
|
+
source = 'database'
|
|
294
|
+
} = {}) {
|
|
210
295
|
if (!attributes || !schemaStructure) {
|
|
211
296
|
return attributes
|
|
212
297
|
}
|
|
@@ -232,7 +317,12 @@ export function normalizeAttributes (attributes, schemaStructure) {
|
|
|
232
317
|
|
|
233
318
|
// Normalize date/dateTime/time values
|
|
234
319
|
if (fieldDef.type === 'date' || fieldDef.type === 'dateTime' || fieldDef.type === 'time') {
|
|
235
|
-
normalized[fieldName] = normalizeDateValue(value, fieldDef.type
|
|
320
|
+
normalized[fieldName] = normalizeDateValue(value, fieldDef.type, {
|
|
321
|
+
temporalPrecision: fieldDef.temporalPrecision,
|
|
322
|
+
fieldName,
|
|
323
|
+
resourceType,
|
|
324
|
+
source
|
|
325
|
+
})
|
|
236
326
|
}
|
|
237
327
|
}
|
|
238
328
|
|
|
@@ -278,8 +368,8 @@ export function normalizeAttributes (attributes, schemaStructure) {
|
|
|
278
368
|
* // id: '1',
|
|
279
369
|
* // attributes: {
|
|
280
370
|
* // title: 'My Article',
|
|
281
|
-
* // is_published: true,
|
|
282
|
-
* // published_at:
|
|
371
|
+
* // is_published: true,
|
|
372
|
+
* // published_at: '2024-01-15T10:00:00.000Z'
|
|
283
373
|
* // }
|
|
284
374
|
* // },
|
|
285
375
|
* // included: [{
|
|
@@ -287,8 +377,8 @@ export function normalizeAttributes (attributes, schemaStructure) {
|
|
|
287
377
|
* // id: '10',
|
|
288
378
|
* // attributes: {
|
|
289
379
|
* // name: 'John',
|
|
290
|
-
* // is_admin: false,
|
|
291
|
-
* // last_login:
|
|
380
|
+
* // is_admin: false,
|
|
381
|
+
* // last_login: '2024-01-14T15:30:00.000Z'
|
|
292
382
|
* // }
|
|
293
383
|
* // }]
|
|
294
384
|
* // }
|
|
@@ -330,47 +420,45 @@ export function normalizeAttributes (attributes, schemaStructure) {
|
|
|
330
420
|
* 4. Processes included array the same way
|
|
331
421
|
* 5. Returns complete response with normalized values
|
|
332
422
|
*/
|
|
333
|
-
export function normalizeRecordAttributes (record, scopes) {
|
|
423
|
+
export function normalizeRecordAttributes (record, scopes, { source = 'database' } = {}) {
|
|
334
424
|
if (!record || !scopes) {
|
|
335
425
|
return record
|
|
336
426
|
}
|
|
337
427
|
|
|
428
|
+
const normalizeEntry = (entry) => {
|
|
429
|
+
const schemaInfo = scopes[entry?.type]?.vars?.schemaInfo
|
|
430
|
+
if (!schemaInfo || !entry?.attributes) return
|
|
431
|
+
|
|
432
|
+
entry.attributes = normalizeAttributes(
|
|
433
|
+
entry.attributes,
|
|
434
|
+
{
|
|
435
|
+
...(schemaInfo.schemaStructure || {}),
|
|
436
|
+
...(schemaInfo.computed || {})
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
resourceType: entry.type,
|
|
440
|
+
source
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
338
445
|
// Normalize main data records
|
|
339
446
|
if (record.data) {
|
|
340
447
|
if (Array.isArray(record.data)) {
|
|
341
448
|
// Handle array of records (query result)
|
|
342
449
|
for (const entry of record.data) {
|
|
343
|
-
|
|
344
|
-
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
345
|
-
entry.attributes = normalizeAttributes(
|
|
346
|
-
entry.attributes,
|
|
347
|
-
scope.vars.schemaInfo.schemaStructure
|
|
348
|
-
)
|
|
349
|
-
}
|
|
450
|
+
normalizeEntry(entry)
|
|
350
451
|
}
|
|
351
452
|
} else {
|
|
352
453
|
// Handle single record (get result)
|
|
353
|
-
|
|
354
|
-
const scope = scopes[entry.type]
|
|
355
|
-
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
356
|
-
entry.attributes = normalizeAttributes(
|
|
357
|
-
entry.attributes,
|
|
358
|
-
scope.vars.schemaInfo.schemaStructure
|
|
359
|
-
)
|
|
360
|
-
}
|
|
454
|
+
normalizeEntry(record.data)
|
|
361
455
|
}
|
|
362
456
|
}
|
|
363
457
|
|
|
364
458
|
// Normalize included records
|
|
365
459
|
if (record.included && Array.isArray(record.included)) {
|
|
366
460
|
for (const entry of record.included) {
|
|
367
|
-
|
|
368
|
-
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
369
|
-
entry.attributes = normalizeAttributes(
|
|
370
|
-
entry.attributes,
|
|
371
|
-
scope.vars.schemaInfo.schemaStructure
|
|
372
|
-
)
|
|
373
|
-
}
|
|
461
|
+
normalizeEntry(entry)
|
|
374
462
|
}
|
|
375
463
|
}
|
|
376
464
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildQuerySelection } from '../querying/knex-query-helpers-base.js'
|
|
2
2
|
import { createSelectTranslator } from '../storage/storage-adapter.js'
|
|
3
3
|
import { buildQueryFieldRuntimes } from './query-field-helpers.js'
|
|
4
|
+
import { RestApiFieldsetError } from '../../../../lib/rest-api-errors.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Checks if a field exists only in code, not in the database
|
|
@@ -211,7 +212,12 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
211
212
|
if (nonDatabaseFields.has(field)) return
|
|
212
213
|
|
|
213
214
|
const fieldDef = schemaStructure[field]
|
|
214
|
-
if (!fieldDef)
|
|
215
|
+
if (!fieldDef) {
|
|
216
|
+
throw new RestApiFieldsetError({
|
|
217
|
+
field,
|
|
218
|
+
resourceType: scopeName
|
|
219
|
+
})
|
|
220
|
+
}
|
|
215
221
|
|
|
216
222
|
if (fieldDef.belongsToPolymorphic) {
|
|
217
223
|
return
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
|
|
2
|
+
|
|
1
3
|
export const translateCanonicalAttributesForStorage = (attributes = {}, descriptor = {}) => {
|
|
2
4
|
const row = {}
|
|
3
5
|
|
|
@@ -8,7 +10,15 @@ export const translateCanonicalAttributesForStorage = (attributes = {}, descript
|
|
|
8
10
|
if (slot.slotType === 'belongsTo') {
|
|
9
11
|
row[slot.slot] = value == null ? null : String(value)
|
|
10
12
|
} else {
|
|
11
|
-
row[slot.slot] =
|
|
13
|
+
row[slot.slot] = normalizeValueForDatabaseStorage(
|
|
14
|
+
value,
|
|
15
|
+
descriptor.schema?.[fieldName]?.type,
|
|
16
|
+
{
|
|
17
|
+
temporalPrecision: descriptor.schema?.[fieldName]?.temporalPrecision,
|
|
18
|
+
fieldName,
|
|
19
|
+
resourceType: descriptor.resource
|
|
20
|
+
}
|
|
21
|
+
)
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
if (slot.slotType === 'belongsTo') {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getCanonicalResourceIdColumn,
|
|
10
10
|
translateCanonicalAttributesForStorage,
|
|
11
11
|
} from './canonical-storage-mapping.js'
|
|
12
|
-
import {
|
|
12
|
+
import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
|
|
13
13
|
|
|
14
14
|
const passthrough = (value) => value
|
|
15
15
|
const identityTranslate = (_field, value) => value
|
|
@@ -77,7 +77,9 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (['date', 'dateTime', 'time'].includes(type)) {
|
|
80
|
-
return
|
|
80
|
+
return normalizeValueForDatabaseStorage(value, type, {
|
|
81
|
+
temporalPrecision: definition?.temporalPrecision
|
|
82
|
+
})
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
return value
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
|
|
2
|
+
|
|
1
3
|
const EMPTY_STORAGE_INFO = Object.freeze({
|
|
2
4
|
idColumn: 'id',
|
|
3
5
|
storage: Object.freeze({
|
|
@@ -242,7 +244,11 @@ export const translateAttributesForStorage = (attributes, schemaInfo = {}, optio
|
|
|
242
244
|
context,
|
|
243
245
|
operation
|
|
244
246
|
})
|
|
245
|
-
: value
|
|
247
|
+
: normalizeValueForDatabaseStorage(value, fieldStorage?.definition?.type, {
|
|
248
|
+
temporalPrecision: fieldStorage?.definition?.temporalPrecision,
|
|
249
|
+
fieldName,
|
|
250
|
+
resourceType: context?.scopeName || schemaInfo.tableName
|
|
251
|
+
})
|
|
246
252
|
|
|
247
253
|
translated[columnName] = serializedValue
|
|
248
254
|
return translated
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getRequestContracts,
|
|
14
14
|
validateRequestContractOrThrow
|
|
15
15
|
} from '../lib/querying-writing/request-contracts.js'
|
|
16
|
+
import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
|
|
16
17
|
|
|
17
18
|
export { normalizeReturnRecordMode as normalizeReturnValue }
|
|
18
19
|
|
|
@@ -690,6 +691,10 @@ export async function handleRecordReturnAfterWrite ({
|
|
|
690
691
|
await runHooks('finish')
|
|
691
692
|
await runHooks(`finish${methodSpecificHookSuffix}`)
|
|
692
693
|
|
|
694
|
+
context.responseRecord = normalizeRecordAttributes(context.responseRecord, scopes, {
|
|
695
|
+
source: 'response'
|
|
696
|
+
})
|
|
697
|
+
|
|
693
698
|
// No transformation needed - GET already returns the correct format
|
|
694
699
|
// based on context.simplified
|
|
695
700
|
return context.responseRecord
|
|
@@ -181,6 +181,12 @@ export default async function getMethod ({
|
|
|
181
181
|
await runHooks('finish')
|
|
182
182
|
await runHooks('finishGet')
|
|
183
183
|
|
|
184
|
+
// Enrichers and finish hooks run after the database pass and may introduce
|
|
185
|
+
// native values. Reassert the public temporal contract at the return boundary.
|
|
186
|
+
context.record = normalizeRecordAttributes(context.record, scopes, {
|
|
187
|
+
source: 'response'
|
|
188
|
+
})
|
|
189
|
+
|
|
184
190
|
// Get schema info for transformation
|
|
185
191
|
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
186
192
|
|
|
@@ -154,6 +154,12 @@ export default async function queryMethod ({
|
|
|
154
154
|
await runHooks('finish')
|
|
155
155
|
await runHooks('finishQuery')
|
|
156
156
|
|
|
157
|
+
// Enrichers and finish hooks run after the database pass and may introduce
|
|
158
|
+
// native values. Reassert the public temporal contract at the return boundary.
|
|
159
|
+
context.record = normalizeRecordAttributes(context.record, scopes, {
|
|
160
|
+
source: 'response'
|
|
161
|
+
})
|
|
162
|
+
|
|
157
163
|
// Transform output if in simplified mode
|
|
158
164
|
if (context.simplified) {
|
|
159
165
|
// Convert JSON:API response back to simplified format
|