json-rest-api 1.0.14 → 1.0.16
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { requirePackage } from 'hooked-api'
|
|
2
|
-
import { createSchema }
|
|
3
|
-
import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js'
|
|
4
|
-
import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js'
|
|
5
|
-
import { getForeignKeyFields } from './lib/querying-writing/field-utils.js'
|
|
6
|
-
import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js'
|
|
7
|
-
import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
|
|
8
|
-
import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
|
|
9
|
-
import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
|
|
10
|
-
import { processIncludes } from './lib/querying/knex-process-includes.js'
|
|
11
|
-
import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js'
|
|
1
|
+
import { requirePackage } from 'hooked-api'
|
|
2
|
+
import { createSchema } from 'json-rest-schema'
|
|
3
|
+
import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js'
|
|
4
|
+
import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js'
|
|
5
|
+
import { getForeignKeyFields } from './lib/querying-writing/field-utils.js'
|
|
6
|
+
import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js'
|
|
7
|
+
import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
|
|
8
|
+
import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
|
|
9
|
+
import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
|
|
10
|
+
import { processIncludes } from './lib/querying/knex-process-includes.js'
|
|
11
|
+
import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js'
|
|
12
12
|
import {
|
|
13
13
|
polymorphicFiltersHook,
|
|
14
14
|
crossTableFiltersHook,
|
|
15
15
|
basicFiltersHook
|
|
16
|
-
} from './lib/querying/knex-query-helpers.js'
|
|
17
|
-
import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js'
|
|
18
|
-
import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js'
|
|
19
|
-
import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js'
|
|
20
|
-
import {
|
|
21
|
-
calculatePaginationMeta,
|
|
16
|
+
} from './lib/querying/knex-query-helpers.js'
|
|
17
|
+
import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js'
|
|
18
|
+
import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js'
|
|
19
|
+
import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js'
|
|
20
|
+
import {
|
|
21
|
+
calculatePaginationMeta,
|
|
22
22
|
generatePaginationLinks,
|
|
23
23
|
generateCursorPaginationLinks,
|
|
24
24
|
buildCursorMeta,
|
|
25
25
|
parseCursor
|
|
26
|
-
} from './lib/querying/knex-pagination-helpers.js'
|
|
27
|
-
import { getUrlPrefix } from './lib/querying/url-helpers.js'
|
|
28
|
-
import { createStorageAdapter } from './lib/storage/storage-adapter.js'
|
|
26
|
+
} from './lib/querying/knex-pagination-helpers.js'
|
|
27
|
+
import { getUrlPrefix } from './lib/querying/url-helpers.js'
|
|
28
|
+
import { createStorageAdapter } from './lib/storage/storage-adapter.js'
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Strips non-database fields (computed and virtual) from attributes before database operations
|
|
@@ -34,184 +34,179 @@ import { createStorageAdapter } from './lib/storage/storage-adapter.js';
|
|
|
34
34
|
* @returns {Object} Attributes with computed and virtual fields removed
|
|
35
35
|
*/
|
|
36
36
|
const stripNonDatabaseFields = (attributes, schemaInfo) => {
|
|
37
|
-
if (!attributes || !schemaInfo) return attributes || {}
|
|
38
|
-
|
|
39
|
-
const { computed = {}, schemaStructure = {} } = schemaInfo
|
|
37
|
+
if (!attributes || !schemaInfo) return attributes || {}
|
|
38
|
+
|
|
39
|
+
const { computed = {}, schemaStructure = {} } = schemaInfo
|
|
40
40
|
return Object.entries(attributes)
|
|
41
41
|
.filter(([key]) => {
|
|
42
42
|
// Remove computed fields
|
|
43
|
-
if (key in computed) return false
|
|
43
|
+
if (key in computed) return false
|
|
44
44
|
// Remove fields marked as virtual in the schema
|
|
45
|
-
const fieldDef = schemaStructure[key]
|
|
45
|
+
const fieldDef = schemaStructure[key]
|
|
46
46
|
if (fieldDef && fieldDef.virtual === true) {
|
|
47
|
-
return false
|
|
47
|
+
return false
|
|
48
48
|
}
|
|
49
|
-
return true
|
|
49
|
+
return true
|
|
50
50
|
})
|
|
51
|
-
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
|
|
52
|
-
}
|
|
53
|
-
|
|
51
|
+
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
|
|
52
|
+
}
|
|
54
53
|
|
|
55
54
|
export const RestApiKnexPlugin = {
|
|
56
55
|
name: 'rest-api-knex',
|
|
57
56
|
dependencies: ['rest-api'],
|
|
58
57
|
|
|
59
|
-
async install({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
|
|
58
|
+
async install ({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
|
|
60
59
|
// Try to import knex dynamically
|
|
61
|
-
let knexLib
|
|
60
|
+
let knexLib
|
|
62
61
|
try {
|
|
63
|
-
knexLib = await import('knex')
|
|
62
|
+
knexLib = await import('knex')
|
|
64
63
|
} catch (e) {
|
|
65
|
-
requirePackage('knex', 'rest-api-knex',
|
|
66
|
-
'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
|
|
64
|
+
requirePackage('knex', 'rest-api-knex',
|
|
65
|
+
'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
|
|
67
66
|
}
|
|
68
|
-
|
|
67
|
+
|
|
69
68
|
// Get Knex instance from plugin options
|
|
70
|
-
const knexOptions = pluginOptions || {}
|
|
71
|
-
const knex = knexOptions.knex
|
|
72
|
-
|
|
69
|
+
const knexOptions = pluginOptions || {}
|
|
70
|
+
const knex = knexOptions.knex
|
|
71
|
+
|
|
73
72
|
// Expose Knex instance and helpers in a structured way
|
|
74
73
|
api.knex = {
|
|
75
74
|
instance: knex,
|
|
76
75
|
helpers: {}
|
|
77
|
-
}
|
|
76
|
+
}
|
|
78
77
|
|
|
79
|
-
const storageAdapters = new Map()
|
|
78
|
+
const storageAdapters = new Map()
|
|
80
79
|
|
|
81
80
|
const getScopeStorageAdapter = (scopeName) => {
|
|
82
|
-
if (!scopeName) return null
|
|
83
|
-
const resource = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
84
|
-
const schemaInfo = resource?.vars?.schemaInfo
|
|
85
|
-
if (!schemaInfo) return null
|
|
81
|
+
if (!scopeName) return null
|
|
82
|
+
const resource = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
83
|
+
const schemaInfo = resource?.vars?.schemaInfo
|
|
84
|
+
if (!schemaInfo) return null
|
|
86
85
|
|
|
87
|
-
const cached = storageAdapters.get(scopeName)
|
|
86
|
+
const cached = storageAdapters.get(scopeName)
|
|
88
87
|
if (cached && cached.schemaInfo === schemaInfo) {
|
|
89
|
-
return cached.adapter
|
|
88
|
+
return cached.adapter
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
const adapter = createStorageAdapter({ knex, schemaInfo })
|
|
93
|
-
storageAdapters.set(scopeName, { adapter, schemaInfo })
|
|
91
|
+
const adapter = createStorageAdapter({ knex, schemaInfo })
|
|
92
|
+
storageAdapters.set(scopeName, { adapter, schemaInfo })
|
|
94
93
|
if (resource?.vars) {
|
|
95
|
-
resource.vars.storageAdapter = adapter
|
|
94
|
+
resource.vars.storageAdapter = adapter
|
|
96
95
|
}
|
|
97
|
-
return adapter
|
|
98
|
-
}
|
|
96
|
+
return adapter
|
|
97
|
+
}
|
|
99
98
|
|
|
100
|
-
api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
|
|
101
|
-
helpers.getStorageAdapter = getScopeStorageAdapter
|
|
99
|
+
api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
|
|
100
|
+
helpers.getStorageAdapter = getScopeStorageAdapter
|
|
102
101
|
|
|
103
102
|
const createSelectTranslator = (adapter) => {
|
|
104
|
-
if (!adapter) return null
|
|
103
|
+
if (!adapter) return null
|
|
105
104
|
return (field, alias) => {
|
|
106
105
|
if (field === '*') {
|
|
107
|
-
return alias ? `${alias}.*` : '*'
|
|
106
|
+
return alias ? `${alias}.*` : '*'
|
|
108
107
|
}
|
|
109
|
-
const translated = adapter.translateColumn(field)
|
|
110
|
-
const result = translated || field
|
|
108
|
+
const translated = adapter.translateColumn(field)
|
|
109
|
+
const result = translated || field
|
|
111
110
|
if (!alias) {
|
|
112
|
-
return result
|
|
111
|
+
return result
|
|
113
112
|
}
|
|
114
113
|
if (result.includes('.')) {
|
|
115
|
-
return result
|
|
114
|
+
return result
|
|
116
115
|
}
|
|
117
|
-
return `${alias}.${result}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
116
|
+
return `${alias}.${result}`
|
|
117
|
+
}
|
|
118
|
+
}
|
|
120
119
|
|
|
121
120
|
// Check database capabilities
|
|
122
|
-
const hasWindowFunctions = await supportsWindowFunctions(knex)
|
|
123
|
-
const dbInfo = await getDatabaseInfo(knex)
|
|
124
|
-
|
|
121
|
+
const hasWindowFunctions = await supportsWindowFunctions(knex)
|
|
122
|
+
const dbInfo = await getDatabaseInfo(knex)
|
|
123
|
+
|
|
125
124
|
// Store capabilities in API instance for access throughout
|
|
126
125
|
api.knex.capabilities = {
|
|
127
126
|
windowFunctions: hasWindowFunctions,
|
|
128
127
|
dbInfo
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
log.info(
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
log.info('Database capabilities detected:', {
|
|
132
131
|
database: dbInfo.client,
|
|
133
132
|
version: dbInfo.version,
|
|
134
133
|
windowFunctions: hasWindowFunctions
|
|
135
|
-
})
|
|
134
|
+
})
|
|
136
135
|
|
|
137
136
|
// Cross-table search functions are now imported directly and used with full signatures
|
|
138
|
-
|
|
137
|
+
|
|
139
138
|
/* ╔═════════════════════════════════════════════════════════════════════╗
|
|
140
139
|
* ║ MAIN QUERY FILTERING HOOK ║
|
|
141
140
|
* ║ This is the heart of the filtering system. It processes searchSchema ║
|
|
142
141
|
* ║ filters and builds SQL WHERE conditions with proper JOINs ║
|
|
143
142
|
* ╚═════════════════════════════════════════════════════════════════════╝ */
|
|
144
|
-
|
|
143
|
+
|
|
145
144
|
// Register the three separate filter hooks
|
|
146
145
|
// Dependencies object for the hooks
|
|
147
|
-
const polymorphicFiltersHookParams
|
|
148
|
-
|
|
146
|
+
const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
147
|
+
|
|
149
148
|
// Register in specific order: polymorphic → cross-table → basic
|
|
150
149
|
// This ensures proper field qualification when JOINs are present
|
|
151
|
-
|
|
150
|
+
|
|
152
151
|
// 1. Polymorphic filters (adds JOINs for polymorphic relationships)
|
|
153
|
-
addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
|
|
152
|
+
addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
|
|
154
153
|
async (hookParams) => polymorphicFiltersHook(hookParams, polymorphicFiltersHookParams)
|
|
155
|
-
)
|
|
156
|
-
|
|
154
|
+
)
|
|
155
|
+
|
|
157
156
|
// 2. Cross-table filters (adds JOINs for cross-table fields)
|
|
158
|
-
addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
|
|
157
|
+
addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
|
|
159
158
|
async (hookParams) => crossTableFiltersHook(hookParams, polymorphicFiltersHookParams)
|
|
160
|
-
)
|
|
161
|
-
|
|
159
|
+
)
|
|
160
|
+
|
|
162
161
|
// 3. Basic filters (processes simple main table filters)
|
|
163
|
-
addHook('knexQueryFiltering', 'basicFiltersHook', {},
|
|
162
|
+
addHook('knexQueryFiltering', 'basicFiltersHook', {},
|
|
164
163
|
async (hookParams) => basicFiltersHook(hookParams, polymorphicFiltersHookParams)
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
// 3. Basic filters (processes simple main table filters)
|
|
168
|
-
addHook('release', 'releaseHook', {},
|
|
169
|
-
async ({ api }) => api.knex.instance.destroy()
|
|
170
|
-
);
|
|
164
|
+
)
|
|
171
165
|
|
|
166
|
+
// 3. Basic filters (processes simple main table filters)
|
|
167
|
+
addHook('release', 'releaseHook', {},
|
|
168
|
+
async ({ api }) => api.knex.instance.destroy()
|
|
169
|
+
)
|
|
172
170
|
|
|
173
171
|
// Helper scope method to get all schema-related information
|
|
174
172
|
addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
|
|
175
173
|
// Create a filtered schema that excludes virtual fields
|
|
176
|
-
const schemaStructure = vars.schemaInfo.schemaStructure || {}
|
|
177
|
-
const filteredSchema = {}
|
|
178
|
-
|
|
174
|
+
const schemaStructure = vars.schemaInfo.schemaStructure || {}
|
|
175
|
+
const filteredSchema = {}
|
|
176
|
+
|
|
179
177
|
// Copy only non-virtual fields to the filtered schema
|
|
180
178
|
Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
181
179
|
if (!fieldDef.virtual) {
|
|
182
|
-
filteredSchema[fieldName] = fieldDef
|
|
180
|
+
filteredSchema[fieldName] = fieldDef
|
|
183
181
|
}
|
|
184
|
-
})
|
|
185
|
-
|
|
182
|
+
})
|
|
183
|
+
|
|
186
184
|
// Create schema object from filtered fields
|
|
187
|
-
const tableSchemaInstance = createSchema(filteredSchema)
|
|
188
|
-
|
|
185
|
+
const tableSchemaInstance = createSchema(filteredSchema)
|
|
186
|
+
|
|
189
187
|
await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
|
|
190
188
|
})
|
|
191
189
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
|
|
190
|
+
// Helper scope method to alter existing fields in a table
|
|
191
|
+
addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
|
|
195
192
|
// Validate required parameters
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
if (!params.fields || typeof params.fields !== 'object') {
|
|
194
|
+
throw new Error('fields parameter is required for alterKnexFields')
|
|
195
|
+
}
|
|
199
196
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
197
|
+
await alterKnexFields(
|
|
198
|
+
api.knex.instance,
|
|
199
|
+
vars.schemaInfo.tableName,
|
|
200
|
+
params.fields,
|
|
201
|
+
params.options // Pass through any additional options
|
|
202
|
+
)
|
|
203
|
+
})
|
|
207
204
|
|
|
208
|
-
|
|
209
205
|
// Helper scope method to add a field to an existing table
|
|
210
206
|
addScopeMethod('addKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
|
|
211
|
-
|
|
212
207
|
// Create schema object from filtered fields
|
|
213
|
-
const partialTableSchema = createSchema(params.fields)
|
|
214
|
-
|
|
208
|
+
const partialTableSchema = createSchema(params.fields)
|
|
209
|
+
|
|
215
210
|
await addKnexFields(api.knex.instance, vars.schemaInfo.tableName, partialTableSchema)
|
|
216
211
|
})
|
|
217
212
|
|
|
@@ -226,7 +221,7 @@ export const RestApiKnexPlugin = {
|
|
|
226
221
|
|
|
227
222
|
/**
|
|
228
223
|
* Checks if a resource exists in the database
|
|
229
|
-
*
|
|
224
|
+
*
|
|
230
225
|
* @param {Object} params - The parameters object
|
|
231
226
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
232
227
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -238,32 +233,32 @@ export const RestApiKnexPlugin = {
|
|
|
238
233
|
* @returns {Promise<boolean>} True if the resource exists, false otherwise
|
|
239
234
|
*/
|
|
240
235
|
helpers.dataExists = async ({ scopeName, context }) => {
|
|
241
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
236
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
242
237
|
if (storageAdapter) {
|
|
243
|
-
context.storageAdapter = storageAdapter
|
|
238
|
+
context.storageAdapter = storageAdapter
|
|
244
239
|
}
|
|
245
240
|
|
|
246
|
-
const id = context.id
|
|
241
|
+
const id = context.id
|
|
242
|
+
|
|
243
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
244
|
+
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
245
|
+
const db = context.db || api.knex.instance
|
|
246
|
+
|
|
247
|
+
log.debug(`[Knex] EXISTS ${tableName}/${id}`)
|
|
248
|
+
|
|
249
|
+
const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
|
|
247
250
|
|
|
248
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName;
|
|
249
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty;
|
|
250
|
-
const db = context.db || api.knex.instance;
|
|
251
|
-
|
|
252
|
-
log.debug(`[Knex] EXISTS ${tableName}/${id}`);
|
|
253
|
-
|
|
254
|
-
const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id';
|
|
255
|
-
|
|
256
251
|
const record = await db(tableName)
|
|
257
252
|
.where(idProperty, id)
|
|
258
253
|
.select(selectClause)
|
|
259
|
-
.first()
|
|
260
|
-
|
|
261
|
-
return !!record
|
|
262
|
-
}
|
|
254
|
+
.first()
|
|
255
|
+
|
|
256
|
+
return !!record
|
|
257
|
+
}
|
|
263
258
|
|
|
264
259
|
/**
|
|
265
260
|
* Retrieves a single resource by ID with support for sparse fieldsets and includes
|
|
266
|
-
*
|
|
261
|
+
*
|
|
267
262
|
* @param {Object} params - The parameters object
|
|
268
263
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
269
264
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -281,89 +276,89 @@ export const RestApiKnexPlugin = {
|
|
|
281
276
|
* @throws {RestApiResourceError} When the resource is not found
|
|
282
277
|
*/
|
|
283
278
|
helpers.dataGet = async ({ scopeName, context, runHooks }) => {
|
|
284
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
279
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
285
280
|
if (storageAdapter) {
|
|
286
|
-
context.storageAdapter = storageAdapter
|
|
281
|
+
context.storageAdapter = storageAdapter
|
|
287
282
|
}
|
|
288
|
-
const scope = api.resources[scopeName]
|
|
283
|
+
const scope = api.resources[scopeName]
|
|
289
284
|
if (!scope) {
|
|
290
|
-
log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) })
|
|
291
|
-
throw new Error(`Scope '${scopeName}' not found in api.resources`)
|
|
285
|
+
log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) })
|
|
286
|
+
throw new Error(`Scope '${scopeName}' not found in api.resources`)
|
|
292
287
|
}
|
|
293
288
|
if (!scope.scopeName && !scope.name) {
|
|
294
|
-
log.debug('[DATA-GET] Scope structure:', {
|
|
289
|
+
log.debug('[DATA-GET] Scope structure:', {
|
|
295
290
|
scopeKeys: Object.keys(scope),
|
|
296
291
|
scopeName,
|
|
297
292
|
hasVars: !!scope.vars,
|
|
298
293
|
varKeys: scope.vars ? Object.keys(scope.vars) : []
|
|
299
|
-
})
|
|
294
|
+
})
|
|
300
295
|
}
|
|
301
|
-
const id = context.id
|
|
302
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
303
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
304
|
-
const db = context.db || api.knex.instance
|
|
305
|
-
|
|
306
|
-
log.debug(`[Knex] GET ${tableName}/${id}`)
|
|
307
|
-
|
|
296
|
+
const id = context.id
|
|
297
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
298
|
+
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
299
|
+
const db = context.db || api.knex.instance
|
|
300
|
+
|
|
301
|
+
log.debug(`[Knex] GET ${tableName}/${id}`)
|
|
302
|
+
|
|
308
303
|
// Build field selection for sparse fieldsets
|
|
309
304
|
// This determines which fields to SELECT from database
|
|
310
305
|
// and tracks dependencies needed for computed fields
|
|
311
306
|
const fieldSelectionInfo = await buildFieldSelection(
|
|
312
307
|
scope,
|
|
313
308
|
{ context }
|
|
314
|
-
)
|
|
315
|
-
|
|
309
|
+
)
|
|
310
|
+
|
|
316
311
|
// Store dependency info in context for enrichAttributes
|
|
317
312
|
// Example: If user requests 'profit_margin' (computed), this might contain ['cost']
|
|
318
313
|
// The REST API plugin will use this to remove 'cost' from response if not requested
|
|
319
|
-
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
320
|
-
|
|
314
|
+
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
315
|
+
|
|
321
316
|
// Build query - no filtering hooks for single records
|
|
322
317
|
// Permission checks will handle access control
|
|
323
|
-
let query = db(tableName).where(idProperty, id)
|
|
324
|
-
|
|
318
|
+
let query = db(tableName).where(idProperty, id)
|
|
319
|
+
|
|
325
320
|
// Apply field selection
|
|
326
|
-
const selectTranslator = createSelectTranslator(storageAdapter)
|
|
321
|
+
const selectTranslator = createSelectTranslator(storageAdapter)
|
|
327
322
|
query = buildQuerySelection(
|
|
328
323
|
query,
|
|
329
324
|
tableName,
|
|
330
325
|
fieldSelectionInfo.fieldsToSelect,
|
|
331
326
|
false,
|
|
332
|
-
selectTranslator ? { translateColumn: selectTranslator } : undefined
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
const record = await query.first()
|
|
336
|
-
|
|
327
|
+
selectTranslator ? { translateColumn: selectTranslator } : undefined
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
const record = await query.first()
|
|
331
|
+
|
|
337
332
|
if (!record) {
|
|
338
333
|
throw new RestApiResourceError(
|
|
339
|
-
|
|
340
|
-
{
|
|
334
|
+
'Resource not found',
|
|
335
|
+
{
|
|
341
336
|
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
342
337
|
resourceType: scopeName,
|
|
343
338
|
resourceId: id
|
|
344
339
|
}
|
|
345
|
-
)
|
|
340
|
+
)
|
|
346
341
|
}
|
|
347
|
-
|
|
342
|
+
|
|
348
343
|
// Load relationship identifiers for all hasMany relationships
|
|
349
|
-
const records = [record]
|
|
350
|
-
await loadRelationshipIdentifiers(records, scopeName, scopes, db)
|
|
351
|
-
|
|
344
|
+
const records = [record] // Wrap in array for processing
|
|
345
|
+
await loadRelationshipIdentifiers(records, scopeName, scopes, db)
|
|
346
|
+
|
|
352
347
|
// Process includes
|
|
353
348
|
const included = await processIncludes(scope, records, {
|
|
354
349
|
log,
|
|
355
350
|
scopes,
|
|
356
351
|
knex,
|
|
357
352
|
context
|
|
358
|
-
})
|
|
359
|
-
|
|
353
|
+
})
|
|
354
|
+
|
|
360
355
|
// Build and return response
|
|
361
|
-
return buildJsonApiResponse(scope, records, included, true, scopeName, context)
|
|
362
|
-
}
|
|
356
|
+
return buildJsonApiResponse(scope, records, included, true, scopeName, context)
|
|
357
|
+
}
|
|
363
358
|
|
|
364
359
|
/**
|
|
365
360
|
* Retrieves a single resource by ID with minimal processing (no includes or sparse fieldsets)
|
|
366
|
-
*
|
|
361
|
+
*
|
|
367
362
|
* @param {Object} params - The parameters object
|
|
368
363
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
369
364
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -375,41 +370,41 @@ export const RestApiKnexPlugin = {
|
|
|
375
370
|
* @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
|
|
376
371
|
*/
|
|
377
372
|
helpers.dataGetMinimal = async ({ scopeName, context }) => {
|
|
378
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
373
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
379
374
|
if (storageAdapter) {
|
|
380
|
-
context.storageAdapter = storageAdapter
|
|
375
|
+
context.storageAdapter = storageAdapter
|
|
381
376
|
}
|
|
382
|
-
const scope = api.resources[scopeName]
|
|
383
|
-
const id = context.id
|
|
384
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
385
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
386
|
-
const db = context.db || api.knex.instance
|
|
387
|
-
|
|
388
|
-
log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
|
|
389
|
-
|
|
377
|
+
const scope = api.resources[scopeName]
|
|
378
|
+
const id = context.id
|
|
379
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
380
|
+
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
381
|
+
const db = context.db || api.knex.instance
|
|
382
|
+
|
|
383
|
+
log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
|
|
384
|
+
|
|
390
385
|
// Build query - no filtering hooks for single records
|
|
391
386
|
// Permission checks will handle access control
|
|
392
|
-
let query = db(tableName).where(idProperty, id)
|
|
393
|
-
|
|
387
|
+
let query = db(tableName).where(idProperty, id)
|
|
388
|
+
|
|
394
389
|
// Add alias if idProperty is not 'id'
|
|
395
390
|
if (idProperty !== 'id') {
|
|
396
|
-
query = query.select('*', `${idProperty} as id`)
|
|
391
|
+
query = query.select('*', `${idProperty} as id`)
|
|
397
392
|
}
|
|
398
|
-
|
|
393
|
+
|
|
399
394
|
// Execute query
|
|
400
|
-
const record = await query.first()
|
|
401
|
-
|
|
395
|
+
const record = await query.first()
|
|
396
|
+
|
|
402
397
|
if (!record) {
|
|
403
|
-
return null
|
|
398
|
+
return null
|
|
404
399
|
}
|
|
405
|
-
|
|
400
|
+
|
|
406
401
|
// Transform to JSON:API format with belongsTo relationships
|
|
407
|
-
return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
|
|
408
|
-
}
|
|
402
|
+
return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
|
|
403
|
+
}
|
|
409
404
|
|
|
410
405
|
/**
|
|
411
406
|
* Queries resources with support for filtering, sorting, pagination, sparse fieldsets, and includes
|
|
412
|
-
*
|
|
407
|
+
*
|
|
413
408
|
* @param {Object} params - The parameters object
|
|
414
409
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
415
410
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -436,61 +431,61 @@ export const RestApiKnexPlugin = {
|
|
|
436
431
|
* @returns {Promise<Object>} JSON:API formatted response with data array, optional included resources, and pagination meta/links
|
|
437
432
|
*/
|
|
438
433
|
helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
|
|
439
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
434
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
440
435
|
if (storageAdapter) {
|
|
441
|
-
context.storageAdapter = storageAdapter
|
|
436
|
+
context.storageAdapter = storageAdapter
|
|
442
437
|
}
|
|
443
|
-
const scope = api.resources[scopeName]
|
|
444
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
445
|
-
const schemaInfo = context.schemaInfo
|
|
446
|
-
const queryParams = context.queryParams
|
|
447
|
-
const db = context.db || api.knex.instance
|
|
448
|
-
const sortableFields = context.sortableFields
|
|
449
|
-
|
|
450
|
-
log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
|
|
451
|
-
log.debug(`[Knex] QUERY ${tableName}`, queryParams)
|
|
452
|
-
|
|
438
|
+
const scope = api.resources[scopeName]
|
|
439
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
440
|
+
const schemaInfo = context.schemaInfo
|
|
441
|
+
const queryParams = context.queryParams
|
|
442
|
+
const db = context.db || api.knex.instance
|
|
443
|
+
const sortableFields = context.sortableFields
|
|
444
|
+
|
|
445
|
+
log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
|
|
446
|
+
log.debug(`[Knex] QUERY ${tableName}`, queryParams)
|
|
447
|
+
|
|
453
448
|
// Build field selection for sparse fieldsets
|
|
454
449
|
// This determines which fields to SELECT from database
|
|
455
450
|
// and tracks dependencies needed for computed fields
|
|
456
451
|
const fieldSelectionInfo = await buildFieldSelection(
|
|
457
452
|
scope,
|
|
458
453
|
{ context }
|
|
459
|
-
)
|
|
460
|
-
|
|
454
|
+
)
|
|
455
|
+
|
|
461
456
|
// Store dependency info in context for enrichAttributes
|
|
462
457
|
// Example: If user requests 'profit_margin' (computed), this might contain ['cost']
|
|
463
458
|
// The REST API plugin will use this to remove 'cost' from response if not requested
|
|
464
|
-
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
465
|
-
|
|
459
|
+
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
460
|
+
|
|
466
461
|
// Start building query with table prefix (for JOIN support)
|
|
467
|
-
let query = db(tableName)
|
|
462
|
+
let query = db(tableName)
|
|
468
463
|
|
|
469
|
-
const selectTranslator = createSelectTranslator(storageAdapter)
|
|
464
|
+
const selectTranslator = createSelectTranslator(storageAdapter)
|
|
470
465
|
query = buildQuerySelection(
|
|
471
466
|
query,
|
|
472
467
|
tableName,
|
|
473
468
|
fieldSelectionInfo.fieldsToSelect,
|
|
474
469
|
true,
|
|
475
|
-
selectTranslator ? { translateColumn: selectTranslator } : undefined
|
|
476
|
-
)
|
|
477
|
-
|
|
470
|
+
selectTranslator ? { translateColumn: selectTranslator } : undefined
|
|
471
|
+
)
|
|
472
|
+
|
|
478
473
|
/* ═══════════════════════════════════════════════════════════════════
|
|
479
474
|
* FILTERING HOOKS
|
|
480
475
|
* This is where the magic happens. The knexQueryFiltering hook is called
|
|
481
476
|
* to apply all filter conditions. The searchSchemaFilter hook (registered
|
|
482
477
|
* above) will process the searchSchema and apply filters with JOINs.
|
|
483
|
-
*
|
|
478
|
+
*
|
|
484
479
|
* IMPORTANT: Each hook should wrap its conditions in query.where(function() {...})
|
|
485
480
|
* to ensure proper grouping and prevent accidental filter bypass.
|
|
486
481
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
487
|
-
|
|
488
|
-
log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
|
|
489
|
-
|
|
490
|
-
log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' })
|
|
491
|
-
|
|
492
|
-
log.trace('[DATA-QUERY] Storing query data in context before calling runHooks')
|
|
493
|
-
|
|
482
|
+
|
|
483
|
+
log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
|
|
484
|
+
|
|
485
|
+
log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' })
|
|
486
|
+
|
|
487
|
+
log.trace('[DATA-QUERY] Storing query data in context before calling runHooks')
|
|
488
|
+
|
|
494
489
|
// YOU ARE HERE: pass 'sesrchSchemaInstance' in the context below
|
|
495
490
|
|
|
496
491
|
// Store the query data in context where hooks can access it
|
|
@@ -504,63 +499,63 @@ export const RestApiKnexPlugin = {
|
|
|
504
499
|
tableName,
|
|
505
500
|
db,
|
|
506
501
|
adapter: storageAdapter,
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
|
|
510
505
|
}
|
|
511
|
-
|
|
512
|
-
await runHooks('knexQueryFiltering')
|
|
513
|
-
|
|
506
|
+
|
|
507
|
+
await runHooks('knexQueryFiltering')
|
|
508
|
+
|
|
514
509
|
// Clean up after hook execution
|
|
515
510
|
if (context && context.knexQuery) {
|
|
516
|
-
delete context.knexQuery
|
|
511
|
+
delete context.knexQuery
|
|
517
512
|
}
|
|
518
|
-
|
|
519
|
-
log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
|
|
520
|
-
|
|
513
|
+
|
|
514
|
+
log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
|
|
515
|
+
|
|
521
516
|
// Apply sorting directly (no hooks)
|
|
522
517
|
if (queryParams.sort && queryParams.sort.length > 0) {
|
|
523
518
|
queryParams.sort.forEach(sortField => {
|
|
524
|
-
const desc = sortField.startsWith('-')
|
|
525
|
-
const field = desc ? sortField.substring(1) : sortField
|
|
526
|
-
|
|
519
|
+
const desc = sortField.startsWith('-')
|
|
520
|
+
const field = desc ? sortField.substring(1) : sortField
|
|
521
|
+
|
|
527
522
|
// Check if field is sortable
|
|
528
523
|
if (sortableFields && sortableFields.length > 0 && !sortableFields.includes(field)) {
|
|
529
|
-
log.warn(`Ignoring non-sortable field: ${field}`)
|
|
530
|
-
return
|
|
524
|
+
log.warn(`Ignoring non-sortable field: ${field}`)
|
|
525
|
+
return // Skip non-sortable fields
|
|
531
526
|
}
|
|
532
|
-
|
|
527
|
+
|
|
533
528
|
// Map relationship names to actual database columns for sorting
|
|
534
529
|
// Check if this is a relationship field that needs to be mapped
|
|
535
|
-
let dbField = field
|
|
536
|
-
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
530
|
+
let dbField = field
|
|
531
|
+
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
537
532
|
if (searchField?.actualField) {
|
|
538
533
|
// This is a relationship field, use the actual database column
|
|
539
|
-
dbField = searchField.actualField
|
|
534
|
+
dbField = searchField.actualField
|
|
540
535
|
}
|
|
541
536
|
const translatedField = storageAdapter?.translateColumn
|
|
542
537
|
? storageAdapter.translateColumn(dbField)
|
|
543
|
-
: dbField
|
|
538
|
+
: dbField
|
|
544
539
|
|
|
545
|
-
query.orderBy(translatedField, desc ? 'desc' : 'asc')
|
|
546
|
-
})
|
|
540
|
+
query.orderBy(translatedField, desc ? 'desc' : 'asc')
|
|
541
|
+
})
|
|
547
542
|
}
|
|
548
|
-
|
|
543
|
+
|
|
549
544
|
// Apply pagination
|
|
550
545
|
// Check if page object has any actual pagination parameters
|
|
551
|
-
const hasPageParams = queryParams.page &&
|
|
552
|
-
(queryParams.page.size !== undefined ||
|
|
553
|
-
queryParams.page.number !== undefined ||
|
|
554
|
-
queryParams.page.after !== undefined ||
|
|
555
|
-
queryParams.page.before !== undefined)
|
|
556
|
-
|
|
546
|
+
const hasPageParams = queryParams.page &&
|
|
547
|
+
(queryParams.page.size !== undefined ||
|
|
548
|
+
queryParams.page.number !== undefined ||
|
|
549
|
+
queryParams.page.after !== undefined ||
|
|
550
|
+
queryParams.page.before !== undefined)
|
|
551
|
+
|
|
557
552
|
if (hasPageParams) {
|
|
558
|
-
const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
553
|
+
const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
559
554
|
const pageSize = Math.min(
|
|
560
555
|
requestedSize,
|
|
561
556
|
scope.vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
|
|
562
|
-
)
|
|
563
|
-
|
|
557
|
+
)
|
|
558
|
+
|
|
564
559
|
// Validate page size
|
|
565
560
|
if (requestedSize <= 0) {
|
|
566
561
|
throw new RestApiValidationError(
|
|
@@ -573,28 +568,28 @@ export const RestApiKnexPlugin = {
|
|
|
573
568
|
message: 'Page size must be a positive number'
|
|
574
569
|
}]
|
|
575
570
|
}
|
|
576
|
-
)
|
|
571
|
+
)
|
|
577
572
|
}
|
|
578
|
-
|
|
573
|
+
|
|
579
574
|
// Offset-based pagination
|
|
580
575
|
if (queryParams.page.number !== undefined) {
|
|
581
|
-
const pageNumber = queryParams.page.number || 1
|
|
576
|
+
const pageNumber = queryParams.page.number || 1
|
|
582
577
|
query
|
|
583
578
|
.limit(pageSize)
|
|
584
|
-
.offset((pageNumber - 1) * pageSize)
|
|
579
|
+
.offset((pageNumber - 1) * pageSize)
|
|
585
580
|
}
|
|
586
581
|
// Cursor-based pagination
|
|
587
582
|
else if (queryParams.page.after || queryParams.page.before) {
|
|
588
583
|
// Fetch one extra record to determine if there are more
|
|
589
|
-
query.limit(pageSize + 1)
|
|
590
|
-
|
|
584
|
+
query.limit(pageSize + 1)
|
|
585
|
+
|
|
591
586
|
// Get the configured ID property
|
|
592
|
-
const idProperty = context.schemaInfo.idProperty || 'id'
|
|
593
|
-
|
|
587
|
+
const idProperty = context.schemaInfo.idProperty || 'id'
|
|
588
|
+
|
|
594
589
|
if (queryParams.page.after) {
|
|
595
|
-
let cursorData
|
|
590
|
+
let cursorData
|
|
596
591
|
try {
|
|
597
|
-
cursorData = parseCursor(queryParams.page.after)
|
|
592
|
+
cursorData = parseCursor(queryParams.page.after)
|
|
598
593
|
} catch (error) {
|
|
599
594
|
throw new RestApiValidationError(
|
|
600
595
|
'Invalid cursor format in page[after] parameter',
|
|
@@ -606,64 +601,63 @@ export const RestApiKnexPlugin = {
|
|
|
606
601
|
message: 'The cursor value is not valid'
|
|
607
602
|
}]
|
|
608
603
|
}
|
|
609
|
-
)
|
|
604
|
+
)
|
|
610
605
|
}
|
|
611
|
-
|
|
606
|
+
|
|
612
607
|
// Build sort fields array with directions
|
|
613
|
-
const sortFields = []
|
|
608
|
+
const sortFields = []
|
|
614
609
|
if (queryParams.sort && queryParams.sort.length > 0) {
|
|
615
610
|
queryParams.sort.forEach(sortField => {
|
|
616
|
-
const desc = sortField.startsWith('-')
|
|
617
|
-
const field = desc ? sortField.substring(1) : sortField
|
|
618
|
-
sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
|
|
619
|
-
})
|
|
611
|
+
const desc = sortField.startsWith('-')
|
|
612
|
+
const field = desc ? sortField.substring(1) : sortField
|
|
613
|
+
sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
|
|
614
|
+
})
|
|
620
615
|
} else if (scope.vars.defaultSort) {
|
|
621
616
|
sortFields.push({
|
|
622
617
|
field: scope.vars.defaultSort.field || idProperty,
|
|
623
618
|
direction: scope.vars.defaultSort.direction || 'ASC'
|
|
624
|
-
})
|
|
619
|
+
})
|
|
625
620
|
} else {
|
|
626
|
-
sortFields.push({ field: idProperty, direction: 'ASC' })
|
|
621
|
+
sortFields.push({ field: idProperty, direction: 'ASC' })
|
|
627
622
|
}
|
|
628
|
-
|
|
623
|
+
|
|
629
624
|
// Build compound WHERE clause for multi-field cursor
|
|
630
|
-
query.where(function() {
|
|
625
|
+
query.where(function () {
|
|
631
626
|
sortFields.forEach((sortInfo, index) => {
|
|
632
|
-
const { field, direction } = sortInfo
|
|
633
|
-
const qualifiedField = `${tableName}.${field}
|
|
634
|
-
const cursorValue = cursorData[field]
|
|
635
|
-
|
|
627
|
+
const { field, direction } = sortInfo
|
|
628
|
+
const qualifiedField = `${tableName}.${field}`
|
|
629
|
+
const cursorValue = cursorData[field]
|
|
630
|
+
|
|
636
631
|
if (cursorValue === undefined) {
|
|
637
|
-
log.warn(`Cursor missing value for sort field: ${field}`)
|
|
638
|
-
return
|
|
632
|
+
log.warn(`Cursor missing value for sort field: ${field}`)
|
|
633
|
+
return
|
|
639
634
|
}
|
|
640
|
-
|
|
635
|
+
|
|
641
636
|
// Build condition for this level and all previous levels
|
|
642
|
-
this.orWhere(function() {
|
|
637
|
+
this.orWhere(function () {
|
|
643
638
|
// All previous fields must be equal
|
|
644
639
|
for (let i = 0; i < index; i++) {
|
|
645
|
-
const prevField = sortFields[i].field
|
|
646
|
-
const prevQualifiedField = `${tableName}.${prevField}
|
|
647
|
-
const prevValue = cursorData[prevField]
|
|
640
|
+
const prevField = sortFields[i].field
|
|
641
|
+
const prevQualifiedField = `${tableName}.${prevField}`
|
|
642
|
+
const prevValue = cursorData[prevField]
|
|
648
643
|
if (prevValue !== undefined) {
|
|
649
|
-
this.where(prevQualifiedField, '=', prevValue)
|
|
644
|
+
this.where(prevQualifiedField, '=', prevValue)
|
|
650
645
|
}
|
|
651
646
|
}
|
|
652
|
-
|
|
647
|
+
|
|
653
648
|
// Current field must be greater/less than cursor value
|
|
654
649
|
if (direction === 'DESC') {
|
|
655
|
-
this.where(qualifiedField, '<', cursorValue)
|
|
650
|
+
this.where(qualifiedField, '<', cursorValue)
|
|
656
651
|
} else {
|
|
657
|
-
this.where(qualifiedField, '>', cursorValue)
|
|
652
|
+
this.where(qualifiedField, '>', cursorValue)
|
|
658
653
|
}
|
|
659
|
-
})
|
|
660
|
-
})
|
|
661
|
-
})
|
|
662
|
-
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
})
|
|
663
657
|
} else if (queryParams.page.before) {
|
|
664
|
-
let cursorData
|
|
658
|
+
let cursorData
|
|
665
659
|
try {
|
|
666
|
-
cursorData = parseCursor(queryParams.page.before)
|
|
660
|
+
cursorData = parseCursor(queryParams.page.before)
|
|
667
661
|
} catch (error) {
|
|
668
662
|
throw new RestApiValidationError(
|
|
669
663
|
'Invalid cursor format in page[before] parameter',
|
|
@@ -675,82 +669,82 @@ export const RestApiKnexPlugin = {
|
|
|
675
669
|
message: 'The cursor value is not valid'
|
|
676
670
|
}]
|
|
677
671
|
}
|
|
678
|
-
)
|
|
672
|
+
)
|
|
679
673
|
}
|
|
680
|
-
|
|
674
|
+
|
|
681
675
|
// Build sort fields array with directions
|
|
682
|
-
const sortFields = []
|
|
676
|
+
const sortFields = []
|
|
683
677
|
if (queryParams.sort && queryParams.sort.length > 0) {
|
|
684
678
|
queryParams.sort.forEach(sortField => {
|
|
685
|
-
const desc = sortField.startsWith('-')
|
|
686
|
-
const field = desc ? sortField.substring(1) : sortField
|
|
687
|
-
sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
|
|
688
|
-
})
|
|
679
|
+
const desc = sortField.startsWith('-')
|
|
680
|
+
const field = desc ? sortField.substring(1) : sortField
|
|
681
|
+
sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
|
|
682
|
+
})
|
|
689
683
|
} else if (scope.vars.defaultSort) {
|
|
690
684
|
sortFields.push({
|
|
691
685
|
field: scope.vars.defaultSort.field || idProperty,
|
|
692
686
|
direction: scope.vars.defaultSort.direction || 'ASC'
|
|
693
|
-
})
|
|
687
|
+
})
|
|
694
688
|
} else {
|
|
695
|
-
sortFields.push({ field: idProperty, direction: 'ASC' })
|
|
689
|
+
sortFields.push({ field: idProperty, direction: 'ASC' })
|
|
696
690
|
}
|
|
697
|
-
|
|
691
|
+
|
|
698
692
|
// Build compound WHERE clause for multi-field cursor (reversed for before)
|
|
699
|
-
query.where(function() {
|
|
693
|
+
query.where(function () {
|
|
700
694
|
sortFields.forEach((sortInfo, index) => {
|
|
701
|
-
const { field, direction } = sortInfo
|
|
702
|
-
const qualifiedField = `${tableName}.${field}
|
|
703
|
-
const cursorValue = cursorData[field]
|
|
704
|
-
|
|
695
|
+
const { field, direction } = sortInfo
|
|
696
|
+
const qualifiedField = `${tableName}.${field}`
|
|
697
|
+
const cursorValue = cursorData[field]
|
|
698
|
+
|
|
705
699
|
if (cursorValue === undefined) {
|
|
706
|
-
log.warn(`Cursor missing value for sort field: ${field}`)
|
|
707
|
-
return
|
|
700
|
+
log.warn(`Cursor missing value for sort field: ${field}`)
|
|
701
|
+
return
|
|
708
702
|
}
|
|
709
|
-
|
|
703
|
+
|
|
710
704
|
// Build condition for this level and all previous levels
|
|
711
|
-
this.orWhere(function() {
|
|
705
|
+
this.orWhere(function () {
|
|
712
706
|
// All previous fields must be equal
|
|
713
707
|
for (let i = 0; i < index; i++) {
|
|
714
|
-
const prevField = sortFields[i].field
|
|
715
|
-
const prevQualifiedField = `${tableName}.${prevField}
|
|
716
|
-
const prevValue = cursorData[prevField]
|
|
708
|
+
const prevField = sortFields[i].field
|
|
709
|
+
const prevQualifiedField = `${tableName}.${prevField}`
|
|
710
|
+
const prevValue = cursorData[prevField]
|
|
717
711
|
if (prevValue !== undefined) {
|
|
718
|
-
this.where(prevQualifiedField, '=', prevValue)
|
|
712
|
+
this.where(prevQualifiedField, '=', prevValue)
|
|
719
713
|
}
|
|
720
714
|
}
|
|
721
|
-
|
|
715
|
+
|
|
722
716
|
// Current field must be less/greater than cursor value (reversed)
|
|
723
717
|
if (direction === 'DESC') {
|
|
724
|
-
this.where(qualifiedField, '>', cursorValue)
|
|
718
|
+
this.where(qualifiedField, '>', cursorValue)
|
|
725
719
|
} else {
|
|
726
|
-
this.where(qualifiedField, '<', cursorValue)
|
|
720
|
+
this.where(qualifiedField, '<', cursorValue)
|
|
727
721
|
}
|
|
728
|
-
})
|
|
729
|
-
})
|
|
730
|
-
})
|
|
722
|
+
})
|
|
723
|
+
})
|
|
724
|
+
})
|
|
731
725
|
}
|
|
732
726
|
}
|
|
733
727
|
// Default pagination if only size is specified - treat as cursor-based for "load more"
|
|
734
728
|
else {
|
|
735
729
|
// Fetch one extra to detect hasMore
|
|
736
|
-
query.limit(pageSize + 1)
|
|
730
|
+
query.limit(pageSize + 1)
|
|
737
731
|
}
|
|
738
732
|
} else {
|
|
739
733
|
// No pagination params provided - apply default limit
|
|
740
|
-
const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
741
|
-
query.limit(defaultLimit)
|
|
734
|
+
const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
735
|
+
query.limit(defaultLimit)
|
|
742
736
|
}
|
|
743
|
-
|
|
737
|
+
|
|
744
738
|
// Execute query
|
|
745
|
-
const records = await query
|
|
746
|
-
|
|
739
|
+
const records = await query
|
|
740
|
+
|
|
747
741
|
// Store query string for response building
|
|
748
|
-
const queryParts = []
|
|
742
|
+
const queryParts = []
|
|
749
743
|
Object.entries(queryParams).forEach(([key, value]) => {
|
|
750
744
|
if (Array.isArray(value)) {
|
|
751
745
|
// Handle arrays (like sort, include)
|
|
752
746
|
if (value.length > 0) {
|
|
753
|
-
queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`)
|
|
747
|
+
queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`)
|
|
754
748
|
}
|
|
755
749
|
} else if (typeof value === 'object' && value !== null) {
|
|
756
750
|
// Handle nested objects (like filters, fields, page)
|
|
@@ -760,33 +754,33 @@ export const RestApiKnexPlugin = {
|
|
|
760
754
|
// Handle deeply nested objects (like filters[country][code])
|
|
761
755
|
Object.entries(subValue).forEach(([subSubKey, subSubValue]) => {
|
|
762
756
|
if (subSubValue !== undefined && subSubValue !== null) {
|
|
763
|
-
queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`)
|
|
757
|
+
queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`)
|
|
764
758
|
}
|
|
765
|
-
})
|
|
759
|
+
})
|
|
766
760
|
} else {
|
|
767
|
-
queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`)
|
|
761
|
+
queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`)
|
|
768
762
|
}
|
|
769
763
|
}
|
|
770
|
-
})
|
|
764
|
+
})
|
|
771
765
|
} else if (value !== undefined && value !== null) {
|
|
772
|
-
queryParts.push(`${key}=${encodeURIComponent(value)}`)
|
|
766
|
+
queryParts.push(`${key}=${encodeURIComponent(value)}`)
|
|
773
767
|
}
|
|
774
|
-
})
|
|
775
|
-
|
|
768
|
+
})
|
|
769
|
+
|
|
776
770
|
// Initialize returnMeta namespace for thread-safe metadata
|
|
777
|
-
context.returnMeta = context.returnMeta || {}
|
|
778
|
-
context.returnMeta.queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
|
771
|
+
context.returnMeta = context.returnMeta || {}
|
|
772
|
+
context.returnMeta.queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
|
779
773
|
|
|
780
774
|
// Execute count query for pagination if offset-based pagination is used
|
|
781
775
|
if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
|
|
782
|
-
const page = parseInt(queryParams.page?.number) || 1
|
|
783
|
-
const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
784
|
-
|
|
776
|
+
const page = parseInt(queryParams.page?.number) || 1
|
|
777
|
+
const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
778
|
+
|
|
785
779
|
// Only execute count query if enabled
|
|
786
780
|
if (scope.vars.enablePaginationCounts) {
|
|
787
781
|
// Build count query with same filters as main query
|
|
788
|
-
const countQuery = db(tableName)
|
|
789
|
-
|
|
782
|
+
const countQuery = db(tableName)
|
|
783
|
+
|
|
790
784
|
// Apply filters through hooks (same as main query)
|
|
791
785
|
if (queryParams.filters && Object.keys(queryParams.filters).length > 0) {
|
|
792
786
|
// Store query data in context for hooks
|
|
@@ -798,67 +792,67 @@ export const RestApiKnexPlugin = {
|
|
|
798
792
|
tableName,
|
|
799
793
|
schemaInfo,
|
|
800
794
|
db
|
|
801
|
-
}
|
|
795
|
+
}
|
|
802
796
|
}
|
|
803
|
-
|
|
797
|
+
|
|
804
798
|
// Run the same filtering hooks
|
|
805
|
-
await runHooks('knexQueryFiltering')
|
|
806
|
-
|
|
799
|
+
await runHooks('knexQueryFiltering')
|
|
800
|
+
|
|
807
801
|
// Clean up
|
|
808
802
|
if (context && context.knexQuery) {
|
|
809
|
-
delete context.knexQuery
|
|
803
|
+
delete context.knexQuery
|
|
810
804
|
}
|
|
811
805
|
}
|
|
812
|
-
|
|
806
|
+
|
|
813
807
|
// Get total count
|
|
814
|
-
const countResult = await countQuery.count('* as total').first()
|
|
815
|
-
const total = parseInt(countResult.total)
|
|
816
|
-
|
|
808
|
+
const countResult = await countQuery.count('* as total').first()
|
|
809
|
+
const total = parseInt(countResult.total)
|
|
810
|
+
|
|
817
811
|
// Calculate pagination metadata with total
|
|
818
|
-
context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
|
|
812
|
+
context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
|
|
819
813
|
} else {
|
|
820
814
|
// Without count, we can still provide basic pagination info
|
|
821
815
|
context.returnMeta.paginationMeta = {
|
|
822
816
|
page,
|
|
823
817
|
pageSize
|
|
824
818
|
// No total, pageCount, or hasMore when counts are disabled
|
|
825
|
-
}
|
|
819
|
+
}
|
|
826
820
|
}
|
|
827
|
-
|
|
828
|
-
// Generate links
|
|
829
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
821
|
+
|
|
822
|
+
// Generate links
|
|
823
|
+
const urlPrefix = getUrlPrefix(context, scope)
|
|
830
824
|
context.returnMeta.paginationLinks = generatePaginationLinks(
|
|
831
825
|
urlPrefix,
|
|
832
826
|
scopeName,
|
|
833
827
|
queryParams,
|
|
834
828
|
context.returnMeta.paginationMeta
|
|
835
|
-
)
|
|
829
|
+
)
|
|
836
830
|
}
|
|
837
|
-
|
|
831
|
+
|
|
838
832
|
// Handle cursor-based pagination meta
|
|
839
833
|
// Generate cursor metadata when using cursor parameters OR when only size is specified (no page number)
|
|
840
|
-
if (queryParams.page?.after || queryParams.page?.before ||
|
|
834
|
+
if (queryParams.page?.after || queryParams.page?.before ||
|
|
841
835
|
(queryParams.page?.size && queryParams.page?.number === undefined)) {
|
|
842
|
-
const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
843
|
-
|
|
836
|
+
const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
837
|
+
|
|
844
838
|
// Check if there are more records
|
|
845
839
|
// We fetched pageSize + 1 records to detect if there are more
|
|
846
|
-
const hasMore = records.length > pageSize
|
|
847
|
-
|
|
840
|
+
const hasMore = records.length > pageSize
|
|
841
|
+
|
|
848
842
|
// Remove the extra record if present
|
|
849
843
|
if (hasMore) {
|
|
850
|
-
records.pop()
|
|
844
|
+
records.pop()
|
|
851
845
|
}
|
|
852
|
-
|
|
846
|
+
|
|
853
847
|
// Determine sort fields for cursor
|
|
854
|
-
const idProperty = context.schemaInfo.idProperty || 'id'
|
|
855
|
-
let sortFields = [idProperty]
|
|
848
|
+
const idProperty = context.schemaInfo.idProperty || 'id'
|
|
849
|
+
let sortFields = [idProperty]
|
|
856
850
|
if (queryParams.sort && queryParams.sort.length > 0) {
|
|
857
|
-
sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s)
|
|
851
|
+
sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s)
|
|
858
852
|
}
|
|
859
|
-
|
|
860
|
-
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields)
|
|
861
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
853
|
+
|
|
854
|
+
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields)
|
|
855
|
+
const urlPrefix = getUrlPrefix(context, scope)
|
|
862
856
|
context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
863
857
|
urlPrefix,
|
|
864
858
|
scopeName,
|
|
@@ -867,12 +861,12 @@ export const RestApiKnexPlugin = {
|
|
|
867
861
|
pageSize,
|
|
868
862
|
hasMore,
|
|
869
863
|
sortFields
|
|
870
|
-
)
|
|
864
|
+
)
|
|
871
865
|
}
|
|
872
|
-
|
|
866
|
+
|
|
873
867
|
// Load relationship identifiers for all hasMany relationships
|
|
874
|
-
await loadRelationshipIdentifiers(records, scopeName, scopes, db)
|
|
875
|
-
|
|
868
|
+
await loadRelationshipIdentifiers(records, scopeName, scopes, db)
|
|
869
|
+
|
|
876
870
|
// Process includes
|
|
877
871
|
const included = await processIncludes(scope, records, {
|
|
878
872
|
log,
|
|
@@ -880,16 +874,15 @@ export const RestApiKnexPlugin = {
|
|
|
880
874
|
knex,
|
|
881
875
|
context,
|
|
882
876
|
api
|
|
883
|
-
})
|
|
884
|
-
|
|
877
|
+
})
|
|
885
878
|
|
|
886
879
|
// Build and return response
|
|
887
|
-
return buildJsonApiResponse(scope, records, included, false, scopeName, context)
|
|
888
|
-
}
|
|
889
|
-
|
|
880
|
+
return buildJsonApiResponse(scope, records, included, false, scopeName, context)
|
|
881
|
+
}
|
|
882
|
+
|
|
890
883
|
/**
|
|
891
884
|
* Creates a new resource in the database
|
|
892
|
-
*
|
|
885
|
+
*
|
|
893
886
|
* @param {Object} params - The parameters object
|
|
894
887
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
895
888
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -904,35 +897,35 @@ export const RestApiKnexPlugin = {
|
|
|
904
897
|
* @returns {Promise<string|number>} The ID of the newly created resource
|
|
905
898
|
*/
|
|
906
899
|
helpers.dataPost = async ({ scopeName, context }) => {
|
|
907
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
900
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
908
901
|
if (storageAdapter) {
|
|
909
|
-
context.storageAdapter = storageAdapter
|
|
902
|
+
context.storageAdapter = storageAdapter
|
|
910
903
|
}
|
|
911
|
-
const scope = api.resources[scopeName]
|
|
912
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
913
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
914
|
-
const db = context.db || api.knex.instance
|
|
915
|
-
const inputRecord = context.inputRecord
|
|
916
|
-
|
|
917
|
-
log.debug(`[Knex] POST ${tableName}`, inputRecord)
|
|
918
|
-
|
|
904
|
+
const scope = api.resources[scopeName]
|
|
905
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
906
|
+
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
907
|
+
const db = context.db || api.knex.instance
|
|
908
|
+
const inputRecord = context.inputRecord
|
|
909
|
+
|
|
910
|
+
log.debug(`[Knex] POST ${tableName}`, inputRecord)
|
|
911
|
+
|
|
919
912
|
// Extract attributes from JSON:API format
|
|
920
|
-
const attributes = inputRecord.data.attributes
|
|
921
|
-
|
|
913
|
+
const attributes = inputRecord.data.attributes
|
|
914
|
+
|
|
922
915
|
// Strip non-database fields (computed and virtual) before insert
|
|
923
|
-
const dbAttributes = stripNonDatabaseFields(attributes, context.schemaInfo)
|
|
924
|
-
|
|
916
|
+
const dbAttributes = stripNonDatabaseFields(attributes, context.schemaInfo)
|
|
917
|
+
|
|
925
918
|
// Insert and get the new ID
|
|
926
|
-
const result = await db(tableName).insert(dbAttributes).returning(idProperty)
|
|
927
|
-
|
|
919
|
+
const result = await db(tableName).insert(dbAttributes).returning(idProperty)
|
|
920
|
+
|
|
928
921
|
// Extract the ID value (SQLite returns array of objects)
|
|
929
|
-
const id = result[0]?.[idProperty] || result[0]
|
|
922
|
+
const id = result[0]?.[idProperty] || result[0]
|
|
930
923
|
return id
|
|
931
|
-
}
|
|
932
|
-
|
|
924
|
+
}
|
|
925
|
+
|
|
933
926
|
/**
|
|
934
927
|
* Replaces an entire resource (PUT operation) or creates it with a specific ID
|
|
935
|
-
*
|
|
928
|
+
*
|
|
936
929
|
* @param {Object} params - The parameters object
|
|
937
930
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
938
931
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -951,31 +944,31 @@ export const RestApiKnexPlugin = {
|
|
|
951
944
|
* @throws {RestApiResourceError} When updating and the resource is not found
|
|
952
945
|
*/
|
|
953
946
|
helpers.dataPut = async ({ scopeName, context }) => {
|
|
954
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
947
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
955
948
|
if (storageAdapter) {
|
|
956
|
-
context.storageAdapter = storageAdapter
|
|
949
|
+
context.storageAdapter = storageAdapter
|
|
957
950
|
}
|
|
958
|
-
const scope = api.resources[scopeName]
|
|
959
|
-
const id = context.id
|
|
960
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
961
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
962
|
-
const db = context.db || api.knex.instance
|
|
963
|
-
const inputRecord = context.inputRecord
|
|
964
|
-
const isCreate = context.isCreate
|
|
965
|
-
|
|
966
|
-
log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
|
|
967
|
-
|
|
951
|
+
const scope = api.resources[scopeName]
|
|
952
|
+
const id = context.id
|
|
953
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
954
|
+
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
955
|
+
const db = context.db || api.knex.instance
|
|
956
|
+
const inputRecord = context.inputRecord
|
|
957
|
+
const isCreate = context.isCreate
|
|
958
|
+
|
|
959
|
+
log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
|
|
960
|
+
|
|
968
961
|
// Extract attributes and process relationships using helper
|
|
969
|
-
const attributes = inputRecord.data.attributes || {}
|
|
970
|
-
const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
971
|
-
const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
972
|
-
|
|
962
|
+
const attributes = inputRecord.data.attributes || {}
|
|
963
|
+
const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
964
|
+
const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
965
|
+
|
|
973
966
|
// Strip non-database fields (computed and virtual) before database operation
|
|
974
|
-
const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
|
|
975
|
-
|
|
967
|
+
const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
|
|
968
|
+
|
|
976
969
|
// Map 'id' to actual idProperty if needed (for PUT with specific ID)
|
|
977
970
|
if (idProperty !== 'id' && inputRecord.data.id) {
|
|
978
|
-
finalAttributes[idProperty] = inputRecord.data.id
|
|
971
|
+
finalAttributes[idProperty] = inputRecord.data.id
|
|
979
972
|
}
|
|
980
973
|
|
|
981
974
|
if (isCreate) {
|
|
@@ -983,43 +976,41 @@ export const RestApiKnexPlugin = {
|
|
|
983
976
|
const recordData = {
|
|
984
977
|
...finalAttributes,
|
|
985
978
|
[idProperty]: id
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
await db(tableName).insert(recordData)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
await db(tableName).insert(recordData)
|
|
989
982
|
} else {
|
|
990
983
|
// Update mode - check if record exists first
|
|
991
984
|
const exists = await db(tableName)
|
|
992
985
|
.where(idProperty, id)
|
|
993
|
-
.first()
|
|
994
|
-
|
|
986
|
+
.first()
|
|
987
|
+
|
|
995
988
|
if (!exists) {
|
|
996
989
|
throw new RestApiResourceError(
|
|
997
|
-
|
|
998
|
-
{
|
|
990
|
+
'Resource not found',
|
|
991
|
+
{
|
|
999
992
|
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1000
993
|
resourceType: scopeName,
|
|
1001
994
|
resourceId: id
|
|
1002
995
|
}
|
|
1003
|
-
)
|
|
996
|
+
)
|
|
1004
997
|
}
|
|
1005
|
-
|
|
998
|
+
|
|
1006
999
|
// Remove the idProperty from attributes to prevent updating the primary key
|
|
1007
|
-
delete finalAttributes[idProperty]
|
|
1008
|
-
|
|
1000
|
+
delete finalAttributes[idProperty]
|
|
1001
|
+
|
|
1009
1002
|
// Update the record (replace all fields)
|
|
1010
1003
|
if (Object.keys(finalAttributes).length > 0) {
|
|
1011
1004
|
await db(tableName)
|
|
1012
1005
|
.where(idProperty, id)
|
|
1013
|
-
.update(finalAttributes)
|
|
1006
|
+
.update(finalAttributes)
|
|
1014
1007
|
}
|
|
1015
1008
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1020
1011
|
/**
|
|
1021
1012
|
* Partially updates a resource (PATCH operation)
|
|
1022
|
-
*
|
|
1013
|
+
*
|
|
1023
1014
|
* @param {Object} params - The parameters object
|
|
1024
1015
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
1025
1016
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -1037,61 +1028,59 @@ export const RestApiKnexPlugin = {
|
|
|
1037
1028
|
* @throws {RestApiResourceError} When the resource is not found
|
|
1038
1029
|
*/
|
|
1039
1030
|
helpers.dataPatch = async ({ scopeName, context }) => {
|
|
1040
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1031
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1041
1032
|
if (storageAdapter) {
|
|
1042
|
-
context.storageAdapter = storageAdapter
|
|
1033
|
+
context.storageAdapter = storageAdapter
|
|
1043
1034
|
}
|
|
1044
|
-
const scope = api.resources[scopeName]
|
|
1045
|
-
const id = context.id
|
|
1046
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1047
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1048
|
-
const db = context.db || api.knex.instance
|
|
1049
|
-
const inputRecord = context.inputRecord
|
|
1050
|
-
|
|
1051
|
-
log.debug(`[Knex] PATCH ${tableName}/${id}`)
|
|
1052
|
-
|
|
1035
|
+
const scope = api.resources[scopeName]
|
|
1036
|
+
const id = context.id
|
|
1037
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1038
|
+
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1039
|
+
const db = context.db || api.knex.instance
|
|
1040
|
+
const inputRecord = context.inputRecord
|
|
1041
|
+
|
|
1042
|
+
log.debug(`[Knex] PATCH ${tableName}/${id}`)
|
|
1043
|
+
|
|
1053
1044
|
// Check if record exists
|
|
1054
1045
|
const exists = await db(tableName)
|
|
1055
1046
|
.where(idProperty, id)
|
|
1056
|
-
.first()
|
|
1057
|
-
|
|
1047
|
+
.first()
|
|
1048
|
+
|
|
1058
1049
|
if (!exists) {
|
|
1059
1050
|
throw new RestApiResourceError(
|
|
1060
|
-
|
|
1061
|
-
{
|
|
1051
|
+
'Resource not found',
|
|
1052
|
+
{
|
|
1062
1053
|
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1063
1054
|
resourceType: scopeName,
|
|
1064
1055
|
resourceId: id
|
|
1065
1056
|
}
|
|
1066
|
-
)
|
|
1057
|
+
)
|
|
1067
1058
|
}
|
|
1068
|
-
|
|
1059
|
+
|
|
1069
1060
|
// Extract attributes and process relationships using helper
|
|
1070
|
-
const attributes = inputRecord.data.attributes || {}
|
|
1071
|
-
const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
1072
|
-
const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
1073
|
-
|
|
1061
|
+
const attributes = inputRecord.data.attributes || {}
|
|
1062
|
+
const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
1063
|
+
const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
1064
|
+
|
|
1074
1065
|
// Strip non-database fields (computed and virtual) before database operation
|
|
1075
|
-
const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
|
|
1066
|
+
const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
|
|
1067
|
+
|
|
1068
|
+
log.debug('[Knex] PATCH finalAttributes:', finalAttributes)
|
|
1076
1069
|
|
|
1077
|
-
log.debug(`[Knex] PATCH finalAttributes:`, finalAttributes);
|
|
1078
|
-
|
|
1079
1070
|
// Remove the idProperty from attributes to prevent updating the primary key
|
|
1080
|
-
delete finalAttributes[idProperty]
|
|
1081
|
-
|
|
1071
|
+
delete finalAttributes[idProperty]
|
|
1072
|
+
|
|
1082
1073
|
// Update only if there are changes
|
|
1083
1074
|
if (Object.keys(finalAttributes).length > 0) {
|
|
1084
1075
|
await db(tableName)
|
|
1085
1076
|
.where(idProperty, id)
|
|
1086
|
-
.update(finalAttributes)
|
|
1077
|
+
.update(finalAttributes)
|
|
1087
1078
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1092
1081
|
/**
|
|
1093
1082
|
* Deletes a resource from the database
|
|
1094
|
-
*
|
|
1083
|
+
*
|
|
1095
1084
|
* @param {Object} params - The parameters object
|
|
1096
1085
|
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
1097
1086
|
* @param {Object} params.context - The context object containing request-specific data
|
|
@@ -1104,43 +1093,43 @@ export const RestApiKnexPlugin = {
|
|
|
1104
1093
|
* @throws {RestApiResourceError} When the resource is not found
|
|
1105
1094
|
*/
|
|
1106
1095
|
helpers.dataDelete = async ({ scopeName, context }) => {
|
|
1107
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1096
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1108
1097
|
if (storageAdapter) {
|
|
1109
|
-
context.storageAdapter = storageAdapter
|
|
1098
|
+
context.storageAdapter = storageAdapter
|
|
1110
1099
|
}
|
|
1111
|
-
const scope = api.resources[scopeName]
|
|
1112
|
-
const id = context.id
|
|
1113
|
-
|
|
1114
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1115
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1116
|
-
const db = context.db || api.knex.instance
|
|
1117
|
-
|
|
1118
|
-
log.debug(`[Knex] DELETE ${tableName}/${id}`)
|
|
1119
|
-
|
|
1100
|
+
const scope = api.resources[scopeName]
|
|
1101
|
+
const id = context.id
|
|
1102
|
+
|
|
1103
|
+
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1104
|
+
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1105
|
+
const db = context.db || api.knex.instance
|
|
1106
|
+
|
|
1107
|
+
log.debug(`[Knex] DELETE ${tableName}/${id}`)
|
|
1108
|
+
|
|
1120
1109
|
// Check if record exists
|
|
1121
1110
|
const exists = await db(tableName)
|
|
1122
1111
|
.where(idProperty, id)
|
|
1123
|
-
.first()
|
|
1124
|
-
|
|
1112
|
+
.first()
|
|
1113
|
+
|
|
1125
1114
|
if (!exists) {
|
|
1126
1115
|
throw new RestApiResourceError(
|
|
1127
|
-
|
|
1128
|
-
{
|
|
1116
|
+
'Resource not found',
|
|
1117
|
+
{
|
|
1129
1118
|
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1130
1119
|
resourceType: scopeName,
|
|
1131
1120
|
resourceId: id
|
|
1132
1121
|
}
|
|
1133
|
-
)
|
|
1122
|
+
)
|
|
1134
1123
|
}
|
|
1135
|
-
|
|
1124
|
+
|
|
1136
1125
|
// Delete the record
|
|
1137
1126
|
await db(tableName)
|
|
1138
1127
|
.where(idProperty, id)
|
|
1139
|
-
.delete()
|
|
1140
|
-
|
|
1141
|
-
return { success: true }
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
log.info('RestApiKnexPlugin installed - basic CRUD operations ready')
|
|
1128
|
+
.delete()
|
|
1129
|
+
|
|
1130
|
+
return { success: true }
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
log.info('RestApiKnexPlugin installed - basic CRUD operations ready')
|
|
1145
1134
|
}
|
|
1146
1135
|
}
|