json-rest-api 1.0.23 → 1.0.24
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/README.md +22 -1
- package/docs/API.md +145 -24
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +83 -4
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +26 -34
- package/plugins/core/connectors/express-plugin.js +0 -6
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/lib/anyapi/schema-utils.js +43 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
- package/plugins/core/lib/dbIntrospection.js +1 -1
- package/plugins/core/lib/dbTablesOperations.js +10 -3
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +0 -1
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +11 -18
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +1 -2
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +22 -2
- package/plugins/core/lib/storage/storage-adapter.js +3 -2
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
- package/plugins/core/rest-api-anyapi-knex-plugin.js +194 -102
- package/plugins/core/rest-api-knex-plugin.js +23 -16
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
- package/plugins/core/rest-api-plugin-methods/common.js +27 -9
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
- package/plugins/core/rest-api-plugin-methods/patch.js +31 -3
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/post.js +8 -0
- package/plugins/core/rest-api-plugin-methods/put.js +31 -3
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +4 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +1 -1
- package/plugins/storage/s3-storage.js +5 -2
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +7 -0
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/db-introspection.test.js +4 -1
- package/tests/db-table-operations.test.js +25 -20
- package/tests/fastify-plugin.test.js +21 -6
- package/tests/fixtures/api-configs.js +133 -0
- package/tests/id-normalization.test.js +444 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +11 -11
- package/tests/storage-mapping.test.js +55 -6
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RestApiResourceError,
|
|
3
|
+
RestApiValidationError
|
|
4
|
+
} from '../../../../lib/rest-api-errors.js'
|
|
5
|
+
|
|
6
|
+
export function defaultNormalizeResourceId (value) {
|
|
7
|
+
if (value === null || value === undefined) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof value === 'string') {
|
|
12
|
+
const normalized = value.trim()
|
|
13
|
+
return normalized || null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
return Number.isFinite(value) ? String(value) : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof value === 'bigint') {
|
|
21
|
+
return String(value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const normalized = String(value).trim()
|
|
25
|
+
return normalized || null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveResourceIdNormalizer ({ scopeOptions = null, vars = null } = {}) {
|
|
29
|
+
if (typeof scopeOptions?.normalizeId === 'function') {
|
|
30
|
+
return scopeOptions.normalizeId
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof vars?.normalizeId === 'function') {
|
|
34
|
+
return vars.normalizeId
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return defaultNormalizeResourceId
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeResourceId (value, options = {}) {
|
|
41
|
+
const normalizer = resolveResourceIdNormalizer(options)
|
|
42
|
+
return defaultNormalizeResourceId(normalizer(value))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function requireExistingResourceId (value, {
|
|
46
|
+
scopeOptions = null,
|
|
47
|
+
vars = null,
|
|
48
|
+
scopeName = ''
|
|
49
|
+
} = {}) {
|
|
50
|
+
const normalizedId = normalizeResourceId(value, { scopeOptions, vars })
|
|
51
|
+
if (normalizedId) {
|
|
52
|
+
return normalizedId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new RestApiResourceError(
|
|
56
|
+
'Resource not found',
|
|
57
|
+
{
|
|
58
|
+
subtype: 'not_found',
|
|
59
|
+
resourceType: scopeName,
|
|
60
|
+
resourceId: value == null ? null : String(value)
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function requireDocumentResourceId (value, {
|
|
66
|
+
scopeOptions = null,
|
|
67
|
+
vars = null
|
|
68
|
+
} = {}) {
|
|
69
|
+
const normalizedId = normalizeResourceId(value, { scopeOptions, vars })
|
|
70
|
+
if (normalizedId) {
|
|
71
|
+
return normalizedId
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new RestApiValidationError(
|
|
75
|
+
'Resource document id is invalid',
|
|
76
|
+
{
|
|
77
|
+
fields: ['data.id'],
|
|
78
|
+
violations: [{
|
|
79
|
+
field: 'data.id',
|
|
80
|
+
rule: 'invalid_resource_id',
|
|
81
|
+
message: 'Resource document id must normalize to a non-empty value.'
|
|
82
|
+
}]
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveResourceNormalizationOptions (resourceType, { api } = {}) {
|
|
88
|
+
const scope = api?.resources?.[resourceType]
|
|
89
|
+
return {
|
|
90
|
+
scopeOptions: scope?.scopeOptions || scope?._scopeOptions || null,
|
|
91
|
+
vars: scope?.vars || null,
|
|
92
|
+
scopeName: resourceType
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function requireReferencedResourceId (resourceType, value, { api } = {}) {
|
|
97
|
+
return requireExistingResourceId(
|
|
98
|
+
value,
|
|
99
|
+
resolveResourceNormalizationOptions(resourceType, { api })
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeRelationshipIdentifier (identifier, { api } = {}) {
|
|
104
|
+
if (!identifier || typeof identifier !== 'object' || Array.isArray(identifier)) {
|
|
105
|
+
return identifier
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const type = typeof identifier.type === 'string' ? identifier.type : ''
|
|
109
|
+
if (!type || !Object.hasOwn(identifier, 'id')) {
|
|
110
|
+
return {
|
|
111
|
+
...identifier
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...identifier,
|
|
117
|
+
id: requireReferencedResourceId(type, identifier.id, { api })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function normalizeRelationshipIdentifiers (relationshipData, { api } = {}) {
|
|
122
|
+
if (Array.isArray(relationshipData)) {
|
|
123
|
+
return relationshipData.map((identifier) => normalizeRelationshipIdentifier(identifier, { api }))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (relationshipData == null) {
|
|
127
|
+
return relationshipData
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return normalizeRelationshipIdentifier(relationshipData, { api })
|
|
131
|
+
}
|
|
@@ -280,22 +280,15 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
280
280
|
relationshipsData[relName] = {
|
|
281
281
|
data: value.map(relId => ({ type: targetType, id: String(relId) }))
|
|
282
282
|
}
|
|
283
|
-
}
|
|
284
|
-
// Handle hasOne relationships
|
|
285
|
-
else if (relConfig.type === 'hasOne' && value !== undefined) {
|
|
283
|
+
} else if (relConfig.type === 'hasOne' && value !== undefined) {
|
|
286
284
|
relationshipsData[relName] = {
|
|
287
285
|
data: value ? { type: relConfig.target, id: String(value) } : null
|
|
288
286
|
}
|
|
289
|
-
}
|
|
290
|
-
// Handle single belongsTo relationships if they were also defined in 'relationships'
|
|
291
|
-
// (though typically they are defined in schema.belongsTo)
|
|
292
|
-
else if (relConfig.belongsTo && value !== undefined) {
|
|
287
|
+
} else if (relConfig.belongsTo && value !== undefined) {
|
|
293
288
|
relationshipsData[relName] = {
|
|
294
289
|
data: value ? { type: relConfig.belongsTo, id: String(value) } : null
|
|
295
290
|
}
|
|
296
|
-
}
|
|
297
|
-
// Handle polymorphic belongsTo relationships with minimal object format
|
|
298
|
-
else if (relConfig.belongsToPolymorphic && value !== undefined) {
|
|
291
|
+
} else if (relConfig.belongsToPolymorphic && value !== undefined) {
|
|
299
292
|
if (value === null) {
|
|
300
293
|
relationshipsData[relName] = { data: null }
|
|
301
294
|
} else if (typeof value === 'object' && value !== null && value.id && value._type) {
|
|
@@ -780,7 +773,7 @@ export const transformSingleJsonApiToSimplified = (scope, deps) => {
|
|
|
780
773
|
const schemaEntry = Object.entries(schema).find(([_, def]) => def.as === relName)
|
|
781
774
|
|
|
782
775
|
if (schemaEntry) {
|
|
783
|
-
const [
|
|
776
|
+
const [, fieldDef] = schemaEntry
|
|
784
777
|
if (fieldDef.belongsTo && relData.data) {
|
|
785
778
|
// Create minimal relationship object (no more foreign key field)
|
|
786
779
|
simplified[relName] = { id: relData.data.id }
|
|
@@ -23,9 +23,29 @@ export const translateCanonicalAttributesForStorage = (attributes = {}, descript
|
|
|
23
23
|
return row
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export const getCanonicalResourceIdColumn = (descriptor = {}) => (
|
|
27
|
+
descriptor?.canonical?.logicalIdColumn || 'logical_id'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export const getCanonicalResourceId = (row = {}, descriptor = {}) => {
|
|
31
|
+
const logicalIdColumn = getCanonicalResourceIdColumn(descriptor)
|
|
32
|
+
const logicalId = row?.[logicalIdColumn]
|
|
33
|
+
|
|
34
|
+
if (logicalId !== undefined && logicalId !== null) {
|
|
35
|
+
return String(logicalId)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (row?.id !== undefined && row?.id !== null) {
|
|
39
|
+
return String(row.id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
export const translateCanonicalRecordFromStorage = (row = {}, descriptor = {}, options = {}) => {
|
|
27
46
|
const attributes = {}
|
|
28
|
-
const
|
|
47
|
+
const logicalIdColumn = getCanonicalResourceIdColumn(descriptor)
|
|
48
|
+
const consumedColumns = new Set(['id', logicalIdColumn])
|
|
29
49
|
const allowedExtraFields = new Set(options.allowedExtraFields || [])
|
|
30
50
|
|
|
31
51
|
for (const [slot, logical] of Object.entries(descriptor.reverseAttributes || {})) {
|
|
@@ -97,7 +117,7 @@ export const getCanonicalFieldValue = (row, descriptor = {}, fieldName) => {
|
|
|
97
117
|
if (!row || !fieldName) return undefined
|
|
98
118
|
|
|
99
119
|
if (fieldName === 'id') {
|
|
100
|
-
return row
|
|
120
|
+
return getCanonicalResourceId(row, descriptor)
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
const fieldInfo = descriptor.fields?.[fieldName]
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from './storage-mapping.js'
|
|
7
7
|
import {
|
|
8
8
|
getCanonicalFieldValue,
|
|
9
|
+
getCanonicalResourceIdColumn,
|
|
9
10
|
translateCanonicalAttributesForStorage,
|
|
10
11
|
} from './canonical-storage-mapping.js'
|
|
11
12
|
|
|
@@ -146,7 +147,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
146
147
|
|
|
147
148
|
const translateColumn = (field) => {
|
|
148
149
|
if (!field) return field
|
|
149
|
-
if (field === 'id') return
|
|
150
|
+
if (field === 'id') return getCanonicalResourceIdColumn(descriptor)
|
|
150
151
|
|
|
151
152
|
const canonicalEntry = canonicalFieldMap[field]
|
|
152
153
|
if (typeof canonicalEntry === 'string') {
|
|
@@ -214,7 +215,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
214
215
|
return baseAdapter({
|
|
215
216
|
knex,
|
|
216
217
|
tableName: canonical.tableName,
|
|
217
|
-
idColumn:
|
|
218
|
+
idColumn: getCanonicalResourceIdColumn(descriptor),
|
|
218
219
|
translateColumn,
|
|
219
220
|
translateFilterValue,
|
|
220
221
|
applyResourceScope,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const EMPTY_STORAGE_INFO = Object.freeze({
|
|
2
2
|
idColumn: 'id',
|
|
3
|
+
storage: Object.freeze({
|
|
4
|
+
naming: 'snake_case'
|
|
5
|
+
}),
|
|
3
6
|
fields: Object.freeze({}),
|
|
4
7
|
columns: Object.freeze({})
|
|
5
8
|
})
|
|
@@ -10,14 +13,75 @@ const isPlainObject = (value) => (
|
|
|
10
13
|
!Array.isArray(value)
|
|
11
14
|
)
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
const SNAKE_CASE_STORAGE_NAMING = 'snake_case'
|
|
17
|
+
const EXACT_STORAGE_NAMING = 'exact'
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
const STORAGE_NAMING_ALIASES = Object.freeze({
|
|
20
|
+
snake: SNAKE_CASE_STORAGE_NAMING,
|
|
21
|
+
snake_case: SNAKE_CASE_STORAGE_NAMING,
|
|
22
|
+
snakeCase: SNAKE_CASE_STORAGE_NAMING,
|
|
23
|
+
exact: EXACT_STORAGE_NAMING,
|
|
24
|
+
field: EXACT_STORAGE_NAMING,
|
|
25
|
+
verbatim: EXACT_STORAGE_NAMING
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const toSnakeCase = (value) => String(value)
|
|
29
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
30
|
+
.replace(/([A-Z]+)([A-Z][a-z0-9])/g, '$1_$2')
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
|
|
33
|
+
export const normalizeStorageConfig = (storage = undefined) => {
|
|
34
|
+
if (storage === undefined || storage === null) {
|
|
35
|
+
return {
|
|
36
|
+
naming: SNAKE_CASE_STORAGE_NAMING
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isPlainObject(storage)) {
|
|
41
|
+
throw new Error('Resource storage metadata must be an object.')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rawNaming = storage.naming
|
|
45
|
+
const naming = rawNaming === undefined
|
|
46
|
+
? SNAKE_CASE_STORAGE_NAMING
|
|
47
|
+
: STORAGE_NAMING_ALIASES[rawNaming]
|
|
48
|
+
|
|
49
|
+
if (!naming) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid resource storage.naming value '${String(rawNaming)}'. Expected 'snake_case' or 'exact'.`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...storage,
|
|
57
|
+
naming
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const resolveDefaultStorageColumn = (fieldName, { idColumn = 'id', storageConfig = undefined } = {}) => {
|
|
62
|
+
if (fieldName === 'id') {
|
|
63
|
+
return idColumn
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const normalizedStorage = normalizeStorageConfig(storageConfig)
|
|
67
|
+
if (normalizedStorage.naming === EXACT_STORAGE_NAMING) {
|
|
68
|
+
return fieldName
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return toSnakeCase(fieldName)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = 'id', storageConfig = undefined } = {}) => {
|
|
75
|
+
const fieldStorage = definition.storage
|
|
76
|
+
|
|
77
|
+
if (fieldStorage !== undefined && !isPlainObject(fieldStorage)) {
|
|
17
78
|
throw new Error(`Field '${fieldName}' has invalid storage metadata. Expected an object.`)
|
|
18
79
|
}
|
|
19
80
|
|
|
20
|
-
const column =
|
|
81
|
+
const column = fieldStorage?.column ?? resolveDefaultStorageColumn(fieldName, {
|
|
82
|
+
idColumn,
|
|
83
|
+
storageConfig
|
|
84
|
+
})
|
|
21
85
|
if (typeof column !== 'string' || column.trim() === '') {
|
|
22
86
|
throw new Error(`Field '${fieldName}' has an invalid storage.column value.`)
|
|
23
87
|
}
|
|
@@ -28,25 +92,29 @@ export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = '
|
|
|
28
92
|
)
|
|
29
93
|
}
|
|
30
94
|
|
|
31
|
-
if (
|
|
95
|
+
if (fieldStorage?.serialize !== undefined && typeof fieldStorage.serialize !== 'function') {
|
|
32
96
|
throw new Error(`Field '${fieldName}' has an invalid storage.serialize value. Expected a function.`)
|
|
33
97
|
}
|
|
34
98
|
|
|
35
99
|
return {
|
|
36
100
|
column,
|
|
37
|
-
serialize:
|
|
101
|
+
serialize: fieldStorage?.serialize || null,
|
|
38
102
|
persisted: definition.virtual !== true && definition.computed !== true
|
|
39
103
|
}
|
|
40
104
|
}
|
|
41
105
|
|
|
42
|
-
export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id' } = {}) => {
|
|
106
|
+
export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id', storage = undefined } = {}) => {
|
|
43
107
|
const idColumn = idProperty || 'id'
|
|
108
|
+
const storageConfig = normalizeStorageConfig(storage)
|
|
44
109
|
const fields = {}
|
|
45
110
|
const columns = {}
|
|
46
111
|
const usedColumns = new Map([[idColumn, 'id']])
|
|
47
112
|
|
|
48
113
|
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
49
|
-
const normalized = normalizeFieldStorage(fieldName, definition, {
|
|
114
|
+
const normalized = normalizeFieldStorage(fieldName, definition, {
|
|
115
|
+
idColumn,
|
|
116
|
+
storageConfig
|
|
117
|
+
})
|
|
50
118
|
const isIdSurrogateField = fieldName === idColumn && definition.type === 'id'
|
|
51
119
|
|
|
52
120
|
if (fieldName !== 'id' && normalized.column === idColumn && !isIdSurrogateField) {
|
|
@@ -75,6 +143,7 @@ export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id' } = {
|
|
|
75
143
|
|
|
76
144
|
return {
|
|
77
145
|
idColumn,
|
|
146
|
+
storage: storageConfig,
|
|
78
147
|
fields,
|
|
79
148
|
columns
|
|
80
149
|
}
|
|
@@ -91,7 +160,8 @@ export const getStorageInfo = (schemaInfo = {}) => {
|
|
|
91
160
|
|
|
92
161
|
return buildStorageInfo({
|
|
93
162
|
schemaStructure: schemaInfo.schemaStructure,
|
|
94
|
-
idProperty: schemaInfo.idProperty || 'id'
|
|
163
|
+
idProperty: schemaInfo.idProperty || 'id',
|
|
164
|
+
storage: schemaInfo.storage
|
|
95
165
|
})
|
|
96
166
|
}
|
|
97
167
|
|
|
@@ -165,13 +235,13 @@ export const translateAttributesForStorage = (attributes, schemaInfo = {}, optio
|
|
|
165
235
|
: fieldStorage?.column || fieldName
|
|
166
236
|
const serializedValue = fieldStorage?.serialize
|
|
167
237
|
? fieldStorage.serialize(value, {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
238
|
+
fieldName,
|
|
239
|
+
columnName,
|
|
240
|
+
definition: fieldStorage.definition,
|
|
241
|
+
schemaInfo,
|
|
242
|
+
context,
|
|
243
|
+
operation
|
|
244
|
+
})
|
|
175
245
|
: value
|
|
176
246
|
|
|
177
247
|
translated[columnName] = serializedValue
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeRelationshipIdentifiers } from '../querying-writing/resource-id-normalization.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Updates many-to-many relationships intelligently by synchronizing pivot table records
|
|
@@ -103,7 +103,8 @@ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-hel
|
|
|
103
103
|
export const updateManyToManyRelationship = async (scope, deps) => {
|
|
104
104
|
// Extract values from deps
|
|
105
105
|
const { api, context } = deps
|
|
106
|
-
const { resourceId, relDef,
|
|
106
|
+
const { resourceId, relDef, transaction: trx } = context
|
|
107
|
+
const relData = normalizeRelationshipIdentifiers(context.relData || [], { api })
|
|
107
108
|
|
|
108
109
|
// Get the knex instance from the pivot scope
|
|
109
110
|
const pivotScope = api.resources[relDef.through]
|
|
@@ -257,6 +258,7 @@ export const updateManyToManyRelationship = async (scope, deps) => {
|
|
|
257
258
|
* 6. Returns (no data returned, throws on error)
|
|
258
259
|
*/
|
|
259
260
|
export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
|
|
261
|
+
relData = normalizeRelationshipIdentifiers(relData, { api })
|
|
260
262
|
if (relData.length === 0) return // Early exit if nothing to create
|
|
261
263
|
|
|
262
264
|
// Get pivot table info
|