json-rest-api 1.0.21 → 1.0.23
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +4 -4
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getFieldValue as getLegacyFieldValue,
|
|
3
|
+
getIdColumn,
|
|
4
|
+
getStorageColumn,
|
|
5
|
+
translateAttributesForStorage,
|
|
6
|
+
} from './storage-mapping.js'
|
|
7
|
+
import {
|
|
8
|
+
getCanonicalFieldValue,
|
|
9
|
+
translateCanonicalAttributesForStorage,
|
|
10
|
+
} from './canonical-storage-mapping.js'
|
|
11
|
+
|
|
1
12
|
const passthrough = (value) => value
|
|
2
13
|
const identityTranslate = (_field, value) => value
|
|
3
14
|
const identityScope = (query) => query
|
|
@@ -22,6 +33,59 @@ const normalizeBelongsToValue = (value) => {
|
|
|
22
33
|
return String(value)
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
const translateSourceColumn = (source, adapter) => {
|
|
37
|
+
if (!adapter || source === '*') return source
|
|
38
|
+
return adapter.translateColumn(source) || source
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const createSelectTranslator = (adapter) => {
|
|
42
|
+
if (!adapter) return null
|
|
43
|
+
|
|
44
|
+
return (field, alias) => {
|
|
45
|
+
if (field === '*') {
|
|
46
|
+
return alias ? `${alias}.*` : '*'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const translated = adapter.translateColumn(field)
|
|
50
|
+
const result = translated || field
|
|
51
|
+
|
|
52
|
+
if (field === 'id' && result !== 'id') {
|
|
53
|
+
const qualified = alias ? `${alias}.${result}` : result
|
|
54
|
+
return `${qualified} as id`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!alias) {
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (result.includes('.')) {
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `${alias}.${result}`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const translateSelectFieldsForAdapter = (fields, adapter) => {
|
|
70
|
+
if (!adapter || !fields) return fields
|
|
71
|
+
|
|
72
|
+
const translateField = createSelectTranslator(adapter)
|
|
73
|
+
|
|
74
|
+
return fields.map((field) => {
|
|
75
|
+
if (typeof field !== 'string') return field
|
|
76
|
+
if (field === '*') return '*'
|
|
77
|
+
|
|
78
|
+
const aliasMatch = field.match(/\s+as\s+/i)
|
|
79
|
+
if (aliasMatch) {
|
|
80
|
+
const [source, alias] = field.split(/\s+as\s+/i)
|
|
81
|
+
const translatedSource = translateSourceColumn(source.trim(), adapter)
|
|
82
|
+
return `${translatedSource} as ${alias.trim()}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return translateField(field)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
25
89
|
const baseAdapter = ({
|
|
26
90
|
knex,
|
|
27
91
|
tableName,
|
|
@@ -29,6 +93,8 @@ const baseAdapter = ({
|
|
|
29
93
|
translateColumn,
|
|
30
94
|
translateFilterValue = identityTranslate,
|
|
31
95
|
applyResourceScope = identityScope,
|
|
96
|
+
toStorageRow = passthrough,
|
|
97
|
+
getFieldValue = (record, fieldName) => record?.[fieldName],
|
|
32
98
|
isCanonical = false,
|
|
33
99
|
}) => {
|
|
34
100
|
const buildBaseQuery = ({ transaction } = {}) => {
|
|
@@ -43,6 +109,8 @@ const baseAdapter = ({
|
|
|
43
109
|
translateColumn,
|
|
44
110
|
translateFilterValue,
|
|
45
111
|
applyResourceScope,
|
|
112
|
+
toStorageRow,
|
|
113
|
+
getFieldValue,
|
|
46
114
|
buildBaseQuery,
|
|
47
115
|
selectColumns: (builder, columns) => selectColumnsOnBuilder(builder, columns),
|
|
48
116
|
}
|
|
@@ -50,9 +118,11 @@ const baseAdapter = ({
|
|
|
50
118
|
|
|
51
119
|
const createLegacyAdapter = ({ knex, schemaInfo }) => {
|
|
52
120
|
const tableName = schemaInfo.tableName
|
|
53
|
-
const idColumn = schemaInfo
|
|
121
|
+
const idColumn = getIdColumn(schemaInfo)
|
|
54
122
|
|
|
55
|
-
const translateColumn = (
|
|
123
|
+
const translateColumn = (field) => getStorageColumn(schemaInfo, field)
|
|
124
|
+
const toStorageRow = (attributes, options = {}) => translateAttributesForStorage(attributes, schemaInfo, options)
|
|
125
|
+
const getFieldValue = (record, fieldName) => getLegacyFieldValue(record, schemaInfo, fieldName)
|
|
56
126
|
|
|
57
127
|
return baseAdapter({
|
|
58
128
|
knex,
|
|
@@ -61,6 +131,8 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
|
|
|
61
131
|
translateColumn,
|
|
62
132
|
translateFilterValue: identityTranslate,
|
|
63
133
|
applyResourceScope: identityScope,
|
|
134
|
+
toStorageRow,
|
|
135
|
+
getFieldValue,
|
|
64
136
|
isCanonical: false,
|
|
65
137
|
})
|
|
66
138
|
}
|
|
@@ -136,6 +208,9 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
136
208
|
.where(canonical.resourceColumn, descriptor.resource)
|
|
137
209
|
}
|
|
138
210
|
|
|
211
|
+
const toStorageRow = (attributes) => translateCanonicalAttributesForStorage(attributes, descriptor)
|
|
212
|
+
const getFieldValue = (record, fieldName) => getCanonicalFieldValue(record, descriptor, fieldName)
|
|
213
|
+
|
|
139
214
|
return baseAdapter({
|
|
140
215
|
knex,
|
|
141
216
|
tableName: canonical.tableName,
|
|
@@ -143,6 +218,8 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
143
218
|
translateColumn,
|
|
144
219
|
translateFilterValue,
|
|
145
220
|
applyResourceScope,
|
|
221
|
+
toStorageRow,
|
|
222
|
+
getFieldValue,
|
|
146
223
|
isCanonical: true,
|
|
147
224
|
})
|
|
148
225
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const EMPTY_STORAGE_INFO = Object.freeze({
|
|
2
|
+
idColumn: 'id',
|
|
3
|
+
fields: Object.freeze({}),
|
|
4
|
+
columns: Object.freeze({})
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
const isPlainObject = (value) => (
|
|
8
|
+
value !== null &&
|
|
9
|
+
typeof value === 'object' &&
|
|
10
|
+
!Array.isArray(value)
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = 'id' } = {}) => {
|
|
14
|
+
const storage = definition.storage
|
|
15
|
+
|
|
16
|
+
if (storage !== undefined && !isPlainObject(storage)) {
|
|
17
|
+
throw new Error(`Field '${fieldName}' has invalid storage metadata. Expected an object.`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const column = storage?.column ?? fieldName
|
|
21
|
+
if (typeof column !== 'string' || column.trim() === '') {
|
|
22
|
+
throw new Error(`Field '${fieldName}' has an invalid storage.column value.`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (fieldName === 'id' && column !== idColumn) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Field 'id' must use storage.column '${idColumn}' to match the resource id column.`
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (storage?.serialize !== undefined && typeof storage.serialize !== 'function') {
|
|
32
|
+
throw new Error(`Field '${fieldName}' has an invalid storage.serialize value. Expected a function.`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
column,
|
|
37
|
+
serialize: storage?.serialize || null,
|
|
38
|
+
persisted: definition.virtual !== true && definition.computed !== true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id' } = {}) => {
|
|
43
|
+
const idColumn = idProperty || 'id'
|
|
44
|
+
const fields = {}
|
|
45
|
+
const columns = {}
|
|
46
|
+
const usedColumns = new Map([[idColumn, 'id']])
|
|
47
|
+
|
|
48
|
+
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
49
|
+
const normalized = normalizeFieldStorage(fieldName, definition, { idColumn })
|
|
50
|
+
const isIdSurrogateField = fieldName === idColumn && definition.type === 'id'
|
|
51
|
+
|
|
52
|
+
if (fieldName !== 'id' && normalized.column === idColumn && !isIdSurrogateField) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Field '${fieldName}' cannot use storage.column '${idColumn}' because it is reserved for the resource id.`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const existingField = usedColumns.get(normalized.column)
|
|
59
|
+
if (existingField && existingField !== fieldName && !(isIdSurrogateField && existingField === 'id')) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Fields '${existingField}' and '${fieldName}' both map to storage column '${normalized.column}'.`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
usedColumns.set(normalized.column, fieldName)
|
|
66
|
+
fields[fieldName] = {
|
|
67
|
+
...normalized,
|
|
68
|
+
definition
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (normalized.persisted && !isIdSurrogateField) {
|
|
72
|
+
columns[normalized.column] = fieldName
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
idColumn,
|
|
78
|
+
fields,
|
|
79
|
+
columns
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const getStorageInfo = (schemaInfo = {}) => {
|
|
84
|
+
if (schemaInfo.storageInfo) {
|
|
85
|
+
return schemaInfo.storageInfo
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!schemaInfo.schemaStructure) {
|
|
89
|
+
return EMPTY_STORAGE_INFO
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return buildStorageInfo({
|
|
93
|
+
schemaStructure: schemaInfo.schemaStructure,
|
|
94
|
+
idProperty: schemaInfo.idProperty || 'id'
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const getIdColumn = (schemaInfo = {}) => {
|
|
99
|
+
return getStorageInfo(schemaInfo).idColumn || schemaInfo.idProperty || 'id'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const getStorageColumn = (schemaInfo = {}, fieldName) => {
|
|
103
|
+
if (!fieldName) return fieldName
|
|
104
|
+
if (fieldName === 'id') return getIdColumn(schemaInfo)
|
|
105
|
+
|
|
106
|
+
const fieldStorage = getStorageInfo(schemaInfo).fields[fieldName]
|
|
107
|
+
return fieldStorage?.column || fieldName
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const getLogicalFieldName = (schemaInfo = {}, columnName) => {
|
|
111
|
+
if (!columnName) return columnName
|
|
112
|
+
if (columnName === getIdColumn(schemaInfo)) return 'id'
|
|
113
|
+
|
|
114
|
+
const logicalField = getStorageInfo(schemaInfo).columns[columnName]
|
|
115
|
+
return logicalField || columnName
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const getFieldValue = (record, schemaInfo = {}, fieldName) => {
|
|
119
|
+
if (!record || !fieldName) return undefined
|
|
120
|
+
|
|
121
|
+
if (fieldName === 'id') {
|
|
122
|
+
if (Object.hasOwn(record, 'id')) {
|
|
123
|
+
return record.id
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const idColumn = getIdColumn(schemaInfo)
|
|
127
|
+
if (Object.hasOwn(record, idColumn)) {
|
|
128
|
+
return record[idColumn]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return translateRecordFromStorage(record, schemaInfo).id
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Object.hasOwn(record, fieldName)) {
|
|
135
|
+
return record[fieldName]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const columnName = getStorageColumn(schemaInfo, fieldName)
|
|
139
|
+
if (Object.hasOwn(record, columnName)) {
|
|
140
|
+
return record[columnName]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return translateRecordFromStorage(record, schemaInfo)[fieldName]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const translateAttributesForStorage = (attributes, schemaInfo = {}, options = {}) => {
|
|
147
|
+
if (!attributes) return attributes || {}
|
|
148
|
+
|
|
149
|
+
const { context = null, operation = null } = options
|
|
150
|
+
const storageInfo = getStorageInfo(schemaInfo)
|
|
151
|
+
const computed = schemaInfo.computed || {}
|
|
152
|
+
|
|
153
|
+
return Object.entries(attributes).reduce((translated, [fieldName, value]) => {
|
|
154
|
+
if (fieldName in computed) {
|
|
155
|
+
return translated
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fieldStorage = storageInfo.fields[fieldName]
|
|
159
|
+
if (fieldStorage && !fieldStorage.persisted) {
|
|
160
|
+
return translated
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const columnName = fieldName === 'id'
|
|
164
|
+
? storageInfo.idColumn
|
|
165
|
+
: fieldStorage?.column || fieldName
|
|
166
|
+
const serializedValue = fieldStorage?.serialize
|
|
167
|
+
? fieldStorage.serialize(value, {
|
|
168
|
+
fieldName,
|
|
169
|
+
columnName,
|
|
170
|
+
definition: fieldStorage.definition,
|
|
171
|
+
schemaInfo,
|
|
172
|
+
context,
|
|
173
|
+
operation
|
|
174
|
+
})
|
|
175
|
+
: value
|
|
176
|
+
|
|
177
|
+
translated[columnName] = serializedValue
|
|
178
|
+
return translated
|
|
179
|
+
}, {})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const translateRecordFromStorage = (record, schemaInfo = {}) => {
|
|
183
|
+
if (!record || !schemaInfo) return record
|
|
184
|
+
|
|
185
|
+
const storageInfo = getStorageInfo(schemaInfo)
|
|
186
|
+
const translated = {}
|
|
187
|
+
|
|
188
|
+
for (const [columnName, value] of Object.entries(record)) {
|
|
189
|
+
if (columnName === storageInfo.idColumn) {
|
|
190
|
+
translated.id = value
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const logicalField = storageInfo.columns[columnName]
|
|
195
|
+
if (logicalField) {
|
|
196
|
+
translated[logicalField] = value
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
translated[columnName] = value
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return translated
|
|
204
|
+
}
|