json-rest-api 1.0.23 → 1.0.25
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 +24 -2
- package/docs/API.md +157 -33
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +31 -38
- package/plugins/core/connectors/express-plugin.js +6 -7
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/schema-utils.js +44 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
- 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 +20 -16
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
- package/plugins/core/lib/storage/storage-adapter.js +76 -14
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
- package/plugins/core/rest-api-knex-plugin.js +93 -76
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
- package/plugins/core/rest-api-plugin-methods/common.js +112 -72
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
- package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
- package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
- package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
- package/plugins/core/rest-api-plugin-methods/post.js +12 -7
- package/plugins/core/rest-api-plugin-methods/put.js +35 -10
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +11 -37
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +45 -20
- package/plugins/storage/s3-storage.js +23 -54
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +8 -1
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -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 +49 -6
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +495 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/id-normalization.test.js +444 -0
- package/tests/local-storage.test.js +96 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +132 -11
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +433 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +56 -6
- package/tests/test-development-guide.md +5 -5
|
@@ -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]
|
|
@@ -135,7 +136,7 @@ export const updateManyToManyRelationship = async (scope, deps) => {
|
|
|
135
136
|
await api.resources[related.type].get({
|
|
136
137
|
id: related.id,
|
|
137
138
|
transaction: trx
|
|
138
|
-
})
|
|
139
|
+
}, { ...context })
|
|
139
140
|
} catch (error) {
|
|
140
141
|
throw new RestApiResourceError(
|
|
141
142
|
`Related ${related.type} with id ${related.id} not found`,
|
|
@@ -183,6 +184,7 @@ export const updateManyToManyRelationship = async (scope, deps) => {
|
|
|
183
184
|
* @param {Object} relDef - The relationship definition
|
|
184
185
|
* @param {Array} relData - Array of related resources to link
|
|
185
186
|
* @param {Object} trx - Database transaction object
|
|
187
|
+
* @param {Object} [context] - Caller context forwarded to relationship validation
|
|
186
188
|
* @returns {Promise<void>}
|
|
187
189
|
*
|
|
188
190
|
* @example
|
|
@@ -256,7 +258,8 @@ export const updateManyToManyRelationship = async (scope, deps) => {
|
|
|
256
258
|
* 5. Performs single INSERT with all records
|
|
257
259
|
* 6. Returns (no data returned, throws on error)
|
|
258
260
|
*/
|
|
259
|
-
export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
|
|
261
|
+
export const createPivotRecords = async (api, resourceId, relDef, relData, trx, context = {}) => {
|
|
262
|
+
relData = normalizeRelationshipIdentifiers(relData, { api })
|
|
260
263
|
if (relData.length === 0) return // Early exit if nothing to create
|
|
261
264
|
|
|
262
265
|
// Get pivot table info
|
|
@@ -275,7 +278,7 @@ export const createPivotRecords = async (api, resourceId, relDef, relData, trx)
|
|
|
275
278
|
await api.resources[related.type].get({
|
|
276
279
|
id: related.id,
|
|
277
280
|
transaction: trx
|
|
278
|
-
})
|
|
281
|
+
}, { ...context })
|
|
279
282
|
} catch (error) {
|
|
280
283
|
throw new RestApiResourceError(
|
|
281
284
|
`Related ${related.type} with id ${related.id} not found`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
+
import { validateRelationshipDataCardinality } from '../querying-writing/relationship-contracts.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Processes JSON:API relationship data and converts to database operations
|
|
@@ -183,6 +184,14 @@ export const processRelationships = (scope, deps) => {
|
|
|
183
184
|
fieldDef.as === relName
|
|
184
185
|
)
|
|
185
186
|
|
|
187
|
+
if (schemaField || relDef) {
|
|
188
|
+
validateRelationshipDataCardinality({
|
|
189
|
+
relationshipName: relName,
|
|
190
|
+
relDef: schemaField ? schemaField[1] : relDef,
|
|
191
|
+
data: relData?.data
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
if (schemaField) {
|
|
187
196
|
const [fieldName, fieldDef] = schemaField
|
|
188
197
|
|
|
@@ -235,7 +244,7 @@ export const processRelationships = (scope, deps) => {
|
|
|
235
244
|
foreignKey: relDef.foreignKey,
|
|
236
245
|
otherKey: relDef.otherKey
|
|
237
246
|
},
|
|
238
|
-
relData: relData.data
|
|
247
|
+
relData: relData.data
|
|
239
248
|
})
|
|
240
249
|
}
|
|
241
250
|
// Note: hasOne and hasMany don't need processing here as they don't update the current record
|