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
|
@@ -334,6 +334,68 @@ export async function createCamelCaseBelongsToApi (knex, pluginOptions = {}) {
|
|
|
334
334
|
return api
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
export async function createRelationshipIncludeStorageAdapterApi (knex) {
|
|
338
|
+
const api = new Api({
|
|
339
|
+
name: 'relationship-include-storage-adapter-test',
|
|
340
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
await api.use(RestApiPlugin, {
|
|
344
|
+
simplifiedApi: false,
|
|
345
|
+
simplifiedTransport: false
|
|
346
|
+
})
|
|
347
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
348
|
+
|
|
349
|
+
await knex.schema.createTable('uninitialized_pets', table => {
|
|
350
|
+
table.increments('id').primary()
|
|
351
|
+
table.string('first_name').notNullable()
|
|
352
|
+
table.string('last_name').notNullable()
|
|
353
|
+
})
|
|
354
|
+
await knex.schema.createTable('uninitialized_bookings', table => {
|
|
355
|
+
table.increments('id').primary()
|
|
356
|
+
table.string('reference').notNullable()
|
|
357
|
+
table.integer('pet_id').notNullable()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
await api.addResource('pets', {
|
|
361
|
+
schema: {
|
|
362
|
+
id: { type: 'id' },
|
|
363
|
+
firstName: { type: 'string', required: true },
|
|
364
|
+
lastName: { type: 'string', required: true }
|
|
365
|
+
},
|
|
366
|
+
tableName: 'uninitialized_pets'
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
await api.addResource('bookings', {
|
|
370
|
+
schema: {
|
|
371
|
+
id: { type: 'id' },
|
|
372
|
+
reference: { type: 'string', required: true },
|
|
373
|
+
petId: {
|
|
374
|
+
type: 'id',
|
|
375
|
+
required: true,
|
|
376
|
+
belongsTo: 'pets',
|
|
377
|
+
as: 'pet'
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
tableName: 'uninitialized_bookings'
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
return api
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function seedRelationshipIncludeStorageAdapterApi (knex) {
|
|
387
|
+
await knex('uninitialized_pets').insert({
|
|
388
|
+
id: 7,
|
|
389
|
+
first_name: 'Coco',
|
|
390
|
+
last_name: 'Spaniel'
|
|
391
|
+
})
|
|
392
|
+
await knex('uninitialized_bookings').insert({
|
|
393
|
+
id: 41,
|
|
394
|
+
reference: 'BOOK-41',
|
|
395
|
+
pet_id: 7
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
337
399
|
/**
|
|
338
400
|
* Creates a small API focused on resource ID normalization behavior.
|
|
339
401
|
*/
|
|
@@ -641,7 +703,7 @@ export async function createExtendedApi (knex) {
|
|
|
641
703
|
title: { type: 'string', max: 200 },
|
|
642
704
|
content: { type: 'string', required: true, max: 5000 },
|
|
643
705
|
reviewer_name: { type: 'string', required: true, max: 100 },
|
|
644
|
-
review_date: { type: 'dateTime',
|
|
706
|
+
review_date: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() },
|
|
645
707
|
helpful_count: { type: 'number', default: 0 },
|
|
646
708
|
reviewable_type: { type: 'string', required: true },
|
|
647
709
|
reviewable_id: { type: 'number', required: true }
|
|
@@ -976,6 +1038,18 @@ export async function createComputedFieldsApi (knex, pluginOptions = {}) {
|
|
|
976
1038
|
compute: ({ attributes }) => {
|
|
977
1039
|
return Number((attributes.price - attributes.cost).toFixed(2))
|
|
978
1040
|
}
|
|
1041
|
+
},
|
|
1042
|
+
calculated_at: {
|
|
1043
|
+
type: 'dateTime',
|
|
1044
|
+
temporalPrecision: 0,
|
|
1045
|
+
computed: true,
|
|
1046
|
+
compute: () => new Date('2026-08-25T23:45:01.987Z')
|
|
1047
|
+
},
|
|
1048
|
+
calculated_time: {
|
|
1049
|
+
type: 'time',
|
|
1050
|
+
temporalPrecision: 3,
|
|
1051
|
+
computed: true,
|
|
1052
|
+
compute: () => new Date('2026-08-25T23:45:01.987Z')
|
|
979
1053
|
}
|
|
980
1054
|
},
|
|
981
1055
|
relationships: {
|
|
@@ -1016,6 +1090,12 @@ export async function createComputedFieldsApi (knex, pluginOptions = {}) {
|
|
|
1016
1090
|
const helpfulnessScore = (attributes.helpful_votes / attributes.total_votes) * 100
|
|
1017
1091
|
return helpfulnessScore > 70 && attributes.spam_score < 0.5
|
|
1018
1092
|
}
|
|
1093
|
+
},
|
|
1094
|
+
calculated_at: {
|
|
1095
|
+
type: 'dateTime',
|
|
1096
|
+
temporalPrecision: 0,
|
|
1097
|
+
computed: true,
|
|
1098
|
+
compute: () => new Date('2026-08-25T23:46:02.654Z')
|
|
1019
1099
|
}
|
|
1020
1100
|
},
|
|
1021
1101
|
tableName: 'test_reviews'
|
|
@@ -2090,7 +2170,7 @@ export async function createCustomIdPropertyApi (knex, pluginOptions = {}) {
|
|
|
2090
2170
|
title: { type: 'string', max: 200 },
|
|
2091
2171
|
content: { type: 'string', required: true, max: 5000 },
|
|
2092
2172
|
reviewer_name: { type: 'string', required: true, max: 100 },
|
|
2093
|
-
review_date: { type: 'dateTime',
|
|
2173
|
+
review_date: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() },
|
|
2094
2174
|
reviewable_type: { type: 'string', required: true, search: true },
|
|
2095
2175
|
reviewable_id: { type: 'number', required: true },
|
|
2096
2176
|
// Define the polymorphic field in schema
|
|
@@ -2164,7 +2244,7 @@ export async function createCursorPaginationApi (knex) {
|
|
|
2164
2244
|
price: { type: 'number', required: true },
|
|
2165
2245
|
sku: { type: 'string', required: true, max: 50, unique: true },
|
|
2166
2246
|
status: { type: 'string', max: 50, default: 'active' },
|
|
2167
|
-
createdAt: { type: 'dateTime',
|
|
2247
|
+
createdAt: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() }
|
|
2168
2248
|
},
|
|
2169
2249
|
tableName: 'cursor_products'
|
|
2170
2250
|
})
|
|
@@ -337,6 +337,10 @@ describe('Multi-field Cursor Pagination', () => {
|
|
|
337
337
|
firstPage.data.map((record) => record.attributes.name),
|
|
338
338
|
['Newest Product', 'Recent Product']
|
|
339
339
|
)
|
|
340
|
+
assert.deepEqual(
|
|
341
|
+
firstPage.data.map((record) => record.attributes.createdAt),
|
|
342
|
+
['2026-05-03T10:00:00.000Z', '2026-05-03T09:00:00.000Z']
|
|
343
|
+
)
|
|
340
344
|
assert.deepEqual(
|
|
341
345
|
secondPage.data.map((record) => record.attributes.name),
|
|
342
346
|
['Older Product']
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { after, before, beforeEach, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import {
|
|
5
|
+
createRelationshipIncludeStorageAdapterApi,
|
|
6
|
+
seedRelationshipIncludeStorageAdapterApi
|
|
7
|
+
} from './fixtures/api-configs.js'
|
|
8
|
+
import { cleanTables } from './helpers/test-utils.js'
|
|
9
|
+
|
|
10
|
+
const knex = knexLib({
|
|
11
|
+
client: 'better-sqlite3',
|
|
12
|
+
connection: {
|
|
13
|
+
filename: ':memory:'
|
|
14
|
+
},
|
|
15
|
+
useNullAsDefault: true
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('Relationship include storage adapters', () => {
|
|
19
|
+
let api
|
|
20
|
+
|
|
21
|
+
before(async () => {
|
|
22
|
+
api = await createRelationshipIncludeStorageAdapterApi(knex)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await cleanTables(knex, [
|
|
27
|
+
'uninitialized_bookings',
|
|
28
|
+
'uninitialized_pets'
|
|
29
|
+
])
|
|
30
|
+
await seedRelationshipIncludeStorageAdapterApi(knex)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
after(async () => {
|
|
34
|
+
await knex.destroy()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('maps sparse fields for an included scope whose adapter is not initialized', async () => {
|
|
38
|
+
assert.equal(api.resources.pets.vars.storageAdapter, undefined)
|
|
39
|
+
|
|
40
|
+
const result = await api.resources.bookings.get({
|
|
41
|
+
id: 41,
|
|
42
|
+
queryParams: {
|
|
43
|
+
include: ['pet'],
|
|
44
|
+
fields: {
|
|
45
|
+
bookings: 'reference,petId',
|
|
46
|
+
pets: 'firstName'
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
simplified: false
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const includedPet = result.included.find(resource => resource.type === 'pets')
|
|
53
|
+
assert(includedPet)
|
|
54
|
+
assert.deepEqual(includedPet.attributes, { firstName: 'Coco' })
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -4,6 +4,11 @@ import knexLib from 'knex'
|
|
|
4
4
|
import { Api } from 'hooked-api'
|
|
5
5
|
import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
|
|
6
6
|
import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
|
|
7
|
+
import {
|
|
8
|
+
REST_API_TEMPORAL_DATA_ERROR_CODE,
|
|
9
|
+
RestApiTemporalDataError
|
|
10
|
+
} from '../lib/rest-api-errors.js'
|
|
11
|
+
import { mapRestApiErrorToHttp } from '../plugins/core/connectors/lib/transport-http-helpers.js'
|
|
7
12
|
import {
|
|
8
13
|
cleanTables,
|
|
9
14
|
createJsonApiDocument,
|
|
@@ -23,14 +28,16 @@ const knex = knexLib({
|
|
|
23
28
|
})
|
|
24
29
|
|
|
25
30
|
let api
|
|
31
|
+
const DEFAULT_PROFILE_CREATED_AT = '2026-08-25T12:34:56.789Z'
|
|
32
|
+
const COMPUTED_PROFILE_DATE = new Date('2026-08-25T23:45:01.987Z')
|
|
26
33
|
|
|
27
34
|
const serializeDateTimeForSql = (value) => {
|
|
28
35
|
if (value == null) return null
|
|
29
|
-
if (
|
|
30
|
-
throw new Error('serializeDateTimeForSql expects
|
|
36
|
+
if (typeof value !== 'string') {
|
|
37
|
+
throw new Error('serializeDateTimeForSql expects an RFC 3339 string after schema validation')
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
return value.toISOString().replace('T', ' ').slice(0, 19)
|
|
40
|
+
return new Date(value).toISOString().replace('T', ' ').slice(0, 19)
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
describe('Storage mapping', () => {
|
|
@@ -76,10 +83,27 @@ describe('Storage mapping', () => {
|
|
|
76
83
|
lastSeenAt: {
|
|
77
84
|
type: 'dateTime',
|
|
78
85
|
nullable: true,
|
|
86
|
+
temporalPrecision: 0,
|
|
87
|
+
getter: (value) => value == null ? value : new Date(value),
|
|
79
88
|
storage: {
|
|
80
89
|
serialize: serializeDateTimeForSql
|
|
81
90
|
}
|
|
82
91
|
},
|
|
92
|
+
createdAt: {
|
|
93
|
+
type: 'dateTime',
|
|
94
|
+
temporalPrecision: 3,
|
|
95
|
+
defaultTo: () => new Date(DEFAULT_PROFILE_CREATED_AT).toISOString()
|
|
96
|
+
},
|
|
97
|
+
calculatedAt: {
|
|
98
|
+
type: 'dateTime',
|
|
99
|
+
temporalPrecision: 0,
|
|
100
|
+
computed: true,
|
|
101
|
+
compute: () => new Date(COMPUTED_PROFILE_DATE)
|
|
102
|
+
},
|
|
103
|
+
birthDate: { type: 'date', nullable: true },
|
|
104
|
+
openingTime: { type: 'time', nullable: true, temporalPrecision: 2 },
|
|
105
|
+
observedAtMs: { type: 'epochMilliseconds', nullable: true },
|
|
106
|
+
observedAtSeconds: { type: 'epochSeconds', nullable: true },
|
|
83
107
|
countryId: {
|
|
84
108
|
type: 'id',
|
|
85
109
|
nullable: true,
|
|
@@ -126,7 +150,11 @@ describe('Storage mapping', () => {
|
|
|
126
150
|
{
|
|
127
151
|
displayName: 'Mercury',
|
|
128
152
|
loginCount: 7,
|
|
129
|
-
lastSeenAt: '2026-01-
|
|
153
|
+
lastSeenAt: '2026-01-02T11:04:05+08:00',
|
|
154
|
+
birthDate: '2020-02-29',
|
|
155
|
+
openingTime: '09:30:15.12',
|
|
156
|
+
observedAtMs: '1767323045000',
|
|
157
|
+
observedAtSeconds: '1767323045',
|
|
130
158
|
externalRef: 'legacy-001',
|
|
131
159
|
confirmationToken: 'secret-token'
|
|
132
160
|
},
|
|
@@ -142,8 +170,13 @@ describe('Storage mapping', () => {
|
|
|
142
170
|
loginCount: 7
|
|
143
171
|
})
|
|
144
172
|
assert.equal(createResult.data.attributes.confirmationToken, 'secret-token')
|
|
145
|
-
assert.
|
|
146
|
-
assert.equal(createResult.data.attributes.
|
|
173
|
+
assert.equal(createResult.data.attributes.lastSeenAt, '2026-01-02T03:04:05Z')
|
|
174
|
+
assert.equal(createResult.data.attributes.createdAt, DEFAULT_PROFILE_CREATED_AT)
|
|
175
|
+
assert.equal(createResult.data.attributes.calculatedAt, '2026-08-25T23:45:01Z')
|
|
176
|
+
assert.equal(createResult.data.attributes.birthDate, '2020-02-29')
|
|
177
|
+
assert.equal(createResult.data.attributes.openingTime, '09:30:15.12')
|
|
178
|
+
assert.equal(createResult.data.attributes.observedAtMs, 1767323045000)
|
|
179
|
+
assert.equal(createResult.data.attributes.observedAtSeconds, 1767323045)
|
|
147
180
|
assert.ok(!('display_name' in createResult.data.attributes))
|
|
148
181
|
assert.ok(!('country_id' in createResult.data.attributes))
|
|
149
182
|
|
|
@@ -151,6 +184,8 @@ describe('Storage mapping', () => {
|
|
|
151
184
|
assert.equal(dbRecord.display_name, 'Mercury')
|
|
152
185
|
assert.equal(dbRecord.login_count, 7)
|
|
153
186
|
assert.equal(dbRecord.last_seen_at, '2026-01-02 03:04:05')
|
|
187
|
+
assert.equal(dbRecord.observed_at_ms, 1767323045000)
|
|
188
|
+
assert.equal(dbRecord.observed_at_seconds, 1767323045)
|
|
154
189
|
assert.equal(String(dbRecord.country_id), country.data.id)
|
|
155
190
|
assert.equal(dbRecord.legacy_ref, 'legacy-001')
|
|
156
191
|
assert.ok(!('displayName' in dbRecord))
|
|
@@ -169,8 +204,13 @@ describe('Storage mapping', () => {
|
|
|
169
204
|
displayName: 'Mercury',
|
|
170
205
|
loginCount: 7
|
|
171
206
|
})
|
|
172
|
-
assert.
|
|
173
|
-
assert.equal(getResult.data.attributes.
|
|
207
|
+
assert.equal(getResult.data.attributes.lastSeenAt, '2026-01-02T03:04:05Z')
|
|
208
|
+
assert.equal(getResult.data.attributes.createdAt, DEFAULT_PROFILE_CREATED_AT)
|
|
209
|
+
assert.equal(getResult.data.attributes.calculatedAt, '2026-08-25T23:45:01Z')
|
|
210
|
+
assert.equal(getResult.data.attributes.birthDate, '2020-02-29')
|
|
211
|
+
assert.equal(getResult.data.attributes.openingTime, '09:30:15.12')
|
|
212
|
+
assert.equal(getResult.data.attributes.observedAtMs, 1767323045000)
|
|
213
|
+
assert.equal(getResult.data.attributes.observedAtSeconds, 1767323045)
|
|
174
214
|
assert.ok(!('display_name' in getResult.data.attributes))
|
|
175
215
|
assertResourceRelationship(
|
|
176
216
|
getResult.data,
|
|
@@ -193,6 +233,8 @@ describe('Storage mapping', () => {
|
|
|
193
233
|
displayName: 'Mercury',
|
|
194
234
|
externalRef: 'legacy-001'
|
|
195
235
|
})
|
|
236
|
+
assert.equal(queryResult.data[0].attributes.createdAt, DEFAULT_PROFILE_CREATED_AT)
|
|
237
|
+
assert.equal(queryResult.data[0].attributes.calculatedAt, '2026-08-25T23:45:01Z')
|
|
196
238
|
})
|
|
197
239
|
|
|
198
240
|
it('applies storage mapping on PATCH updates and sparse fieldsets', async () => {
|
|
@@ -228,8 +270,7 @@ describe('Storage mapping', () => {
|
|
|
228
270
|
assert.deepEqual(Object.keys(patchResult.data.attributes).sort(), ['confirmationToken', 'displayName', 'lastSeenAt'])
|
|
229
271
|
assert.equal(patchResult.data.attributes.displayName, 'After')
|
|
230
272
|
assert.equal(patchResult.data.attributes.confirmationToken, 'updated-token')
|
|
231
|
-
assert.
|
|
232
|
-
assert.equal(patchResult.data.attributes.lastSeenAt.toISOString(), '2026-02-03T04:05:06.000Z')
|
|
273
|
+
assert.equal(patchResult.data.attributes.lastSeenAt, '2026-02-03T04:05:06Z')
|
|
233
274
|
|
|
234
275
|
const dbRecord = await knex('mapped_profiles').where('id', created.data.id).first()
|
|
235
276
|
assert.equal(dbRecord.display_name, 'After')
|
|
@@ -237,6 +278,67 @@ describe('Storage mapping', () => {
|
|
|
237
278
|
assert.ok(!('displayName' in dbRecord))
|
|
238
279
|
})
|
|
239
280
|
|
|
281
|
+
it('rejects temporal values outside the public JSON contract before storage', async () => {
|
|
282
|
+
const invalidTemporalValues = [
|
|
283
|
+
{ lastSeenAt: '2026-01-02 03:04:05' },
|
|
284
|
+
{ lastSeenAt: new Date('2026-01-02T03:04:05Z') },
|
|
285
|
+
{ birthDate: '2026-02-30' },
|
|
286
|
+
{ openingTime: '09:30Z' }
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
for (const [index, temporalAttributes] of invalidTemporalValues.entries()) {
|
|
290
|
+
await assert.rejects(
|
|
291
|
+
api.resources.profiles.post({
|
|
292
|
+
inputRecord: createJsonApiDocument('profiles', {
|
|
293
|
+
displayName: `Invalid temporal value ${index}`,
|
|
294
|
+
...temporalAttributes
|
|
295
|
+
})
|
|
296
|
+
}),
|
|
297
|
+
(error) => error?.code === 'REST_API_VALIDATION'
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('fails loudly when storage returns an invalid non-null temporal value', async () => {
|
|
303
|
+
const insertedIds = await knex('mapped_profiles').insert({
|
|
304
|
+
display_name: 'Corrupt temporal row',
|
|
305
|
+
login_count: 0,
|
|
306
|
+
last_seen_at: 'not-a-date',
|
|
307
|
+
created_at: new Date(DEFAULT_PROFILE_CREATED_AT)
|
|
308
|
+
})
|
|
309
|
+
const profileId = insertedIds[0]
|
|
310
|
+
|
|
311
|
+
await assert.rejects(
|
|
312
|
+
api.resources.profiles.get({ id: profileId }),
|
|
313
|
+
(error) => {
|
|
314
|
+
assert(error instanceof RestApiTemporalDataError)
|
|
315
|
+
assert.equal(error.code, REST_API_TEMPORAL_DATA_ERROR_CODE)
|
|
316
|
+
assert.equal(
|
|
317
|
+
error.message,
|
|
318
|
+
"Invalid database value for dateTime field 'profiles.lastSeenAt'."
|
|
319
|
+
)
|
|
320
|
+
assert.deepEqual(error.details, {
|
|
321
|
+
field: 'lastSeenAt',
|
|
322
|
+
resourceType: 'profiles',
|
|
323
|
+
fieldType: 'dateTime',
|
|
324
|
+
source: 'database'
|
|
325
|
+
})
|
|
326
|
+
assert.deepEqual(mapRestApiErrorToHttp(error), {
|
|
327
|
+
status: 500,
|
|
328
|
+
body: {
|
|
329
|
+
errors: [{
|
|
330
|
+
status: '500',
|
|
331
|
+
code: REST_API_TEMPORAL_DATA_ERROR_CODE,
|
|
332
|
+
title: 'Invalid Temporal Data',
|
|
333
|
+
detail: "Invalid database value for dateTime field 'profiles.lastSeenAt'."
|
|
334
|
+
}]
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
return true
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
})
|
|
341
|
+
|
|
240
342
|
it('can opt out of snake_case translation for an entire resource', async () => {
|
|
241
343
|
const created = await api.resources.verbatim_profiles.post({
|
|
242
344
|
inputRecord: createJsonApiDocument('verbatim_profiles', {
|