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
package/docs/API.md
CHANGED
|
@@ -2052,6 +2052,11 @@ const result = await api.resources.articles.query({
|
|
|
2052
2052
|
// GET /articles?fields[articles]=title,summary&fields[users]=name,avatar
|
|
2053
2053
|
```
|
|
2054
2054
|
|
|
2055
|
+
An unknown field raises `RestApiFieldsetError` with the stable code
|
|
2056
|
+
`REST_API_FIELDSET_INVALID` and HTTP status `400`. The same error is preserved
|
|
2057
|
+
for primary resources and included relationships, so transports can classify it
|
|
2058
|
+
without parsing error messages.
|
|
2059
|
+
|
|
2055
2060
|
### Including Related Resources
|
|
2056
2061
|
|
|
2057
2062
|
Load related resources in a single request:
|
|
@@ -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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HookedApiError } from 'hooked-api'
|
|
2
|
+
|
|
3
|
+
export function isRestApiError (error) {
|
|
4
|
+
return error instanceof HookedApiError
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add operational context to unexpected failures without erasing typed API
|
|
9
|
+
* errors that callers and transports need to classify.
|
|
10
|
+
*/
|
|
11
|
+
export function wrapUnexpectedError (error, { message, context } = {}) {
|
|
12
|
+
if (isRestApiError(error)) {
|
|
13
|
+
return error
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const wrappedError = new Error(`${message}: ${error.message}`, {
|
|
17
|
+
cause: error
|
|
18
|
+
})
|
|
19
|
+
wrappedError.context = context
|
|
20
|
+
return wrappedError
|
|
21
|
+
}
|
package/lib/rest-api-errors.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { HookedApiError } from 'hooked-api'
|
|
2
2
|
|
|
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'
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* REST API specific error classes that can be mapped to HTTP status codes
|
|
5
8
|
* by the HTTP plugin or other protocol plugins
|
|
@@ -90,3 +93,70 @@ export class RestApiPayloadError extends HookedApiError {
|
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Invalid JSON:API sparse fieldset.
|
|
99
|
+
*
|
|
100
|
+
* The structured code lets transports distinguish invalid client input from
|
|
101
|
+
* query or include failures without parsing the human-readable message.
|
|
102
|
+
*/
|
|
103
|
+
export class RestApiFieldsetError extends HookedApiError {
|
|
104
|
+
constructor ({ field, resourceType } = {}) {
|
|
105
|
+
super(
|
|
106
|
+
`Unknown sparse field '${field}' requested for '${resourceType}'`,
|
|
107
|
+
REST_API_FIELDSET_ERROR_CODE
|
|
108
|
+
)
|
|
109
|
+
this.type = 'rest_api_fieldset'
|
|
110
|
+
this.statusCode = 400
|
|
111
|
+
this.details = {
|
|
112
|
+
field,
|
|
113
|
+
resourceType
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
toJSON () {
|
|
118
|
+
return {
|
|
119
|
+
name: this.name,
|
|
120
|
+
message: this.message,
|
|
121
|
+
code: this.code,
|
|
122
|
+
type: this.type,
|
|
123
|
+
statusCode: this.statusCode,
|
|
124
|
+
details: this.details,
|
|
125
|
+
stack: this.stack
|
|
126
|
+
}
|
|
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,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REST_API_FIELDSET_ERROR_CODE,
|
|
3
|
+
REST_API_TEMPORAL_DATA_ERROR_CODE
|
|
4
|
+
} from '../../../../lib/rest-api-errors.js'
|
|
5
|
+
|
|
1
6
|
const JSON_API_WRITE_CONTENT_TYPES = [
|
|
2
7
|
'application/vnd.api+json',
|
|
3
8
|
'application/json'
|
|
@@ -63,6 +68,22 @@ export function mapRestApiErrorToHttp (error) {
|
|
|
63
68
|
detail: issue.message,
|
|
64
69
|
...(issue.instancePath ? { source: { pointer: issue.instancePath } } : {})
|
|
65
70
|
}))
|
|
71
|
+
} else if (error.code === REST_API_FIELDSET_ERROR_CODE) {
|
|
72
|
+
status = 400
|
|
73
|
+
errors = [{
|
|
74
|
+
status: '400',
|
|
75
|
+
code: error.code,
|
|
76
|
+
title: 'Invalid Sparse Fieldset',
|
|
77
|
+
detail: error.message
|
|
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
|
+
}]
|
|
66
87
|
} else if (error.code === 'REST_API_VALIDATION') {
|
|
67
88
|
status = 422
|
|
68
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)}`)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildIncludedResources } from './knex-relationship-includes.js'
|
|
2
|
+
import { isRestApiError, wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Processes the ?include= parameter to load related resources efficiently
|
|
@@ -143,6 +144,8 @@ export const processIncludes = async (scope, records, deps) => {
|
|
|
143
144
|
|
|
144
145
|
return includeResult.included
|
|
145
146
|
} catch (error) {
|
|
147
|
+
if (isRestApiError(error)) throw error
|
|
148
|
+
|
|
146
149
|
const { log, context } = deps || {}
|
|
147
150
|
|
|
148
151
|
if (log) {
|
|
@@ -157,10 +160,13 @@ export const processIncludes = async (scope, records, deps) => {
|
|
|
157
160
|
console.error('[PROCESS-INCLUDES] Error processing includes:', error)
|
|
158
161
|
}
|
|
159
162
|
|
|
160
|
-
|
|
161
|
-
`Failed to process includes${context?.scopeName ? ` for scope '${context.scopeName}'` : ''}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
throw wrapUnexpectedError(error, {
|
|
164
|
+
message: `Failed to process includes${context?.scopeName ? ` for scope '${context.scopeName}'` : ''}`,
|
|
165
|
+
context: {
|
|
166
|
+
scopeName: context?.scopeName,
|
|
167
|
+
includeParam: context?.queryParams?.include,
|
|
168
|
+
recordCount: records?.length || 0
|
|
169
|
+
}
|
|
170
|
+
})
|
|
165
171
|
}
|
|
166
172
|
}
|