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,8 +1,8 @@
|
|
|
1
|
-
import { RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { validateQueryPayload } from '../lib/querying-writing/payload-validators.js'
|
|
3
1
|
import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
|
|
4
2
|
import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
|
|
3
|
+
import { getEffectiveSortableFields } from '../lib/querying/query-field-sort-helpers.js'
|
|
5
4
|
import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
|
|
5
|
+
import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
|
|
6
6
|
import { cascadeConfig } from './common.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -61,66 +61,28 @@ export default async function queryMethod ({
|
|
|
61
61
|
context.scopeName = scopeName
|
|
62
62
|
|
|
63
63
|
// These are just shortcuts used in this function and will be returned
|
|
64
|
-
const searchSchema = context.schemaInfo.searchSchemaInstance
|
|
65
64
|
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
66
65
|
const schemaRelationships = context.schemaInfo.schemaRelationships
|
|
67
66
|
|
|
68
67
|
// Sortable fields and sort (mab)
|
|
69
|
-
context.sortableFields = vars
|
|
68
|
+
context.sortableFields = getEffectiveSortableFields(vars)
|
|
70
69
|
// Apply default sort if no sort specified
|
|
71
70
|
if (context.queryParams.sort.length === 0 && vars.defaultSort) {
|
|
72
71
|
context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort]
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`Filtering is not enabled for resource '${scopeName}'. To enable filtering, add 'search: true' to schema fields or define a searchSchema.`,
|
|
88
|
-
{
|
|
89
|
-
fields: Object.keys(context.queryParams.filters).map(field => `filters.${field}`),
|
|
90
|
-
violations: [{
|
|
91
|
-
field: 'filters',
|
|
92
|
-
rule: 'filtering_not_enabled',
|
|
93
|
-
message: 'Resource does not have searchable fields defined'
|
|
94
|
-
}]
|
|
95
|
-
}
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Validate the filter parameters searchSchema
|
|
100
|
-
const { validatedObject, errors } = await searchSchema.validate(context.queryParams.filters, {
|
|
101
|
-
onlyObjectValues: true // Partial validation for filters
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// If there are validation errors, throw an error
|
|
105
|
-
if (Object.keys(errors).length > 0) {
|
|
106
|
-
const violations = Object.entries(errors).map(([field, error]) => ({
|
|
107
|
-
field: `filters.${field}`,
|
|
108
|
-
rule: error.code || 'invalid_value',
|
|
109
|
-
message: error.message
|
|
110
|
-
}))
|
|
111
|
-
|
|
112
|
-
throw new RestApiValidationError(
|
|
113
|
-
'Invalid filter parameters',
|
|
114
|
-
{
|
|
115
|
-
fields: Object.keys(errors).map(field => `filters.${field}`),
|
|
116
|
-
violations
|
|
117
|
-
}
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Replace filter with validated/transformed values
|
|
122
|
-
context.queryParams.filters = validatedObject
|
|
123
|
-
}
|
|
74
|
+
const requestContracts = getRequestContracts({
|
|
75
|
+
scopeName,
|
|
76
|
+
schemaInfo: context.schemaInfo,
|
|
77
|
+
includeDepthLimit: vars.includeDepthLimit,
|
|
78
|
+
sortableFields: context.sortableFields
|
|
79
|
+
})
|
|
80
|
+
const validatedRequest = validateRequestContractOrThrow(
|
|
81
|
+
requestContracts.query,
|
|
82
|
+
{ queryParams: context.queryParams },
|
|
83
|
+
'Query parameters are invalid'
|
|
84
|
+
)
|
|
85
|
+
context.queryParams = validatedRequest.queryParams || {}
|
|
124
86
|
|
|
125
87
|
// Centralised checkPermissions function
|
|
126
88
|
await scope.checkPermissions({
|
|
@@ -16,6 +16,7 @@ import patchMethod from './rest-api-plugin-methods/patch.js'
|
|
|
16
16
|
import deleteMethod from './rest-api-plugin-methods/delete.js'
|
|
17
17
|
import enrichAttributesMethod from './rest-api-plugin-methods/enrich-attributes.js'
|
|
18
18
|
import checkPermissionsMethod from './rest-api-plugin-methods/check-permissions.js'
|
|
19
|
+
import applyQueryFiltersMethod from './rest-api-plugin-methods/apply-query-filters.js'
|
|
19
20
|
import addRouteMethod from './rest-api-plugin-methods/add-route.js'
|
|
20
21
|
import releaseMethod from './rest-api-plugin-methods/release.js'
|
|
21
22
|
import { defaultDataHelpers } from './lib/querying-writing/default-data-helpers.js'
|
|
@@ -148,6 +149,7 @@ export const RestApiPlugin = {
|
|
|
148
149
|
// Non-URL methods
|
|
149
150
|
addScopeMethod('enrichAttributes', enrichAttributesMethod)
|
|
150
151
|
addScopeMethod('checkPermissions', checkPermissionsMethod)
|
|
152
|
+
addScopeMethod('applyQueryFilters', applyQueryFiltersMethod)
|
|
151
153
|
|
|
152
154
|
// *********
|
|
153
155
|
// Helpers
|
|
@@ -27,7 +27,7 @@ export const PositioningPlugin = {
|
|
|
27
27
|
// Get configuration - hooked-api namespaces options by plugin name
|
|
28
28
|
const positioningOptions = pluginOptions || {}
|
|
29
29
|
|
|
30
|
-
// Store configuration in vars (data only)
|
|
30
|
+
// Store configuration in vars (data only) so hooks can use it without re-reading plugin options.
|
|
31
31
|
vars.positioning = {
|
|
32
32
|
field: positioningOptions.field || 'position',
|
|
33
33
|
filters: positioningOptions.filters || [],
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export const QueryProjectionsPlugin = {
|
|
2
|
+
name: 'query-projections',
|
|
3
|
+
dependencies: ['rest-api'],
|
|
4
|
+
|
|
5
|
+
install ({ addHook, log }) {
|
|
6
|
+
const normalizeQueryFields = ({ scopeName, scopeOptions = {} }) => {
|
|
7
|
+
const schema = scopeOptions.schema || {}
|
|
8
|
+
const relationships = scopeOptions.relationships || {}
|
|
9
|
+
const rawQueryFields = scopeOptions.queryFields || {}
|
|
10
|
+
const normalized = {}
|
|
11
|
+
const reservedNames = new Set(Object.keys(schema))
|
|
12
|
+
|
|
13
|
+
if (!rawQueryFields || typeof rawQueryFields !== 'object' || Array.isArray(rawQueryFields)) {
|
|
14
|
+
throw new Error(`Invalid queryFields configuration for scope '${scopeName}'. Expected an object.`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const relName of Object.keys(relationships)) {
|
|
18
|
+
reservedNames.add(relName)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const fieldDef of Object.values(schema)) {
|
|
22
|
+
if (fieldDef?.belongsTo && fieldDef?.as) {
|
|
23
|
+
reservedNames.add(fieldDef.as)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const [fieldName, fieldDef] of Object.entries(rawQueryFields)) {
|
|
28
|
+
if (!fieldDef || typeof fieldDef !== 'object' || Array.isArray(fieldDef)) {
|
|
29
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must be an object.`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (reservedNames.has(fieldName)) {
|
|
33
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' conflicts with an existing schema or relationship name.`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!fieldDef.type) {
|
|
37
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must have a type.`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const select = fieldDef.select || fieldDef.project
|
|
41
|
+
if (typeof select !== 'function') {
|
|
42
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must define a select() function.`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
normalized[fieldName] = {
|
|
46
|
+
...fieldDef,
|
|
47
|
+
select,
|
|
48
|
+
sortable: fieldDef.sortable === true,
|
|
49
|
+
hidden: fieldDef.hidden === true,
|
|
50
|
+
normallyHidden: fieldDef.normallyHidden === true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return normalized
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const stripQueryFieldInput = ({ context, scopeName, scope, scopes }) => {
|
|
58
|
+
const inputAttributes = context?.inputRecord?.data?.attributes
|
|
59
|
+
const queryFields = scopes?.[scopeName]?.vars?.queryFields || scope?.vars?.queryFields || {}
|
|
60
|
+
|
|
61
|
+
if (!inputAttributes || typeof inputAttributes !== 'object' || Array.isArray(inputAttributes)) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const stripped = []
|
|
66
|
+
for (const fieldName of Object.keys(queryFields)) {
|
|
67
|
+
if (Object.hasOwn(inputAttributes, fieldName)) {
|
|
68
|
+
delete inputAttributes[fieldName]
|
|
69
|
+
stripped.push(fieldName)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (stripped.length > 0) {
|
|
74
|
+
const message = `Query fields [${stripped.join(', ')}] were sent in input for resource '${scopeName}' but will be ignored as they are query output only`
|
|
75
|
+
if (typeof log?.warn === 'function') {
|
|
76
|
+
log.warn(message)
|
|
77
|
+
} else {
|
|
78
|
+
console.warn(message)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const validateCompiledQueryFieldNamespace = ({ scopeName, scope, vars, scopes }) => {
|
|
84
|
+
const activeScope = scope || scopes?.[scopeName]
|
|
85
|
+
const scopeVars = vars || activeScope?.vars || {}
|
|
86
|
+
const queryFields = scopeVars.queryFields || {}
|
|
87
|
+
const schemaInfo = activeScope?.vars?.schemaInfo || scopeVars.schemaInfo || {}
|
|
88
|
+
|
|
89
|
+
if (Object.keys(queryFields).length === 0) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const reservedNames = new Set([
|
|
94
|
+
...Object.keys(schemaInfo.schemaStructure || {}),
|
|
95
|
+
...Object.keys(schemaInfo.computed || {}),
|
|
96
|
+
...Object.keys(schemaInfo.schemaRelationships || {})
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
for (const fieldDef of Object.values(schemaInfo.schemaStructure || {})) {
|
|
100
|
+
if (fieldDef?.belongsTo && fieldDef?.as) {
|
|
101
|
+
reservedNames.add(fieldDef.as)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const fieldName of Object.keys(queryFields)) {
|
|
106
|
+
if (reservedNames.has(fieldName)) {
|
|
107
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' conflicts with an existing schema, computed, or relationship name.`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
addHook('scope:added', 'compile-query-projections', { afterFunction: 'turnScopeInitIntoVars' }, ({ context, scopes }) => {
|
|
113
|
+
const scopeName = context.scopeName
|
|
114
|
+
const scope = scopes[scopeName]
|
|
115
|
+
const vars = scope?.vars || {}
|
|
116
|
+
const scopeOptions = scope?.scopeOptions || context.scopeOptions || {}
|
|
117
|
+
const queryFields = normalizeQueryFields({ scopeName, scopeOptions })
|
|
118
|
+
|
|
119
|
+
vars.queryFields = queryFields
|
|
120
|
+
validateCompiledQueryFieldNamespace({ scopeName, scope, vars, scopes })
|
|
121
|
+
|
|
122
|
+
log.debug(`Compiled query fields for '${scopeName}'`, {
|
|
123
|
+
queryFields: Object.keys(queryFields),
|
|
124
|
+
sortable: Object.keys(queryFields).filter((fieldName) => queryFields[fieldName].sortable === true)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
addHook('beforeSchemaValidate', 'strip-query-field-input', {}, stripQueryFieldInput)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -206,9 +206,7 @@ async function registerSubscription ({
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
const { validatedObject, errors } = await searchSchemaInstance.
|
|
210
|
-
onlyObjectValues: true
|
|
211
|
-
})
|
|
209
|
+
const { validatedObject, errors } = await searchSchemaInstance.patch(validatedFilters)
|
|
212
210
|
|
|
213
211
|
if (errors && Object.keys(errors).length > 0) {
|
|
214
212
|
const error = new Error('Invalid filter values')
|