json-rest-api 1.0.21 → 1.0.22
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/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +3 -3
|
@@ -0,0 +1,565 @@
|
|
|
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
|
|
9
|
+
9 * @param {string} targetScopeName - The scope containing the field to validate
|
|
10
|
+
10 * @param {string} fieldName - The field name to check
|
|
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
|
|
25
|
+
25 * try {
|
|
26
|
+
26 * await validateCrossTableField('companies', 'internal_code');
|
|
27
|
+
27 * } catch (error) {
|
|
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
|
+
48 log.trace('[VALIDATE] Starting validateCrossTableField:', { targetScopeName, fieldName, searchedScopes: Array.from(searchedScopes) })
|
|
49
|
+
49
|
|
50
|
+
50 if (searchedScopes.has(targetScopeName)) {
|
|
51
|
+
51 throw new Error(`Circular reference detected: ${targetScopeName} -> ${Array.from(searchedScopes).join(' -> ')}`)
|
|
52
|
+
52 }
|
|
53
|
+
53
|
|
54
|
+
54 log.trace('[VALIDATE] Getting target schema for:', targetScopeName)
|
|
55
|
+
55 let targetSchemaInstance
|
|
56
|
+
56 try {
|
|
57
|
+
57 targetSchemaInstance = scopes[targetScopeName].vars.schemaInfo.schemaInstance
|
|
58
|
+
58 log.trace('[VALIDATE] Got target schema:', { scopeName: targetScopeName, schemaKeys: Object.keys(targetSchemaInstance || {}) })
|
|
59
|
+
59 } catch (error) {
|
|
60
|
+
60 log.trace('[VALIDATE] Error getting target schema:', error.message)
|
|
61
|
+
61 throw new Error(`Target scope '${targetScopeName}' not found`)
|
|
62
|
+
62 }
|
|
63
|
+
63
|
|
64
|
+
64 if (!targetSchemaInstance) {
|
|
65
|
+
65 log.trace('[VALIDATE] Target schema is null/undefined')
|
|
66
|
+
66 throw new Error(`Target scope '${targetScopeName}' has no schema`)
|
|
67
|
+
67 }
|
|
68
|
+
68
|
|
69
|
+
69 const fieldDef = targetSchemaInstance.structure[fieldName]
|
|
70
|
+
70 log.trace('[VALIDATE] Field lookup result:', { fieldName, fieldExists: !!fieldDef })
|
|
71
|
+
71 if (!fieldDef) {
|
|
72
|
+
72 throw new Error(`Field '${fieldName}' not found in scope '${targetScopeName}'`)
|
|
73
|
+
73 }
|
|
74
|
+
74
|
|
75
|
+
75 log.trace('[VALIDATE] Checking indexed status:', { fieldName, indexed: fieldDef.indexed })
|
|
76
|
+
76 if (!fieldDef.indexed) {
|
|
77
|
+
77 throw new Error(`Field '${targetScopeName}.${fieldName}' is not indexed. Add 'indexed: true' to allow cross-table search`)
|
|
78
|
+
78 }
|
|
79
|
+
79
|
|
80
|
+
80 log.trace('[VALIDATE] Validation successful for field:', `${targetScopeName}.${fieldName}`)
|
|
81
|
+
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
|
|
89
|
+
89 * @param {string} fromScopeName - Starting scope (e.g., 'articles')
|
|
90
|
+
90 * @param {string} targetPath - Dot-separated path to target field (e.g., 'author.company.name')
|
|
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',
|
|
103
|
+
103 * // targetTableName: 'authors',
|
|
104
|
+
104 * // sourceField: 'author_id',
|
|
105
|
+
105 * // targetField: 'name',
|
|
106
|
+
106 * // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
|
|
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',
|
|
119
|
+
119 * // joinAlias: 'authors_to_companies_companies',
|
|
120
|
+
120 * // joinCondition: 'articles.author_id = articles_to_authors_authors.id AND articles_to_authors_authors.company_id = authors_to_companies_companies.id',
|
|
121
|
+
121 * // targetField: 'name',
|
|
122
|
+
122 * // isOneToMany: false,
|
|
123
|
+
123 * // isMultiLevel: true,
|
|
124
|
+
124 * // joinChain: [
|
|
125
|
+
125 * // {
|
|
126
|
+
126 * // targetTableName: 'authors',
|
|
127
|
+
127 * // joinAlias: 'articles_to_authors_authors',
|
|
128
|
+
128 * // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
|
|
129
|
+
129 * // isOneToMany: false,
|
|
130
|
+
130 * // relationshipField: 'author_id',
|
|
131
|
+
131 * // relationshipType: 'belongsTo'
|
|
132
|
+
132 * // },
|
|
133
|
+
133 * // {
|
|
134
|
+
134 * // targetTableName: 'companies',
|
|
135
|
+
135 * // joinAlias: 'authors_to_companies_companies',
|
|
136
|
+
136 * // joinCondition: 'articles_to_authors_authors.company_id = authors_to_companies_companies.id',
|
|
137
|
+
137 * // isOneToMany: false,
|
|
138
|
+
138 * // relationshipField: 'company_id',
|
|
139
|
+
139 * // relationshipType: 'belongsTo'
|
|
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',
|
|
152
|
+
152 * // targetTableName: 'articles',
|
|
153
|
+
153 * // sourceField: 'author_id',
|
|
154
|
+
154 * // targetField: 'title',
|
|
155
|
+
155 * // joinCondition: 'authors.id = authors_to_articles_articles.author_id',
|
|
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
|
|
173
|
+
173 * 3. For each segment, finds the appropriate relationship definition
|
|
174
|
+
174 * 4. Constructs JOIN conditions based on foreign keys
|
|
175
|
+
175 * 5. Returns complete JOIN information for SQL query builder
|
|
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
|
+
179 log.trace('[BUILD-JOIN] Starting buildJoinChain:', { fromScopeName, targetPath })
|
|
180
|
+
180
|
|
181
|
+
181 const pathSegments = targetPath.split('.')
|
|
182
|
+
182 if (pathSegments.length < 2) {
|
|
183
|
+
183 throw new Error(`Invalid cross-table path '${targetPath}'. Must contain at least 'scope.field'`)
|
|
184
|
+
184 }
|
|
185
|
+
185
|
|
186
|
+
186 const targetFieldName = pathSegments[pathSegments.length - 1]
|
|
187
|
+
187 const relationshipPath = pathSegments.slice(0, -1)
|
|
188
|
+
188
|
|
189
|
+
189 log.trace('[BUILD-JOIN] Parsed path:', { relationshipPath, targetFieldName })
|
|
190
|
+
190
|
|
191
|
+
191 const joinChain = []
|
|
192
|
+
192 let currentScope = fromScopeName
|
|
193
|
+
193
|
|
194
|
+
194 for (let i = 0; i < relationshipPath.length; i++) {
|
|
195
|
+
195 const targetScope = relationshipPath[i]
|
|
196
|
+
196
|
|
197
|
+
197 log.trace('[BUILD-JOIN] Looking for direct relationship:', { from: currentScope, to: targetScope })
|
|
198
|
+
198
|
|
199
|
+
199 if (searchedScopes.has(currentScope)) {
|
|
200
|
+
200 throw new Error(`Circular reference detected in path: ${Array.from(searchedScopes).join(' -> ')} -> ${currentScope}`)
|
|
201
|
+
201 }
|
|
202
|
+
202 searchedScopes.add(currentScope)
|
|
203
|
+
203
|
|
204
|
+
204 const schemaInfo = scopes[currentScope].vars.schemaInfo
|
|
205
|
+
205 const currentSchema = schemaInfo.schemaInstance
|
|
206
|
+
206 const currentRelationships = schemaInfo.schemaRelationships
|
|
207
|
+
207
|
|
208
|
+
208 let foundRelationship = null
|
|
209
|
+
209 let relationshipType = null
|
|
210
|
+
210 let relationshipField = null
|
|
211
|
+
211 let relationshipDef = null
|
|
212
|
+
212
|
|
213
|
+
213 if (currentRelationships) {
|
|
214
|
+
214 for (const [relName, relDef] of Object.entries(currentRelationships)) {
|
|
215
|
+
215 if (relDef.type === 'hasMany' && relDef.target === targetScope) {
|
|
216
|
+
216 if (relDef.via) {
|
|
217
|
+
217 const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships
|
|
218
|
+
218 const viaRel = targetRelationships?.[relDef.via]
|
|
219
|
+
219
|
|
220
|
+
220 if (viaRel?.belongsToPolymorphic) {
|
|
221
|
+
221 const { typeField, idField } = viaRel.belongsToPolymorphic
|
|
222
|
+
222 foundRelationship = targetScope
|
|
223
|
+
223 relationshipType = 'hasManyPolymorphic'
|
|
224
|
+
224 relationshipField = { typeField, idField, via: relDef.via }
|
|
225
|
+
225 relationshipDef = relDef
|
|
226
|
+
226 log.trace('[BUILD-JOIN] Found polymorphic hasMany relationship:', {
|
|
227
|
+
227 relName, targetScope, via: relDef.via, typeField, idField
|
|
228
|
+
228 })
|
|
229
|
+
229 break
|
|
230
|
+
230 }
|
|
231
|
+
231 }
|
|
232
|
+
232
|
|
233
|
+
233 if (relDef.through) {
|
|
234
|
+
234 foundRelationship = targetScope
|
|
235
|
+
235 relationshipType = 'manyToMany'
|
|
236
|
+
236 relationshipField = {
|
|
237
|
+
237 through: relDef.through,
|
|
238
|
+
238 foreignKey: relDef.foreignKey,
|
|
239
|
+
239 otherKey: relDef.otherKey
|
|
240
|
+
240 }
|
|
241
|
+
241 relationshipDef = relDef
|
|
242
|
+
242 log.trace('[BUILD-JOIN] Found many-to-many relationship:', {
|
|
243
|
+
243 relName,
|
|
244
|
+
244 targetScope,
|
|
245
|
+
245 through: relDef.through,
|
|
246
|
+
246 foreignKey: relDef.foreignKey,
|
|
247
|
+
247 otherKey: relDef.otherKey
|
|
248
|
+
248 })
|
|
249
|
+
249 break
|
|
250
|
+
250 }
|
|
251
|
+
251
|
|
252
|
+
252 foundRelationship = targetScope
|
|
253
|
+
253 relationshipType = 'hasMany'
|
|
254
|
+
254 relationshipField = relDef.foreignKey
|
|
255
|
+
255 if (!relationshipField) {
|
|
256
|
+
256 log.error('[BUILD-JOIN] Missing foreignKey in hasMany relationship:', { relName, currentScope })
|
|
257
|
+
257 throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${currentScope}'`)
|
|
258
|
+
258 }
|
|
259
|
+
259 relationshipDef = relDef
|
|
260
|
+
260 log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField })
|
|
261
|
+
261 break
|
|
262
|
+
262 }
|
|
263
|
+
263 }
|
|
264
|
+
264 }
|
|
265
|
+
265
|
|
266
|
+
266 if (!foundRelationship) {
|
|
267
|
+
267 for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
|
|
268
|
+
268 if (fieldDef.belongsTo === targetScope) {
|
|
269
|
+
269 foundRelationship = targetScope
|
|
270
|
+
270 relationshipType = 'belongsTo'
|
|
271
|
+
271 relationshipField = fieldName
|
|
272
|
+
272 relationshipDef = fieldDef
|
|
273
|
+
273 log.trace('[BUILD-JOIN] Found belongsTo relationship:', { fieldName, targetScope })
|
|
274
|
+
274 break
|
|
275
|
+
275 }
|
|
276
|
+
276 }
|
|
277
|
+
277 }
|
|
278
|
+
278
|
|
279
|
+
279 if (!foundRelationship) {
|
|
280
|
+
280 throw new Error(
|
|
281
|
+
281 `No searchable relationship from '${currentScope}' to '${targetScope}'. `
|
|
282
|
+
282 )
|
|
283
|
+
283 }
|
|
284
|
+
284
|
|
285
|
+
285 const sourceSchema = scopes[currentScope].vars.schemaInfo.schemaInstance
|
|
286
|
+
286 const sourceTableName = scopes[currentScope].vars.schemaInfo.tableName
|
|
287
|
+
287 const targetSchemaInstance = scopes[targetScope].vars.schemaInfo.schemaInstance
|
|
288
|
+
288 const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
289
|
+
289
|
|
290
|
+
290 if (relationshipType === 'manyToMany') {
|
|
291
|
+
291 const { through, foreignKey, otherKey } = relationshipField
|
|
292
|
+
292 const pivotTableName = scopes[through].vars.schemaInfo.tableName || through
|
|
293
|
+
293 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
294
|
+
294
|
|
295
|
+
295 const pivotAlias = `${currentScope}_to_${through}_${through}`
|
|
296
|
+
296 const pivotJoinCondition = `${previousAlias}.id = ${pivotAlias}.${foreignKey}`
|
|
297
|
+
297
|
|
298
|
+
298 joinChain.push({
|
|
299
|
+
299 targetTableName: pivotTableName,
|
|
300
|
+
300 joinAlias: pivotAlias,
|
|
301
|
+
301 joinCondition: pivotJoinCondition,
|
|
302
|
+
302 isOneToMany: true,
|
|
303
|
+
303 isPolymorphic: false,
|
|
304
|
+
304 relationshipField: foreignKey,
|
|
305
|
+
305 relationshipType: 'manyToMany_pivot',
|
|
306
|
+
306 targetScopeName: through
|
|
307
|
+
307 })
|
|
308
|
+
308
|
|
309
|
+
309 const targetAlias = `${through}_to_${targetScope}_${targetScope}`
|
|
310
|
+
310 const targetJoinCondition = `${pivotAlias}.${otherKey} = ${targetAlias}.id`
|
|
311
|
+
311
|
|
312
|
+
312 joinChain.push({
|
|
313
|
+
313 targetTableName,
|
|
314
|
+
314 joinAlias: targetAlias,
|
|
315
|
+
315 joinCondition: targetJoinCondition,
|
|
316
|
+
316 isOneToMany: false,
|
|
317
|
+
317 isPolymorphic: false,
|
|
318
|
+
318 relationshipField: otherKey,
|
|
319
|
+
319 relationshipType: 'manyToMany_target',
|
|
320
|
+
320 targetScopeName: targetScope
|
|
321
|
+
321 })
|
|
322
|
+
322 } else {
|
|
323
|
+
323 const joinAlias = `${currentScope}_to_${targetScope}_${targetScope}`
|
|
324
|
+
324
|
|
325
|
+
325 let joinCondition
|
|
326
|
+
326 if (relationshipType === 'hasManyPolymorphic') {
|
|
327
|
+
327 const { typeField, idField } = relationshipField
|
|
328
|
+
328 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
329
|
+
329 joinCondition = `${joinAlias}.${typeField} = '${currentScope}' AND ${joinAlias}.${idField} = ${previousAlias}.id`
|
|
330
|
+
330 } else if (relationshipType === 'hasMany') {
|
|
331
|
+
331 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
332
|
+
332 joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`
|
|
333
|
+
333 } else {
|
|
334
|
+
334 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
|
|
335
|
+
335 joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`
|
|
336
|
+
336 }
|
|
337
|
+
337
|
|
338
|
+
338 joinChain.push({
|
|
339
|
+
339 targetTableName,
|
|
340
|
+
340 joinAlias,
|
|
341
|
+
341 joinCondition,
|
|
342
|
+
342 isOneToMany: relationshipType === 'hasMany' || relationshipType === 'hasManyPolymorphic',
|
|
343
|
+
343 isPolymorphic: relationshipType === 'hasManyPolymorphic',
|
|
344
|
+
344 relationshipField,
|
|
345
|
+
345 relationshipType,
|
|
346
|
+
346 targetScopeName: targetScope
|
|
347
|
+
347 })
|
|
348
|
+
348 }
|
|
349
|
+
349
|
|
350
|
+
350 currentScope = targetScope
|
|
351
|
+
351 }
|
|
352
|
+
352
|
|
353
|
+
353 const finalScope = relationshipPath[relationshipPath.length - 1]
|
|
354
|
+
354 await validateCrossTableField(scopes, log, finalScope, targetFieldName, searchedScopes)
|
|
355
|
+
355
|
|
356
|
+
356 if (joinChain.length === 0) {
|
|
357
|
+
357 throw new Error(`Invalid path '${targetPath}': no relationships to traverse`)
|
|
358
|
+
358 }
|
|
359
|
+
359
|
|
360
|
+
360 const lastJoin = joinChain[joinChain.length - 1]
|
|
361
|
+
361
|
|
362
|
+
362 if (joinChain.length > 1) {
|
|
363
|
+
363 return {
|
|
364
|
+
364 targetTableName: lastJoin.targetTableName,
|
|
365
|
+
365 joinAlias: lastJoin.joinAlias,
|
|
366
|
+
366 joinCondition: joinChain.map(j => j.joinCondition).join(' AND '),
|
|
367
|
+
367 targetField: targetFieldName,
|
|
368
|
+
368 isOneToMany: joinChain.some(j => j.isOneToMany),
|
|
369
|
+
369 isMultiLevel: true,
|
|
370
|
+
370 joinChain,
|
|
371
|
+
371 targetScopeName: finalScope
|
|
372
|
+
372 }
|
|
373
|
+
373 } else {
|
|
374
|
+
374 return {
|
|
375
|
+
375 joinAlias: lastJoin.joinAlias,
|
|
376
|
+
376 targetTableName: lastJoin.targetTableName,
|
|
377
|
+
377 sourceField: lastJoin.relationshipField,
|
|
378
|
+
378 targetField: targetFieldName,
|
|
379
|
+
379 joinCondition: lastJoin.joinCondition,
|
|
380
|
+
380 isOneToMany: lastJoin.isOneToMany,
|
|
381
|
+
381 isPolymorphic: lastJoin.isPolymorphic,
|
|
382
|
+
382 targetScopeName: lastJoin.targetScopeName || finalScope
|
|
383
|
+
383 }
|
|
384
|
+
384 }
|
|
385
|
+
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
|
+
399 * authorName: {
|
|
400
|
+
400 * type: 'string',
|
|
401
|
+
401 * actualField: 'authors.name',
|
|
402
|
+
402 * filterOperator: 'like'
|
|
403
|
+
403 * },
|
|
404
|
+
404 * companyName: {
|
|
405
|
+
405 * type: 'string',
|
|
406
|
+
406 * actualField: 'authors.company.name'
|
|
407
|
+
407 * },
|
|
408
|
+
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' },
|
|
419
|
+
419 * // { scope: 'authors', field: 'company', reason: 'Cross-table search from articles.companyName' },
|
|
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
|
|
449
|
+
449 * - Output can be used to create indexes manually or automatically
|
|
450
|
+
450 * - Prevents performance issues before queries are executed
|
|
451
|
+
451 */
|
|
452
|
+
452 export const analyzeRequiredIndexes = (scopes, log, scopeName, schemaInfo) => {
|
|
453
|
+
453 const requiredIndexes = []
|
|
454
|
+
454
|
|
455
|
+
455 const schemaToAnalyze = schemaInfo.searchSchemaStructure
|
|
456
|
+
456
|
|
457
|
+
457 Object.entries(schemaToAnalyze).forEach(([filterKey, fieldDef]) => {
|
|
458
|
+
458 if (fieldDef.actualField && fieldDef.actualField.includes('.')) {
|
|
459
|
+
459 const [targetScopeName, targetFieldName] = fieldDef.actualField.split('.')
|
|
460
|
+
460 requiredIndexes.push({
|
|
461
|
+
461 scope: targetScopeName,
|
|
462
|
+
462 field: targetFieldName,
|
|
463
|
+
463 reason: `Cross-table search from ${scopeName}.${filterKey}`
|
|
464
|
+
464 })
|
|
465
|
+
465 }
|
|
466
|
+
466
|
|
467
|
+
467 if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
|
|
468
|
+
468 fieldDef.oneOf.forEach(field => {
|
|
469
|
+
469 if (field.includes('.')) {
|
|
470
|
+
470 const [targetScopeName, targetFieldName] = field.split('.')
|
|
471
|
+
471 requiredIndexes.push({
|
|
472
|
+
472 scope: targetScopeName,
|
|
473
|
+
473 field: targetFieldName,
|
|
474
|
+
474 reason: `Cross-table oneOf search from ${scopeName}.${filterKey}`
|
|
475
|
+
475 })
|
|
476
|
+
476 }
|
|
477
|
+
477 })
|
|
478
|
+
478 }
|
|
479
|
+
479 })
|
|
480
|
+
480
|
|
481
|
+
481 return requiredIndexes
|
|
482
|
+
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
|
|
539
|
+
539 * - Improves query performance for cross-table searches
|
|
540
|
+
540 * - Part of the database optimization phase
|
|
541
|
+
541 */
|
|
542
|
+
542 export const createRequiredIndexes = async (scopes, log, requiredIndexes, knex) => {
|
|
543
|
+
543 const createdIndexes = []
|
|
544
|
+
544
|
|
545
|
+
545 for (const indexInfo of requiredIndexes) {
|
|
546
|
+
546 const { scope, field } = indexInfo
|
|
547
|
+
547 const tableName = scopes[scope].vars.schemaInfo.tableName
|
|
548
|
+
548 const indexName = `idx_${tableName}_${field}_search`
|
|
549
|
+
549
|
|
550
|
+
550 try {
|
|
551
|
+
551 const hasIndex = await knex.schema.hasIndex(tableName, [field])
|
|
552
|
+
552 if (!hasIndex) {
|
|
553
|
+
553 await knex.schema.table(tableName, table => {
|
|
554
|
+
554 table.index([field], indexName)
|
|
555
|
+
555 })
|
|
556
|
+
556 createdIndexes.push({ tableName, field, indexName })
|
|
557
|
+
557 log.info(`Created index: ${indexName} on ${tableName}.${field}`)
|
|
558
|
+
558 }
|
|
559
|
+
559 } catch (error) {
|
|
560
|
+
560 log.warn(`Failed to create index on ${tableName}.${field}:`, error.message)
|
|
561
|
+
561 }
|
|
562
|
+
562 }
|
|
563
|
+
563
|
|
564
|
+
564 return createdIndexes
|
|
565
|
+
565 }
|