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,8 +1,8 @@
|
|
|
1
1
|
// Cross-table search helper functions that enable filtering across related database tables
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
/**
|
|
4
4
|
* Validates that a field exists and is indexed for cross-table search
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @async
|
|
7
7
|
* @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
|
|
8
8
|
* @param {Object} log - Logger instance for debug and trace output
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
|
|
12
12
|
* @returns {Promise<void>}
|
|
13
13
|
* @throws {Error} If scope not found, field not found, field not indexed, or circular reference detected
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* @example
|
|
16
16
|
* // Input: Validate indexed field
|
|
17
17
|
* // Schema has: companies.name with indexed: true
|
|
18
18
|
* await validateCrossTableField(scopes, log, 'companies', 'name');
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* // Output: Validation succeeds (no error thrown)
|
|
21
|
-
*
|
|
21
|
+
*
|
|
22
22
|
* @example
|
|
23
23
|
* // Input: Non-indexed field
|
|
24
24
|
* // Schema has: companies.internal_code without indexed property
|
|
@@ -28,61 +28,61 @@
|
|
|
28
28
|
* console.log(error.message);
|
|
29
29
|
* // "Field 'companies.internal_code' is not indexed. Add 'indexed: true' to allow cross-table search"
|
|
30
30
|
* }
|
|
31
|
-
*
|
|
31
|
+
*
|
|
32
32
|
* @description
|
|
33
33
|
* Used by:
|
|
34
34
|
* - buildJoinChain calls this to ensure target fields are properly indexed
|
|
35
35
|
* - analyzeRequiredIndexes uses this to identify missing indexes
|
|
36
|
-
*
|
|
36
|
+
*
|
|
37
37
|
* Purpose:
|
|
38
38
|
* - Cross-table searches can be slow without proper indexes
|
|
39
39
|
* - Forces developers to explicitly mark searchable fields with indexed: true
|
|
40
40
|
* - Provides clear error messages when configuration is missing
|
|
41
|
-
*
|
|
41
|
+
*
|
|
42
42
|
* Data flow:
|
|
43
43
|
* - Called during query building to validate field configuration
|
|
44
44
|
* - Ensures performance by requiring indexes on searchable fields
|
|
45
45
|
* - Part of the validation phase before expensive JOIN operations
|
|
46
46
|
*/
|
|
47
47
|
export const validateCrossTableField = async (scopes, log, targetScopeName, fieldName, searchedScopes = new Set()) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
48
|
+
log.trace('[VALIDATE] Starting validateCrossTableField:', { targetScopeName, fieldName, searchedScopes: Array.from(searchedScopes) })
|
|
49
|
+
|
|
50
|
+
if (searchedScopes.has(targetScopeName)) {
|
|
51
|
+
throw new Error(`Circular reference detected: ${targetScopeName} -> ${Array.from(searchedScopes).join(' -> ')}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
log.trace('[VALIDATE] Getting target schema for:', targetScopeName)
|
|
55
|
+
let targetSchemaInstance
|
|
56
|
+
try {
|
|
57
|
+
targetSchemaInstance = scopes[targetScopeName].vars.schemaInfo.schemaInstance
|
|
58
|
+
log.trace('[VALIDATE] Got target schema:', { scopeName: targetScopeName, schemaKeys: Object.keys(targetSchemaInstance || {}) })
|
|
59
|
+
} catch (error) {
|
|
60
|
+
log.trace('[VALIDATE] Error getting target schema:', error.message)
|
|
61
|
+
throw new Error(`Target scope '${targetScopeName}' not found`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!targetSchemaInstance) {
|
|
65
|
+
log.trace('[VALIDATE] Target schema is null/undefined')
|
|
66
|
+
throw new Error(`Target scope '${targetScopeName}' has no schema`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const fieldDef = targetSchemaInstance.structure[fieldName]
|
|
70
|
+
log.trace('[VALIDATE] Field lookup result:', { fieldName, fieldExists: !!fieldDef })
|
|
71
|
+
if (!fieldDef) {
|
|
72
|
+
throw new Error(`Field '${fieldName}' not found in scope '${targetScopeName}'`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log.trace('[VALIDATE] Checking indexed status:', { fieldName, indexed: fieldDef.indexed })
|
|
76
|
+
if (!fieldDef.indexed) {
|
|
77
|
+
throw new Error(`Field '${targetScopeName}.${fieldName}' is not indexed. Add 'indexed: true' to allow cross-table search`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log.trace('[VALIDATE] Validation successful for field:', `${targetScopeName}.${fieldName}`)
|
|
81
|
+
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Builds a chain of SQL JOINs to reach a field in a related table
|
|
85
|
-
*
|
|
85
|
+
*
|
|
86
86
|
* @async
|
|
87
87
|
* @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
|
|
88
88
|
* @param {Object} log - Logger instance for debug and trace output
|
|
@@ -91,12 +91,12 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
91
91
|
* @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
|
|
92
92
|
* @returns {Promise<Object>} JOIN information for query builder
|
|
93
93
|
* @throws {Error} If path is invalid or relationships not properly configured
|
|
94
|
-
*
|
|
94
|
+
*
|
|
95
95
|
* @example
|
|
96
96
|
* // Input: Simple belongsTo relationship
|
|
97
97
|
* // articles table has author_id field, authors schema defines belongsTo relationship
|
|
98
98
|
* const joinInfo = await buildJoinChain('articles', 'author.name');
|
|
99
|
-
*
|
|
99
|
+
*
|
|
100
100
|
* // Output: Single JOIN configuration
|
|
101
101
|
* // {
|
|
102
102
|
* // joinAlias: 'articles_to_authors_authors',
|
|
@@ -107,12 +107,12 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
107
107
|
* // isOneToMany: false,
|
|
108
108
|
* // isPolymorphic: false
|
|
109
109
|
* // }
|
|
110
|
-
*
|
|
110
|
+
*
|
|
111
111
|
* @example
|
|
112
112
|
* // Input: Multi-level path through relationships
|
|
113
113
|
* // articles → authors → companies
|
|
114
114
|
* const joinInfo = await buildJoinChain('articles', 'author.company.name');
|
|
115
|
-
*
|
|
115
|
+
*
|
|
116
116
|
* // Output: Multi-level JOIN chain
|
|
117
117
|
* // {
|
|
118
118
|
* // targetTableName: 'companies',
|
|
@@ -140,12 +140,12 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
140
140
|
* // }
|
|
141
141
|
* // ]
|
|
142
142
|
* // }
|
|
143
|
-
*
|
|
143
|
+
*
|
|
144
144
|
* @example
|
|
145
145
|
* // Input: One-to-many relationship (hasMany)
|
|
146
146
|
* // authors have many articles
|
|
147
147
|
* const joinInfo = await buildJoinChain('authors', 'articles.title');
|
|
148
|
-
*
|
|
148
|
+
*
|
|
149
149
|
* // Output: JOIN with isOneToMany flag
|
|
150
150
|
* // {
|
|
151
151
|
* // joinAlias: 'authors_to_articles_articles',
|
|
@@ -156,17 +156,17 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
156
156
|
* // isOneToMany: true,
|
|
157
157
|
* // isPolymorphic: false
|
|
158
158
|
* // }
|
|
159
|
-
*
|
|
159
|
+
*
|
|
160
160
|
* @description
|
|
161
161
|
* Used by:
|
|
162
162
|
* - rest-api-knex-plugin's dataQuery method when processing cross-table filters
|
|
163
163
|
* - Called for each filter that references a related table field
|
|
164
|
-
*
|
|
164
|
+
*
|
|
165
165
|
* Purpose:
|
|
166
166
|
* - Automatically constructs complex SQL JOINs from simple dot notation
|
|
167
167
|
* - Supports many-to-one (belongsTo), one-to-many (hasMany), and many-to-many relationships
|
|
168
168
|
* - Handles polymorphic relationships and multi-level paths
|
|
169
|
-
*
|
|
169
|
+
*
|
|
170
170
|
* Data flow:
|
|
171
171
|
* 1. Filter parser identifies cross-table reference (contains dots)
|
|
172
172
|
* 2. buildJoinChain analyzes relationship path segment by segment
|
|
@@ -176,243 +176,243 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
176
176
|
* 6. Query builder adds these JOINs to enable filtering on related data
|
|
177
177
|
*/
|
|
178
178
|
export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, searchedScopes = new Set()) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
log.trace('[BUILD-JOIN] Starting buildJoinChain:', { fromScopeName, targetPath })
|
|
180
|
+
|
|
181
|
+
const pathSegments = targetPath.split('.')
|
|
182
|
+
if (pathSegments.length < 2) {
|
|
183
|
+
throw new Error(`Invalid cross-table path '${targetPath}'. Must contain at least 'scope.field'`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const targetFieldName = pathSegments[pathSegments.length - 1]
|
|
187
|
+
const relationshipPath = pathSegments.slice(0, -1)
|
|
188
|
+
|
|
189
|
+
log.trace('[BUILD-JOIN] Parsed path:', { relationshipPath, targetFieldName })
|
|
190
|
+
|
|
191
|
+
const joinChain = []
|
|
192
|
+
let currentScope = fromScopeName
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < relationshipPath.length; i++) {
|
|
195
|
+
const targetScope = relationshipPath[i]
|
|
196
|
+
|
|
197
|
+
log.trace('[BUILD-JOIN] Looking for direct relationship:', { from: currentScope, to: targetScope })
|
|
198
|
+
|
|
199
|
+
if (searchedScopes.has(currentScope)) {
|
|
200
|
+
throw new Error(`Circular reference detected in path: ${Array.from(searchedScopes).join(' -> ')} -> ${currentScope}`)
|
|
184
201
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (currentRelationships) {
|
|
214
|
-
for (const [relName, relDef] of Object.entries(currentRelationships)) {
|
|
215
|
-
if (relDef.type === 'hasMany' && relDef.target === targetScope) {
|
|
216
|
-
if (relDef.via) {
|
|
217
|
-
const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships;
|
|
218
|
-
const viaRel = targetRelationships?.[relDef.via];
|
|
219
|
-
|
|
220
|
-
if (viaRel?.belongsToPolymorphic) {
|
|
221
|
-
const { typeField, idField } = viaRel.belongsToPolymorphic;
|
|
222
|
-
foundRelationship = targetScope;
|
|
223
|
-
relationshipType = 'hasManyPolymorphic';
|
|
224
|
-
relationshipField = { typeField, idField, via: relDef.via };
|
|
225
|
-
relationshipDef = relDef;
|
|
226
|
-
log.trace('[BUILD-JOIN] Found polymorphic hasMany relationship:', {
|
|
227
|
-
relName, targetScope, via: relDef.via, typeField, idField
|
|
228
|
-
});
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (relDef.through) {
|
|
234
|
-
foundRelationship = targetScope;
|
|
235
|
-
relationshipType = 'manyToMany';
|
|
236
|
-
relationshipField = {
|
|
237
|
-
through: relDef.through,
|
|
238
|
-
foreignKey: relDef.foreignKey,
|
|
239
|
-
otherKey: relDef.otherKey
|
|
240
|
-
};
|
|
241
|
-
relationshipDef = relDef;
|
|
242
|
-
log.trace('[BUILD-JOIN] Found many-to-many relationship:', {
|
|
243
|
-
relName,
|
|
244
|
-
targetScope,
|
|
245
|
-
through: relDef.through,
|
|
246
|
-
foreignKey: relDef.foreignKey,
|
|
247
|
-
otherKey: relDef.otherKey
|
|
248
|
-
});
|
|
249
|
-
break;
|
|
202
|
+
searchedScopes.add(currentScope)
|
|
203
|
+
|
|
204
|
+
const schemaInfo = scopes[currentScope].vars.schemaInfo
|
|
205
|
+
const currentSchema = schemaInfo.schemaInstance
|
|
206
|
+
const currentRelationships = schemaInfo.schemaRelationships
|
|
207
|
+
|
|
208
|
+
let foundRelationship = null
|
|
209
|
+
let relationshipType = null
|
|
210
|
+
let relationshipField = null
|
|
211
|
+
let relationshipDef = null
|
|
212
|
+
|
|
213
|
+
if (currentRelationships) {
|
|
214
|
+
for (const [relName, relDef] of Object.entries(currentRelationships)) {
|
|
215
|
+
if (relDef.type === 'hasMany' && relDef.target === targetScope) {
|
|
216
|
+
if (relDef.via) {
|
|
217
|
+
const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships
|
|
218
|
+
const viaRel = targetRelationships?.[relDef.via]
|
|
219
|
+
|
|
220
|
+
if (viaRel?.belongsToPolymorphic) {
|
|
221
|
+
const { typeField, idField } = viaRel.belongsToPolymorphic
|
|
222
|
+
foundRelationship = targetScope
|
|
223
|
+
relationshipType = 'hasManyPolymorphic'
|
|
224
|
+
relationshipField = { typeField, idField, via: relDef.via }
|
|
225
|
+
relationshipDef = relDef
|
|
226
|
+
log.trace('[BUILD-JOIN] Found polymorphic hasMany relationship:', {
|
|
227
|
+
relName, targetScope, via: relDef.via, typeField, idField
|
|
228
|
+
})
|
|
229
|
+
break
|
|
250
230
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (relDef.through) {
|
|
234
|
+
foundRelationship = targetScope
|
|
235
|
+
relationshipType = 'manyToMany'
|
|
236
|
+
relationshipField = {
|
|
237
|
+
through: relDef.through,
|
|
238
|
+
foreignKey: relDef.foreignKey,
|
|
239
|
+
otherKey: relDef.otherKey
|
|
258
240
|
}
|
|
259
|
-
relationshipDef = relDef
|
|
260
|
-
log.trace('[BUILD-JOIN] Found
|
|
261
|
-
|
|
241
|
+
relationshipDef = relDef
|
|
242
|
+
log.trace('[BUILD-JOIN] Found many-to-many relationship:', {
|
|
243
|
+
relName,
|
|
244
|
+
targetScope,
|
|
245
|
+
through: relDef.through,
|
|
246
|
+
foreignKey: relDef.foreignKey,
|
|
247
|
+
otherKey: relDef.otherKey
|
|
248
|
+
})
|
|
249
|
+
break
|
|
262
250
|
}
|
|
251
|
+
|
|
252
|
+
foundRelationship = targetScope
|
|
253
|
+
relationshipType = 'hasMany'
|
|
254
|
+
relationshipField = relDef.foreignKey
|
|
255
|
+
if (!relationshipField) {
|
|
256
|
+
log.error('[BUILD-JOIN] Missing foreignKey in hasMany relationship:', { relName, currentScope })
|
|
257
|
+
throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${currentScope}'`)
|
|
258
|
+
}
|
|
259
|
+
relationshipDef = relDef
|
|
260
|
+
log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField })
|
|
261
|
+
break
|
|
263
262
|
}
|
|
264
263
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!foundRelationship) {
|
|
267
|
+
for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
|
|
268
|
+
if (fieldDef.belongsTo === targetScope) {
|
|
269
|
+
foundRelationship = targetScope
|
|
270
|
+
relationshipType = 'belongsTo'
|
|
271
|
+
relationshipField = fieldName
|
|
272
|
+
relationshipDef = fieldDef
|
|
273
|
+
log.trace('[BUILD-JOIN] Found belongsTo relationship:', { fieldName, targetScope })
|
|
274
|
+
break
|
|
276
275
|
}
|
|
277
276
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!foundRelationship) {
|
|
280
|
+
throw new Error(
|
|
281
281
|
`No searchable relationship from '${currentScope}' to '${targetScope}'. `
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const sourceSchema = scopes[currentScope].vars.schemaInfo.schemaInstance
|
|
286
|
+
const sourceTableName = scopes[currentScope].vars.schemaInfo.tableName
|
|
287
|
+
const targetSchemaInstance = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
288
|
+
const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
289
|
+
|
|
290
|
+
if (relationshipType === 'manyToMany') {
|
|
291
|
+
const { through, foreignKey, otherKey } = relationshipField
|
|
292
|
+
const pivotTableName = scopes[through].vars.schemaInfo.tableName || through
|
|
293
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
294
|
+
|
|
295
|
+
const pivotAlias = `${currentScope}_to_${through}_${through}`
|
|
296
|
+
const pivotJoinCondition = `${previousAlias}.id = ${pivotAlias}.${foreignKey}`
|
|
297
|
+
|
|
298
|
+
joinChain.push({
|
|
299
|
+
targetTableName: pivotTableName,
|
|
300
|
+
joinAlias: pivotAlias,
|
|
301
|
+
joinCondition: pivotJoinCondition,
|
|
302
|
+
isOneToMany: true,
|
|
303
|
+
isPolymorphic: false,
|
|
304
|
+
relationshipField: foreignKey,
|
|
305
|
+
relationshipType: 'manyToMany_pivot',
|
|
306
|
+
targetScopeName: through
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const targetAlias = `${through}_to_${targetScope}_${targetScope}`
|
|
310
|
+
const targetJoinCondition = `${pivotAlias}.${otherKey} = ${targetAlias}.id`
|
|
311
|
+
|
|
312
|
+
joinChain.push({
|
|
313
|
+
targetTableName,
|
|
314
|
+
joinAlias: targetAlias,
|
|
315
|
+
joinCondition: targetJoinCondition,
|
|
316
|
+
isOneToMany: false,
|
|
317
|
+
isPolymorphic: false,
|
|
318
|
+
relationshipField: otherKey,
|
|
319
|
+
relationshipType: 'manyToMany_target',
|
|
320
|
+
targetScopeName: targetScope
|
|
321
|
+
})
|
|
322
|
+
} else {
|
|
323
|
+
const joinAlias = `${currentScope}_to_${targetScope}_${targetScope}`
|
|
324
|
+
|
|
325
|
+
let joinCondition
|
|
326
|
+
if (relationshipType === 'hasManyPolymorphic') {
|
|
327
|
+
const { typeField, idField } = relationshipField
|
|
328
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
329
|
+
joinCondition = `${joinAlias}.${typeField} = '${currentScope}' AND ${joinAlias}.${idField} = ${previousAlias}.id`
|
|
330
|
+
} else if (relationshipType === 'hasMany') {
|
|
331
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
332
|
+
joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`
|
|
322
333
|
} else {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
let joinCondition;
|
|
326
|
-
if (relationshipType === 'hasManyPolymorphic') {
|
|
327
|
-
const { typeField, idField } = relationshipField;
|
|
328
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
|
|
329
|
-
joinCondition = `${joinAlias}.${typeField} = '${currentScope}' AND ${joinAlias}.${idField} = ${previousAlias}.id`;
|
|
330
|
-
} else if (relationshipType === 'hasMany') {
|
|
331
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
|
|
332
|
-
joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`;
|
|
333
|
-
} else {
|
|
334
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
|
|
335
|
-
joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
joinChain.push({
|
|
339
|
-
targetTableName,
|
|
340
|
-
joinAlias,
|
|
341
|
-
joinCondition,
|
|
342
|
-
isOneToMany: relationshipType === 'hasMany' || relationshipType === 'hasManyPolymorphic',
|
|
343
|
-
isPolymorphic: relationshipType === 'hasManyPolymorphic',
|
|
344
|
-
relationshipField,
|
|
345
|
-
relationshipType,
|
|
346
|
-
targetScopeName: targetScope
|
|
347
|
-
});
|
|
334
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
335
|
+
joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`
|
|
348
336
|
}
|
|
349
|
-
|
|
350
|
-
|
|
337
|
+
|
|
338
|
+
joinChain.push({
|
|
339
|
+
targetTableName,
|
|
340
|
+
joinAlias,
|
|
341
|
+
joinCondition,
|
|
342
|
+
isOneToMany: relationshipType === 'hasMany' || relationshipType === 'hasManyPolymorphic',
|
|
343
|
+
isPolymorphic: relationshipType === 'hasManyPolymorphic',
|
|
344
|
+
relationshipField,
|
|
345
|
+
relationshipType,
|
|
346
|
+
targetScopeName: targetScope
|
|
347
|
+
})
|
|
351
348
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
349
|
+
|
|
350
|
+
currentScope = targetScope
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const finalScope = relationshipPath[relationshipPath.length - 1]
|
|
354
|
+
await validateCrossTableField(scopes, log, finalScope, targetFieldName, searchedScopes)
|
|
355
|
+
|
|
356
|
+
if (joinChain.length === 0) {
|
|
357
|
+
throw new Error(`Invalid path '${targetPath}': no relationships to traverse`)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const lastJoin = joinChain[joinChain.length - 1]
|
|
361
|
+
|
|
362
|
+
if (joinChain.length > 1) {
|
|
363
|
+
return {
|
|
364
|
+
targetTableName: lastJoin.targetTableName,
|
|
365
|
+
joinAlias: lastJoin.joinAlias,
|
|
366
|
+
joinCondition: joinChain.map(j => j.joinCondition).join(' AND '),
|
|
367
|
+
targetField: targetFieldName,
|
|
368
|
+
isOneToMany: joinChain.some(j => j.isOneToMany),
|
|
369
|
+
isMultiLevel: true,
|
|
370
|
+
joinChain,
|
|
371
|
+
targetScopeName: finalScope
|
|
358
372
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
isMultiLevel: true,
|
|
370
|
-
joinChain,
|
|
371
|
-
targetScopeName: finalScope
|
|
372
|
-
};
|
|
373
|
-
} else {
|
|
374
|
-
return {
|
|
375
|
-
joinAlias: lastJoin.joinAlias,
|
|
376
|
-
targetTableName: lastJoin.targetTableName,
|
|
377
|
-
sourceField: lastJoin.relationshipField,
|
|
378
|
-
targetField: targetFieldName,
|
|
379
|
-
joinCondition: lastJoin.joinCondition,
|
|
380
|
-
isOneToMany: lastJoin.isOneToMany,
|
|
381
|
-
isPolymorphic: lastJoin.isPolymorphic,
|
|
382
|
-
targetScopeName: lastJoin.targetScopeName || finalScope
|
|
383
|
-
};
|
|
373
|
+
} else {
|
|
374
|
+
return {
|
|
375
|
+
joinAlias: lastJoin.joinAlias,
|
|
376
|
+
targetTableName: lastJoin.targetTableName,
|
|
377
|
+
sourceField: lastJoin.relationshipField,
|
|
378
|
+
targetField: targetFieldName,
|
|
379
|
+
joinCondition: lastJoin.joinCondition,
|
|
380
|
+
isOneToMany: lastJoin.isOneToMany,
|
|
381
|
+
isPolymorphic: lastJoin.isPolymorphic,
|
|
382
|
+
targetScopeName: lastJoin.targetScopeName || finalScope
|
|
384
383
|
}
|
|
385
|
-
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
386
|
|
|
387
387
|
/**
|
|
388
388
|
* Analyzes a search schema to identify which fields need database indexes
|
|
389
|
-
*
|
|
389
|
+
*
|
|
390
390
|
* @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
|
|
391
391
|
* @param {Object} log - Logger instance for debug and trace output
|
|
392
392
|
* @param {string} scopeName - The scope being analyzed
|
|
393
393
|
* @param {Object} searchSchema - Search schema definition with filter fields
|
|
394
394
|
* @returns {Array<Object>} Array of required indexes with scope, field, and reason
|
|
395
|
-
*
|
|
395
|
+
*
|
|
396
396
|
* @example
|
|
397
397
|
* // Input: Search schema with cross-table references
|
|
398
398
|
* const searchSchema = {
|
|
399
|
-
* authorName: {
|
|
399
|
+
* authorName: {
|
|
400
400
|
* type: 'string',
|
|
401
401
|
* actualField: 'authors.name',
|
|
402
402
|
* filterOperator: 'like'
|
|
403
403
|
* },
|
|
404
404
|
* companyName: {
|
|
405
|
-
* type: 'string',
|
|
405
|
+
* type: 'string',
|
|
406
406
|
* actualField: 'authors.company.name'
|
|
407
407
|
* },
|
|
408
|
-
* search: {
|
|
408
|
+
* search: {
|
|
409
409
|
* type: 'string',
|
|
410
410
|
* oneOf: ['title', 'authors.name', 'authors.company.name']
|
|
411
411
|
* }
|
|
412
412
|
* };
|
|
413
|
-
*
|
|
413
|
+
*
|
|
414
414
|
* const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
|
|
415
|
-
*
|
|
415
|
+
*
|
|
416
416
|
* // Output: List of fields that need indexes
|
|
417
417
|
* // [
|
|
418
418
|
* // { scope: 'authors', field: 'name', reason: 'Cross-table search from articles.authorName' },
|
|
@@ -420,29 +420,29 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
420
420
|
* // { scope: 'authors', field: 'name', reason: 'Cross-table oneOf search from articles.search' },
|
|
421
421
|
* // { scope: 'authors', field: 'company', reason: 'Cross-table oneOf search from articles.search' }
|
|
422
422
|
* // ]
|
|
423
|
-
*
|
|
423
|
+
*
|
|
424
424
|
* @example
|
|
425
425
|
* // Input: No cross-table references
|
|
426
426
|
* const searchSchema = {
|
|
427
427
|
* title: { type: 'string' },
|
|
428
428
|
* status: { type: 'string' }
|
|
429
429
|
* };
|
|
430
|
-
*
|
|
430
|
+
*
|
|
431
431
|
* const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
|
|
432
|
-
*
|
|
432
|
+
*
|
|
433
433
|
* // Output: Empty array - no cross-table indexes needed
|
|
434
434
|
* // []
|
|
435
|
-
*
|
|
435
|
+
*
|
|
436
436
|
* @description
|
|
437
437
|
* Used by:
|
|
438
438
|
* - Called during API initialization to identify missing indexes
|
|
439
439
|
* - Used by developers to understand performance requirements
|
|
440
|
-
*
|
|
440
|
+
*
|
|
441
441
|
* Purpose:
|
|
442
442
|
* - Cross-table searches require indexes for acceptable performance
|
|
443
443
|
* - Helps identify configuration issues before they cause slow queries
|
|
444
444
|
* - Provides clear documentation of index requirements
|
|
445
|
-
*
|
|
445
|
+
*
|
|
446
446
|
* Data flow:
|
|
447
447
|
* - Runs during schema compilation phase
|
|
448
448
|
* - Analyzes search schemas to find cross-table references
|
|
@@ -450,89 +450,89 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
450
450
|
* - Prevents performance issues before queries are executed
|
|
451
451
|
*/
|
|
452
452
|
export const analyzeRequiredIndexes = (scopes, log, scopeName, schemaInfo) => {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
453
|
+
const requiredIndexes = []
|
|
454
|
+
|
|
455
|
+
const schemaToAnalyze = schemaInfo.searchSchemaStructure
|
|
456
|
+
|
|
457
|
+
Object.entries(schemaToAnalyze).forEach(([filterKey, fieldDef]) => {
|
|
458
|
+
if (fieldDef.actualField && fieldDef.actualField.includes('.')) {
|
|
459
|
+
const [targetScopeName, targetFieldName] = fieldDef.actualField.split('.')
|
|
460
|
+
requiredIndexes.push({
|
|
461
|
+
scope: targetScopeName,
|
|
462
|
+
field: targetFieldName,
|
|
463
|
+
reason: `Cross-table search from ${scopeName}.${filterKey}`
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
|
|
468
|
+
fieldDef.oneOf.forEach(field => {
|
|
469
|
+
if (field.includes('.')) {
|
|
470
|
+
const [targetScopeName, targetFieldName] = field.split('.')
|
|
471
|
+
requiredIndexes.push({
|
|
472
|
+
scope: targetScopeName,
|
|
473
|
+
field: targetFieldName,
|
|
474
|
+
reason: `Cross-table oneOf search from ${scopeName}.${filterKey}`
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
return requiredIndexes
|
|
482
|
+
}
|
|
483
483
|
|
|
484
484
|
/**
|
|
485
485
|
* Creates database indexes for fields identified by analyzeRequiredIndexes
|
|
486
|
-
*
|
|
486
|
+
*
|
|
487
487
|
* @async
|
|
488
488
|
* @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
|
|
489
489
|
* @param {Object} log - Logger instance for debug and trace output
|
|
490
490
|
* @param {Array<Object>} requiredIndexes - Index requirements from analyzeRequiredIndexes
|
|
491
491
|
* @param {Object} knex - Knex database connection instance
|
|
492
492
|
* @returns {Promise<Array<Object>>} Array of successfully created indexes
|
|
493
|
-
*
|
|
493
|
+
*
|
|
494
494
|
* @example
|
|
495
495
|
* // Input: Required indexes from analysis
|
|
496
496
|
* const requiredIndexes = [
|
|
497
497
|
* { scope: 'authors', field: 'name', reason: 'Cross-table search' },
|
|
498
498
|
* { scope: 'companies', field: 'name', reason: 'Cross-table search' }
|
|
499
499
|
* ];
|
|
500
|
-
*
|
|
500
|
+
*
|
|
501
501
|
* const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
|
|
502
|
-
*
|
|
502
|
+
*
|
|
503
503
|
* // Output: Successfully created indexes
|
|
504
504
|
* // [
|
|
505
505
|
* // { tableName: 'authors', field: 'name', indexName: 'idx_authors_name_search' },
|
|
506
506
|
* // { tableName: 'companies', field: 'name', indexName: 'idx_companies_name_search' }
|
|
507
507
|
* // ]
|
|
508
|
-
*
|
|
508
|
+
*
|
|
509
509
|
* // Database effect: CREATE INDEX idx_authors_name_search ON authors(name);
|
|
510
510
|
* // Database effect: CREATE INDEX idx_companies_name_search ON companies(name);
|
|
511
|
-
*
|
|
511
|
+
*
|
|
512
512
|
* @example
|
|
513
513
|
* // Input: Some indexes already exist
|
|
514
514
|
* const requiredIndexes = [
|
|
515
515
|
* { scope: 'authors', field: 'name', reason: 'Cross-table search' }, // Already exists
|
|
516
516
|
* { scope: 'authors', field: 'email', reason: 'Cross-table search' } // New
|
|
517
517
|
* ];
|
|
518
|
-
*
|
|
518
|
+
*
|
|
519
519
|
* const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
|
|
520
|
-
*
|
|
520
|
+
*
|
|
521
521
|
* // Output: Only newly created indexes
|
|
522
522
|
* // [
|
|
523
523
|
* // { tableName: 'authors', field: 'email', indexName: 'idx_authors_email_search' }
|
|
524
524
|
* // ]
|
|
525
|
-
*
|
|
525
|
+
*
|
|
526
526
|
* @description
|
|
527
527
|
* Used by:
|
|
528
528
|
* - Can be called during database migration or setup
|
|
529
529
|
* - Used by developers to automatically create performance indexes
|
|
530
|
-
*
|
|
530
|
+
*
|
|
531
531
|
* Purpose:
|
|
532
532
|
* - Automates index creation based on search schema requirements
|
|
533
533
|
* - Ensures consistent index naming across the application
|
|
534
534
|
* - Safely handles cases where indexes already exist
|
|
535
|
-
*
|
|
535
|
+
*
|
|
536
536
|
* Data flow:
|
|
537
537
|
* - Typically runs during database setup or migration
|
|
538
538
|
* - Creates indexes identified by analyzeRequiredIndexes
|
|
@@ -540,26 +540,26 @@ export const analyzeRequiredIndexes = (scopes, log, scopeName, schemaInfo) => {
|
|
|
540
540
|
* - Part of the database optimization phase
|
|
541
541
|
*/
|
|
542
542
|
export const createRequiredIndexes = async (scopes, log, requiredIndexes, knex) => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
} catch (error) {
|
|
560
|
-
log.warn(`Failed to create index on ${tableName}.${field}:`, error.message);
|
|
543
|
+
const createdIndexes = []
|
|
544
|
+
|
|
545
|
+
for (const indexInfo of requiredIndexes) {
|
|
546
|
+
const { scope, field } = indexInfo
|
|
547
|
+
const tableName = scopes[scope].vars.schemaInfo.tableName
|
|
548
|
+
const indexName = `idx_${tableName}_${field}_search`
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const hasIndex = await knex.schema.hasIndex(tableName, [field])
|
|
552
|
+
if (!hasIndex) {
|
|
553
|
+
await knex.schema.table(tableName, table => {
|
|
554
|
+
table.index([field], indexName)
|
|
555
|
+
})
|
|
556
|
+
createdIndexes.push({ tableName, field, indexName })
|
|
557
|
+
log.info(`Created index: ${indexName} on ${tableName}.${field}`)
|
|
561
558
|
}
|
|
559
|
+
} catch (error) {
|
|
560
|
+
log.warn(`Failed to create index on ${tableName}.${field}:`, error.message)
|
|
562
561
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return createdIndexes
|
|
565
|
+
}
|