json-rest-api 1.0.27 → 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/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 +1 -0
- package/lib/rest-api-errors.js +35 -0
- package/package.json +2 -2
- package/plugins/core/connectors/lib/transport-http-helpers.js +12 -1
- 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-relationship-includes.js +18 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +231 -143
- 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 +21 -0
- package/tests/db-table-operations.test.js +13 -1
- package/tests/fixtures/api-configs.js +83 -3
- 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
|
@@ -13,7 +13,7 @@ await api.addResource('posts', {
|
|
|
13
13
|
content: { type: 'string', required: true },
|
|
14
14
|
view_count: { type: 'number', default: 0, indexed: true },
|
|
15
15
|
published_at: { type: 'dateTime', indexed: true },
|
|
16
|
-
created_at: { type: 'dateTime', defaultTo: Date.
|
|
16
|
+
created_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString(), indexed: true }
|
|
17
17
|
},
|
|
18
18
|
relationships: {
|
|
19
19
|
comments: {
|
|
@@ -54,7 +54,7 @@ await api.addResource('comments', {
|
|
|
54
54
|
schema: {
|
|
55
55
|
content: { type: 'string', required: true },
|
|
56
56
|
author_name: { type: 'string', required: true, max: 100 },
|
|
57
|
-
created_at: { type: 'dateTime', defaultTo: Date.
|
|
57
|
+
created_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString(), indexed: true },
|
|
58
58
|
likes: { type: 'number', default: 0, indexed: true },
|
|
59
59
|
post_id: { type: 'id', belongsTo: 'posts', as: 'post', required: true }
|
|
60
60
|
},
|
|
@@ -970,7 +970,7 @@ await api.addResource('posts', {
|
|
|
970
970
|
title: { type: 'string', required: true },
|
|
971
971
|
content: { type: 'string', required: true },
|
|
972
972
|
author_id: { type: 'number', belongsTo: 'authors', as: 'author' },
|
|
973
|
-
published_at: { type: 'dateTime',
|
|
973
|
+
published_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() }
|
|
974
974
|
},
|
|
975
975
|
tableName: 'posts'
|
|
976
976
|
});
|
|
@@ -1064,7 +1064,7 @@ await api.addResource('posts', {
|
|
|
1064
1064
|
getter: (value) => value?.trim()
|
|
1065
1065
|
},
|
|
1066
1066
|
author_id: { type: 'number', belongsTo: 'authors', as: 'author' },
|
|
1067
|
-
published_at: { type: 'dateTime',
|
|
1067
|
+
published_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() }
|
|
1068
1068
|
},
|
|
1069
1069
|
tableName: 'posts'
|
|
1070
1070
|
});
|
|
@@ -6,7 +6,7 @@ This document explains how the JSON REST API handles date and time values throug
|
|
|
6
6
|
|
|
7
7
|
## Supported Date/Time Types
|
|
8
8
|
|
|
9
|
-
The API supports three temporal
|
|
9
|
+
The API supports three string temporal types and two numeric epoch types in schemas:
|
|
10
10
|
|
|
11
11
|
### 1. `date`
|
|
12
12
|
- **Format**: `YYYY-MM-DD`
|
|
@@ -16,8 +16,8 @@ The API supports three temporal data types in schemas:
|
|
|
16
16
|
- **JSON Output**: ISO 8601 date string
|
|
17
17
|
|
|
18
18
|
### 2. `dateTime`
|
|
19
|
-
- **Format**: `
|
|
20
|
-
- **Example Input**: `2024-01-
|
|
19
|
+
- **Format**: RFC 3339 with seconds and an explicit `Z` or `±HH:MM` offset
|
|
20
|
+
- **Example Input**: `2024-01-15T14:30:00Z`
|
|
21
21
|
- **Example Output**: `2024-01-15T14:30:00.000Z`
|
|
22
22
|
- **Usage**: Timestamps, created/updated times, or any date with time
|
|
23
23
|
- **Database Storage**: DATETIME (MySQL) or TIMESTAMP (PostgreSQL)
|
|
@@ -30,6 +30,13 @@ The API supports three temporal data types in schemas:
|
|
|
30
30
|
- **Database Storage**: TIME column type
|
|
31
31
|
- **JSON Output**: ISO 8601 time string
|
|
32
32
|
|
|
33
|
+
### 4. `epochMilliseconds` and `epochSeconds`
|
|
34
|
+
- **Format**: An integer number or canonical base-10 integer string
|
|
35
|
+
- **Example**: `1767323045000` (`epochMilliseconds`) or `1767323045` (`epochSeconds`)
|
|
36
|
+
- **Usage**: Systems whose temporal contract is explicitly a Unix epoch value
|
|
37
|
+
- **Database Storage**: BIGINT for table-backed resources
|
|
38
|
+
- **JSON Output**: Integer number in the same unit declared by the schema type
|
|
39
|
+
|
|
33
40
|
## Schema Definition
|
|
34
41
|
|
|
35
42
|
Define date/time fields in your schema like this:
|
|
@@ -37,12 +44,18 @@ Define date/time fields in your schema like this:
|
|
|
37
44
|
```javascript
|
|
38
45
|
const articleSchema = {
|
|
39
46
|
publishedDate: { type: 'date', required: true },
|
|
40
|
-
createdAt: { type: 'dateTime', defaultTo:
|
|
41
|
-
updatedAt: { type: 'dateTime', defaultTo:
|
|
47
|
+
createdAt: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() },
|
|
48
|
+
updatedAt: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() },
|
|
42
49
|
dailyPostTime: { type: 'time', nullable: true }
|
|
43
50
|
};
|
|
44
51
|
```
|
|
45
52
|
|
|
53
|
+
`Date.prototype.toISOString()` always emits three fractional-second digits, so
|
|
54
|
+
use it directly with unspecified precision or `temporalPrecision: 3` (or
|
|
55
|
+
higher). A lower precision requires a producer that returns no more than the
|
|
56
|
+
configured number of digits. Validation rejects excess input precision rather
|
|
57
|
+
than silently truncating it.
|
|
58
|
+
|
|
46
59
|
## Input Validation
|
|
47
60
|
|
|
48
61
|
### On Write Operations (POST/PUT/PATCH)
|
|
@@ -64,21 +77,14 @@ The API validates and normalizes date/time inputs:
|
|
|
64
77
|
```
|
|
65
78
|
|
|
66
79
|
**Accepted Input Formats:**
|
|
67
|
-
- **date**:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- ISO 8601 strings (`2024-01-15T14:30:00Z`) - recommended
|
|
73
|
-
- `YYYY-MM-DD HH:MM:SS` (assumed UTC)
|
|
74
|
-
- JavaScript Date parseable strings
|
|
75
|
-
- Unix timestamps (as numbers)
|
|
76
|
-
- **time**:
|
|
77
|
-
- `HH:MM:SS` or `HH:MM`
|
|
78
|
-
- Extracted from datetime strings
|
|
80
|
+
- **date**: a real calendar date in `YYYY-MM-DD` form
|
|
81
|
+
- **dateTime**: an RFC 3339 string with seconds and an explicit timezone, such as `2024-01-15T14:30:00Z` or `2024-01-15T22:30:00+08:00`
|
|
82
|
+
- **time**: an offset-free `HH:MM[:SS[.fraction]]` string
|
|
83
|
+
|
|
84
|
+
JavaScript `Date` objects, SQL datetime strings, Unix timestamps, locale text, and other values accepted by `Date.parse()` are not valid JSON temporal inputs. Use `epochMilliseconds` or `epochSeconds` schema fields for Unix timestamps.
|
|
79
85
|
|
|
80
86
|
**Storage Format:**
|
|
81
|
-
|
|
87
|
+
Schema validation preserves valid JSON temporal strings exactly. The built-in storage adapters convert `date` and `dateTime` strings to database-native `Date` values at the database boundary, while `time` remains a string. A custom field-level `storage.serialize` function receives the validated string and owns any custom database conversion.
|
|
82
88
|
|
|
83
89
|
## Output Normalization
|
|
84
90
|
|
|
@@ -105,9 +111,15 @@ All date/time values are normalized when returned from the API:
|
|
|
105
111
|
### Key Normalization Behaviors:
|
|
106
112
|
|
|
107
113
|
1. **Boolean Normalization**: Database values of `1`/`0` are converted to `true`/`false`
|
|
108
|
-
2. **
|
|
114
|
+
2. **JSON Temporal Values**: `date`, `dateTime`, and `time` attributes are returned as strings that match the same public schema contract accepted on input
|
|
109
115
|
3. **UTC Assumption**: MySQL DATETIME values (which lack timezone info) are assumed to be UTC
|
|
110
116
|
|
|
117
|
+
Normalization runs once after the database read and again at the final response
|
|
118
|
+
boundary. The final pass covers getters, computed fields, and finishing hooks
|
|
119
|
+
that produce native `Date` values. A malformed non-null temporal value is not
|
|
120
|
+
reported as `null`; it fails with `REST_API_TEMPORAL_DATA_INVALID` and HTTP 500
|
|
121
|
+
so stored-data or enrichment defects remain visible.
|
|
122
|
+
|
|
111
123
|
## Database-Specific Handling
|
|
112
124
|
|
|
113
125
|
### MySQL
|
|
@@ -164,7 +176,7 @@ If migrating from a system that stores dates differently:
|
|
|
164
176
|
|
|
165
177
|
1. **Local Time Storage**: Convert all dates to UTC before importing
|
|
166
178
|
2. **String Storage**: Ensure strings match expected formats
|
|
167
|
-
3. **Numeric Timestamps**: Use `
|
|
179
|
+
3. **Numeric Timestamps**: Use `epochMilliseconds` or `epochSeconds`, matching the producer's actual unit
|
|
168
180
|
|
|
169
181
|
### Database Configuration
|
|
170
182
|
|
|
@@ -206,23 +218,24 @@ For optimal date handling, configure your database connection:
|
|
|
206
218
|
|
|
207
219
|
### Issue 3: Time Values Need Date Context
|
|
208
220
|
**Symptom**: Can't perform date arithmetic on time-only values
|
|
209
|
-
**Cause**: Time values
|
|
210
|
-
**Solution**:
|
|
221
|
+
**Cause**: Time values intentionally have no date or timezone context
|
|
222
|
+
**Solution**: Keep recurring wall-clock values as `time`; use `dateTime` when an instant is required
|
|
211
223
|
|
|
212
224
|
## Technical Implementation Details
|
|
213
225
|
|
|
214
226
|
The date/time handling is implemented in two key areas:
|
|
215
227
|
|
|
216
228
|
1. **Input Validation** (`json-rest-schema`):
|
|
217
|
-
- Validates
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
- Returns Date objects for storage (Knex handles DB-specific formatting)
|
|
229
|
+
- Validates strict JSON temporal strings on write operations
|
|
230
|
+
- Preserves accepted strings exactly
|
|
231
|
+
- Rejects JavaScript `Date` objects and permissive legacy date formats
|
|
221
232
|
|
|
222
|
-
2. **Output Normalization** (`database-value-normalizers.js`):
|
|
233
|
+
2. **Storage and Output Normalization** (`storage` adapters and `database-value-normalizers.js`):
|
|
234
|
+
- Converts validated `date` and `dateTime` strings at the database boundary
|
|
235
|
+
- Uses the same database-native representation for writes and query comparisons
|
|
223
236
|
- Handles database-specific quirks (MySQL timezone issues)
|
|
224
|
-
-
|
|
237
|
+
- Parses database date values before emitting schema-valid JSON strings
|
|
225
238
|
- Fixes MySQL datetime strings by assuming UTC
|
|
226
239
|
- Maintains consistency across different database engines
|
|
227
240
|
|
|
228
|
-
This
|
|
241
|
+
This separation keeps the public JSON contract strict and serializable while still giving database drivers native values at the storage boundary.
|
package/index.js
CHANGED
package/lib/rest-api-errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HookedApiError } from 'hooked-api'
|
|
2
2
|
|
|
3
3
|
export const REST_API_FIELDSET_ERROR_CODE = 'REST_API_FIELDSET_INVALID'
|
|
4
|
+
export const REST_API_TEMPORAL_DATA_ERROR_CODE = 'REST_API_TEMPORAL_DATA_INVALID'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* REST API specific error classes that can be mapped to HTTP status codes
|
|
@@ -125,3 +126,37 @@ export class RestApiFieldsetError extends HookedApiError {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A non-null temporal value produced by storage or response enrichment does
|
|
132
|
+
* not satisfy the resource's public temporal contract.
|
|
133
|
+
*/
|
|
134
|
+
export class RestApiTemporalDataError extends HookedApiError {
|
|
135
|
+
constructor ({ field, resourceType, fieldType, source = 'database' } = {}) {
|
|
136
|
+
const fieldPath = [resourceType, field].filter(Boolean).join('.') || 'unknown field'
|
|
137
|
+
super(
|
|
138
|
+
`Invalid ${source} value for ${fieldType || 'temporal'} field '${fieldPath}'.`,
|
|
139
|
+
REST_API_TEMPORAL_DATA_ERROR_CODE
|
|
140
|
+
)
|
|
141
|
+
this.type = 'rest_api_temporal_data'
|
|
142
|
+
this.statusCode = 500
|
|
143
|
+
this.details = {
|
|
144
|
+
field,
|
|
145
|
+
resourceType,
|
|
146
|
+
fieldType,
|
|
147
|
+
source
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
toJSON () {
|
|
152
|
+
return {
|
|
153
|
+
name: this.name,
|
|
154
|
+
message: this.message,
|
|
155
|
+
code: this.code,
|
|
156
|
+
type: this.type,
|
|
157
|
+
statusCode: this.statusCode,
|
|
158
|
+
details: this.details,
|
|
159
|
+
stack: this.stack
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-rest-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"description": "REST API plugin for hooked-api with JSON:API compliance",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"hooked-api": "^1.0.24",
|
|
32
|
-
"json-rest-schema": "^1.0.
|
|
32
|
+
"json-rest-schema": "^1.0.17",
|
|
33
33
|
"semver": "^7.7.2"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
REST_API_FIELDSET_ERROR_CODE,
|
|
3
|
+
REST_API_TEMPORAL_DATA_ERROR_CODE
|
|
4
|
+
} from '../../../../lib/rest-api-errors.js'
|
|
2
5
|
|
|
3
6
|
const JSON_API_WRITE_CONTENT_TYPES = [
|
|
4
7
|
'application/vnd.api+json',
|
|
@@ -73,6 +76,14 @@ export function mapRestApiErrorToHttp (error) {
|
|
|
73
76
|
title: 'Invalid Sparse Fieldset',
|
|
74
77
|
detail: error.message
|
|
75
78
|
}]
|
|
79
|
+
} else if (error.code === REST_API_TEMPORAL_DATA_ERROR_CODE) {
|
|
80
|
+
status = 500
|
|
81
|
+
errors = [{
|
|
82
|
+
status: '500',
|
|
83
|
+
code: error.code,
|
|
84
|
+
title: 'Invalid Temporal Data',
|
|
85
|
+
detail: error.message
|
|
86
|
+
}]
|
|
76
87
|
} else if (error.code === 'REST_API_VALIDATION') {
|
|
77
88
|
status = 422
|
|
78
89
|
if (error.details?.violations?.length) {
|
|
@@ -219,8 +219,9 @@ export const TYPE_TO_POOL = new Map([
|
|
|
219
219
|
['date', 'date'],
|
|
220
220
|
['datetime', 'date'],
|
|
221
221
|
['dateTime', 'date'],
|
|
222
|
-
['timestamp', 'date'],
|
|
223
222
|
['time', 'date'],
|
|
223
|
+
['epochMilliseconds', 'number'],
|
|
224
|
+
['epochSeconds', 'number'],
|
|
224
225
|
['json', 'json'],
|
|
225
226
|
['object', 'json'],
|
|
226
227
|
])
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RestApiValidationError } from '../../../../../lib/rest-api-errors.js'
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeValueForDatabaseStorage } from '../../querying-writing/database-value-normalizers.js'
|
|
3
3
|
|
|
4
4
|
export const normalizeId = (value) => (value === null || value === undefined ? null : String(value))
|
|
5
5
|
|
|
@@ -93,7 +93,9 @@ export const coerceValueForDefinition = (value, definition, { isRelationship } =
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
if (['date', 'dateTime', 'time'].includes(type)) {
|
|
96
|
-
return
|
|
96
|
+
return normalizeValueForDatabaseStorage(value, type, {
|
|
97
|
+
temporalPrecision: definition?.temporalPrecision
|
|
98
|
+
})
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
return value
|
|
@@ -300,7 +300,8 @@ function normalizeColumnShape (definition = {}) {
|
|
|
300
300
|
return 'datetime'
|
|
301
301
|
case 'time':
|
|
302
302
|
return 'time'
|
|
303
|
-
case '
|
|
303
|
+
case 'epochMilliseconds':
|
|
304
|
+
case 'epochSeconds':
|
|
304
305
|
return 'integer'
|
|
305
306
|
case 'array':
|
|
306
307
|
case 'object':
|
|
@@ -344,7 +345,8 @@ function normalizeDesiredTypeKind (definition = {}) {
|
|
|
344
345
|
case 'blob':
|
|
345
346
|
case 'file':
|
|
346
347
|
return 'binary'
|
|
347
|
-
case '
|
|
348
|
+
case 'epochMilliseconds':
|
|
349
|
+
case 'epochSeconds':
|
|
348
350
|
return 'integer'
|
|
349
351
|
case 'string':
|
|
350
352
|
case 'none':
|
|
@@ -361,6 +363,10 @@ function normalizeComparableDefault (value) {
|
|
|
361
363
|
return String(value)
|
|
362
364
|
}
|
|
363
365
|
|
|
366
|
+
const hasStaticDefault = (definition = {}) => (
|
|
367
|
+
definition.defaultTo !== undefined && typeof definition.defaultTo !== 'function'
|
|
368
|
+
)
|
|
369
|
+
|
|
364
370
|
function normalizeDesiredColumn (tableContext, fieldName, definition, options = {}) {
|
|
365
371
|
const nullable = definition.nullable === true
|
|
366
372
|
? true
|
|
@@ -375,8 +381,10 @@ function normalizeDesiredColumn (tableContext, fieldName, definition, options =
|
|
|
375
381
|
typeKind: normalizeDesiredTypeKind(definition),
|
|
376
382
|
nullable,
|
|
377
383
|
required: definition.required === true,
|
|
378
|
-
hasDefault: definition
|
|
379
|
-
defaultValue:
|
|
384
|
+
hasDefault: hasStaticDefault(definition),
|
|
385
|
+
defaultValue: hasStaticDefault(definition)
|
|
386
|
+
? normalizeComparableDefault(definition.defaultTo)
|
|
387
|
+
: undefined,
|
|
380
388
|
unsigned: definition.type === 'id' ? definition.unsigned !== false : definition.unsigned === true,
|
|
381
389
|
maxLength: definition.maxLength ?? null,
|
|
382
390
|
numericPrecision: definition.precision ?? null,
|
|
@@ -499,8 +507,9 @@ function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
|
499
507
|
? table.time(columnName, { precision: temporalPrecision })
|
|
500
508
|
: table.time(columnName)
|
|
501
509
|
|
|
502
|
-
case '
|
|
503
|
-
|
|
510
|
+
case 'epochMilliseconds':
|
|
511
|
+
case 'epochSeconds':
|
|
512
|
+
return table.bigInteger(columnName)
|
|
504
513
|
|
|
505
514
|
case 'array':
|
|
506
515
|
case 'object':
|
|
@@ -524,7 +533,7 @@ function applyColumnConstraints (column, definition) {
|
|
|
524
533
|
column.notNullable()
|
|
525
534
|
}
|
|
526
535
|
|
|
527
|
-
if (definition
|
|
536
|
+
if (hasStaticDefault(definition)) {
|
|
528
537
|
column.defaultTo(definition.defaultTo)
|
|
529
538
|
}
|
|
530
539
|
|
|
@@ -631,8 +640,9 @@ function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
|
631
640
|
}
|
|
632
641
|
break
|
|
633
642
|
|
|
634
|
-
case '
|
|
635
|
-
|
|
643
|
+
case 'epochMilliseconds':
|
|
644
|
+
case 'epochSeconds':
|
|
645
|
+
line = `table.bigInteger(${quoteJsString(columnName)})`
|
|
636
646
|
break
|
|
637
647
|
|
|
638
648
|
case 'array':
|
|
@@ -658,7 +668,7 @@ function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
|
658
668
|
line += '.notNullable()'
|
|
659
669
|
}
|
|
660
670
|
|
|
661
|
-
if (definition
|
|
671
|
+
if (hasStaticDefault(definition)) {
|
|
662
672
|
line += `.defaultTo(${formatCodeLiteral(definition.defaultTo)})`
|
|
663
673
|
}
|
|
664
674
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getFieldValue } from '../storage/storage-mapping.js'
|
|
2
2
|
import { serializeJsonApiQuery } from '../querying-writing/connectors-query-parser.js'
|
|
3
|
+
import { normalizeDateValue } from '../querying-writing/database-value-normalizers.js'
|
|
3
4
|
|
|
4
5
|
const buildJsonApiLink = (baseUrl, queryParams, page) => {
|
|
5
6
|
const queryString = serializeJsonApiQuery(queryParams, { page })
|
|
@@ -236,9 +237,18 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
|
|
|
236
237
|
export const createCursor = (record, sortFields = ['id'], { schemaInfo = null } = {}) => {
|
|
237
238
|
const parts = []
|
|
238
239
|
sortFields.forEach(field => {
|
|
239
|
-
|
|
240
|
+
let value = schemaInfo
|
|
240
241
|
? getFieldValue(record, schemaInfo, field)
|
|
241
242
|
: record[field]
|
|
243
|
+
const definition = schemaInfo?.schemaStructure?.[field]
|
|
244
|
+
if (['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
|
|
245
|
+
value = normalizeDateValue(value, definition.type, {
|
|
246
|
+
temporalPrecision: definition.temporalPrecision,
|
|
247
|
+
fieldName: field,
|
|
248
|
+
resourceType: schemaInfo?.scopeName || schemaInfo?.tableName,
|
|
249
|
+
source: 'cursor'
|
|
250
|
+
})
|
|
251
|
+
}
|
|
242
252
|
if (value !== undefined) {
|
|
243
253
|
const stringValue = value instanceof Date ? value.toISOString() : String(value)
|
|
244
254
|
parts.push(`${field}:${encodeURIComponent(stringValue)}`)
|
|
@@ -90,9 +90,20 @@ import {
|
|
|
90
90
|
getIdColumn as getStorageIdColumn,
|
|
91
91
|
getStorageColumn as getMappedStorageColumn,
|
|
92
92
|
} from '../storage/storage-mapping.js'
|
|
93
|
+
import { createStorageAdapter } from '../storage/storage-adapter.js'
|
|
93
94
|
|
|
94
95
|
const getScopeStorageAdapter = (scopes, scopeName) => scopes[scopeName]?.vars?.storageAdapter || null
|
|
95
96
|
|
|
97
|
+
const resolveScopeStorageAdapter = (scopes, scopeName, knex) => {
|
|
98
|
+
const configuredAdapter = getScopeStorageAdapter(scopes, scopeName)
|
|
99
|
+
if (configuredAdapter) return configuredAdapter
|
|
100
|
+
|
|
101
|
+
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
102
|
+
if (!schemaInfo) return null
|
|
103
|
+
|
|
104
|
+
return createStorageAdapter({ knex, schemaInfo })
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
const getIdColumnForScope = (scopes, scopeName) => {
|
|
97
108
|
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
98
109
|
if (adapter?.getIdColumn) {
|
|
@@ -162,7 +173,7 @@ const applyScopeFiltersToIncludeQuery = async ({
|
|
|
162
173
|
return { query }
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
const storageAdapter =
|
|
176
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, scopeName, db)
|
|
166
177
|
|
|
167
178
|
const queryState = await scopeObject.applyQueryFilters({
|
|
168
179
|
query,
|
|
@@ -503,7 +514,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
503
514
|
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
504
515
|
// Build field selection for sparse fieldsets
|
|
505
516
|
const targetScopeObject = scopes[targetScope]
|
|
506
|
-
const storageAdapter =
|
|
517
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
507
518
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
508
519
|
? await buildFieldSelection(targetScopeObject, {
|
|
509
520
|
context: {
|
|
@@ -745,7 +756,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
745
756
|
|
|
746
757
|
// Step 3: Build field selection for sparse fieldsets
|
|
747
758
|
const targetScopeObject = scopes[targetScope]
|
|
748
|
-
const storageAdapter =
|
|
759
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
749
760
|
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
750
761
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
751
762
|
? await buildFieldSelection(targetScopeObject, {
|
|
@@ -974,7 +985,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
974
985
|
|
|
975
986
|
// Build field selection for sparse fieldsets
|
|
976
987
|
const targetScopeObject = scopes[targetScope]
|
|
977
|
-
const storageAdapter =
|
|
988
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
978
989
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
979
990
|
? await buildFieldSelection(targetScopeObject, {
|
|
980
991
|
context: {
|
|
@@ -1224,7 +1235,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1224
1235
|
|
|
1225
1236
|
// Build query for hasOne - expects single result per parent
|
|
1226
1237
|
const targetScopeObject = scopes[targetScope]
|
|
1227
|
-
const storageAdapter =
|
|
1238
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
1228
1239
|
|
|
1229
1240
|
// Build field selection for sparse fieldsets
|
|
1230
1241
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
@@ -1396,7 +1407,7 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1396
1407
|
|
|
1397
1408
|
// Build field selection for sparse fieldsets
|
|
1398
1409
|
const targetScopeObject = scopes[targetType]
|
|
1399
|
-
const storageAdapter =
|
|
1410
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetType, knex)
|
|
1400
1411
|
const fieldSelectionInfo = fields?.[targetType]
|
|
1401
1412
|
? await buildFieldSelection(targetScopeObject, {
|
|
1402
1413
|
context: {
|
|
@@ -1624,7 +1635,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1624
1635
|
|
|
1625
1636
|
// Build field selection for sparse fieldsets
|
|
1626
1637
|
const targetScopeObject = scopes[targetScope]
|
|
1627
|
-
const storageAdapter =
|
|
1638
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
1628
1639
|
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
1629
1640
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
1630
1641
|
? await buildFieldSelection(targetScopeObject, {
|