json-rest-api 1.0.28 → 2.0.0
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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +746 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +142 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1069
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -385
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +38 -69
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +39 -82
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -150
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -118
- package/tests/fixtures/api-configs.js +0 -2509
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-cursor-multifield.test.js +0 -643
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,48 +1,18 @@
|
|
|
1
|
+
import { assertScalarQueryField } from '../querying-writing/field-utils.js'
|
|
2
|
+
import { getOperationDiagnosticContext } from '../../../../lib/error-context.js'
|
|
1
3
|
// Cross-table search helper functions that enable filtering across related database tables
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Validates that a field exists and is indexed for cross-table search
|
|
5
7
|
*
|
|
6
8
|
* @async
|
|
7
|
-
* @param {Object} scopes - All registered
|
|
9
|
+
* @param {Object} scopes - All registered resources containing schema and relationship definitions
|
|
8
10
|
* @param {Object} log - Logger instance for debug and trace output
|
|
9
11
|
* @param {string} targetScopeName - The scope containing the field to validate
|
|
10
12
|
* @param {string} fieldName - The field name to check
|
|
11
13
|
* @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
|
|
12
14
|
* @returns {Promise<void>}
|
|
13
15
|
* @throws {Error} If scope not found, field not found, field not indexed, or circular reference detected
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* // Input: Validate indexed field
|
|
17
|
-
* // Schema has: companies.name with indexed: true
|
|
18
|
-
* await validateCrossTableField(scopes, log, 'companies', 'name');
|
|
19
|
-
*
|
|
20
|
-
* // Output: Validation succeeds (no error thrown)
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* // Input: Non-indexed field
|
|
24
|
-
* // Schema has: companies.internal_code without indexed property
|
|
25
|
-
* try {
|
|
26
|
-
* await validateCrossTableField('companies', 'internal_code');
|
|
27
|
-
* } catch (error) {
|
|
28
|
-
* console.log(error.message);
|
|
29
|
-
* // "Field 'companies.internal_code' is not indexed. Add 'indexed: true' to allow cross-table search"
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* @description
|
|
33
|
-
* Used by:
|
|
34
|
-
* - buildJoinChain calls this to ensure target fields are properly indexed
|
|
35
|
-
* - analyzeRequiredIndexes uses this to identify missing indexes
|
|
36
|
-
*
|
|
37
|
-
* Purpose:
|
|
38
|
-
* - Cross-table searches can be slow without proper indexes
|
|
39
|
-
* - Forces developers to explicitly mark searchable fields with indexed: true
|
|
40
|
-
* - Provides clear error messages when configuration is missing
|
|
41
|
-
*
|
|
42
|
-
* Data flow:
|
|
43
|
-
* - Called during query building to validate field configuration
|
|
44
|
-
* - Ensures performance by requiring indexes on searchable fields
|
|
45
|
-
* - Part of the validation phase before expensive JOIN operations
|
|
46
16
|
*/
|
|
47
17
|
export const validateCrossTableField = async (scopes, log, targetScopeName, fieldName, searchedScopes = new Set()) => {
|
|
48
18
|
log.trace('[VALIDATE] Starting validateCrossTableField:', { targetScopeName, fieldName, searchedScopes: Array.from(searchedScopes) })
|
|
@@ -57,8 +27,7 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
57
27
|
targetSchemaInstance = scopes[targetScopeName].vars.schemaInfo.schemaInstance
|
|
58
28
|
log.trace('[VALIDATE] Got target schema:', { scopeName: targetScopeName, schemaKeys: Object.keys(targetSchemaInstance || {}) })
|
|
59
29
|
} catch (error) {
|
|
60
|
-
|
|
61
|
-
throw new Error(`Target scope '${targetScopeName}' not found`)
|
|
30
|
+
throw new Error(`Target scope '${targetScopeName}' not found`, { cause: error })
|
|
62
31
|
}
|
|
63
32
|
|
|
64
33
|
if (!targetSchemaInstance) {
|
|
@@ -72,6 +41,8 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
72
41
|
throw new Error(`Field '${fieldName}' not found in scope '${targetScopeName}'`)
|
|
73
42
|
}
|
|
74
43
|
|
|
44
|
+
assertScalarQueryField(fieldDef, `${targetScopeName}.${fieldName}`, 'filter')
|
|
45
|
+
|
|
75
46
|
log.trace('[VALIDATE] Checking indexed status:', { fieldName, indexed: fieldDef.indexed })
|
|
76
47
|
if (!fieldDef.indexed) {
|
|
77
48
|
throw new Error(`Field '${targetScopeName}.${fieldName}' is not indexed. Add 'indexed: true' to allow cross-table search`)
|
|
@@ -84,96 +55,13 @@ export const validateCrossTableField = async (scopes, log, targetScopeName, fiel
|
|
|
84
55
|
* Builds a chain of SQL JOINs to reach a field in a related table
|
|
85
56
|
*
|
|
86
57
|
* @async
|
|
87
|
-
* @param {Object} scopes - All registered
|
|
58
|
+
* @param {Object} scopes - All registered resources containing schema and relationship definitions
|
|
88
59
|
* @param {Object} log - Logger instance for debug and trace output
|
|
89
60
|
* @param {string} fromScopeName - Starting scope (e.g., 'articles')
|
|
90
61
|
* @param {string} targetPath - Dot-separated path to target field (e.g., 'author.company.name')
|
|
91
62
|
* @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
|
|
92
63
|
* @returns {Promise<Object>} JOIN information for query builder
|
|
93
64
|
* @throws {Error} If path is invalid or relationships not properly configured
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* // Input: Simple belongsTo relationship
|
|
97
|
-
* // articles table has author_id field, authors schema defines belongsTo relationship
|
|
98
|
-
* const joinInfo = await buildJoinChain('articles', 'author.name');
|
|
99
|
-
*
|
|
100
|
-
* // Output: Single JOIN configuration
|
|
101
|
-
* // {
|
|
102
|
-
* // joinAlias: 'articles_to_authors_authors',
|
|
103
|
-
* // targetTableName: 'authors',
|
|
104
|
-
* // sourceField: 'author_id',
|
|
105
|
-
* // targetField: 'name',
|
|
106
|
-
* // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
|
|
107
|
-
* // isOneToMany: false,
|
|
108
|
-
* // isPolymorphic: false
|
|
109
|
-
* // }
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* // Input: Multi-level path through relationships
|
|
113
|
-
* // articles → authors → companies
|
|
114
|
-
* const joinInfo = await buildJoinChain('articles', 'author.company.name');
|
|
115
|
-
*
|
|
116
|
-
* // Output: Multi-level JOIN chain
|
|
117
|
-
* // {
|
|
118
|
-
* // targetTableName: 'companies',
|
|
119
|
-
* // joinAlias: 'authors_to_companies_companies',
|
|
120
|
-
* // joinCondition: 'articles.author_id = articles_to_authors_authors.id AND articles_to_authors_authors.company_id = authors_to_companies_companies.id',
|
|
121
|
-
* // targetField: 'name',
|
|
122
|
-
* // isOneToMany: false,
|
|
123
|
-
* // isMultiLevel: true,
|
|
124
|
-
* // joinChain: [
|
|
125
|
-
* // {
|
|
126
|
-
* // targetTableName: 'authors',
|
|
127
|
-
* // joinAlias: 'articles_to_authors_authors',
|
|
128
|
-
* // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
|
|
129
|
-
* // isOneToMany: false,
|
|
130
|
-
* // relationshipField: 'author_id',
|
|
131
|
-
* // relationshipType: 'belongsTo'
|
|
132
|
-
* // },
|
|
133
|
-
* // {
|
|
134
|
-
* // targetTableName: 'companies',
|
|
135
|
-
* // joinAlias: 'authors_to_companies_companies',
|
|
136
|
-
* // joinCondition: 'articles_to_authors_authors.company_id = authors_to_companies_companies.id',
|
|
137
|
-
* // isOneToMany: false,
|
|
138
|
-
* // relationshipField: 'company_id',
|
|
139
|
-
* // relationshipType: 'belongsTo'
|
|
140
|
-
* // }
|
|
141
|
-
* // ]
|
|
142
|
-
* // }
|
|
143
|
-
*
|
|
144
|
-
* @example
|
|
145
|
-
* // Input: One-to-many relationship (hasMany)
|
|
146
|
-
* // authors have many articles
|
|
147
|
-
* const joinInfo = await buildJoinChain('authors', 'articles.title');
|
|
148
|
-
*
|
|
149
|
-
* // Output: JOIN with isOneToMany flag
|
|
150
|
-
* // {
|
|
151
|
-
* // joinAlias: 'authors_to_articles_articles',
|
|
152
|
-
* // targetTableName: 'articles',
|
|
153
|
-
* // sourceField: 'author_id',
|
|
154
|
-
* // targetField: 'title',
|
|
155
|
-
* // joinCondition: 'authors.id = authors_to_articles_articles.author_id',
|
|
156
|
-
* // isOneToMany: true,
|
|
157
|
-
* // isPolymorphic: false
|
|
158
|
-
* // }
|
|
159
|
-
*
|
|
160
|
-
* @description
|
|
161
|
-
* Used by:
|
|
162
|
-
* - rest-api-knex-plugin's dataQuery method when processing cross-table filters
|
|
163
|
-
* - Called for each filter that references a related table field
|
|
164
|
-
*
|
|
165
|
-
* Purpose:
|
|
166
|
-
* - Automatically constructs complex SQL JOINs from simple dot notation
|
|
167
|
-
* - Supports many-to-one (belongsTo), one-to-many (hasMany), and many-to-many relationships
|
|
168
|
-
* - Handles polymorphic relationships and multi-level paths
|
|
169
|
-
*
|
|
170
|
-
* Data flow:
|
|
171
|
-
* 1. Filter parser identifies cross-table reference (contains dots)
|
|
172
|
-
* 2. buildJoinChain analyzes relationship path segment by segment
|
|
173
|
-
* 3. For each segment, finds the appropriate relationship definition
|
|
174
|
-
* 4. Constructs JOIN conditions based on foreign keys
|
|
175
|
-
* 5. Returns complete JOIN information for SQL query builder
|
|
176
|
-
* 6. Query builder adds these JOINs to enable filtering on related data
|
|
177
65
|
*/
|
|
178
66
|
export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, searchedScopes = new Set()) => {
|
|
179
67
|
log.trace('[BUILD-JOIN] Starting buildJoinChain:', { fromScopeName, targetPath })
|
|
@@ -209,9 +97,15 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
209
97
|
let relationshipType = null
|
|
210
98
|
let relationshipField = null
|
|
211
99
|
|
|
100
|
+
const hasEstablishedPath = Object.values(currentRelationships || {}).some(rel => rel.type === 'hasMany' && rel.target === targetScope) ||
|
|
101
|
+
Object.values(currentSchema.structure).some(field => field.belongsTo === targetScope)
|
|
102
|
+
|
|
212
103
|
if (currentRelationships) {
|
|
213
104
|
for (const [relName, relDef] of Object.entries(currentRelationships)) {
|
|
214
|
-
|
|
105
|
+
const matchesHasMany = relDef.type === 'hasMany' && relDef.target === targetScope
|
|
106
|
+
const matchesManyToMany = relDef.type === 'manyToMany' && (relDef.target || relName) === targetScope &&
|
|
107
|
+
!hasEstablishedPath
|
|
108
|
+
if (matchesHasMany || matchesManyToMany) {
|
|
215
109
|
if (relDef.via) {
|
|
216
110
|
const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships
|
|
217
111
|
const viaRel = targetRelationships?.[relDef.via]
|
|
@@ -232,6 +126,7 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
232
126
|
foundRelationship = targetScope
|
|
233
127
|
relationshipType = 'manyToMany'
|
|
234
128
|
relationshipField = {
|
|
129
|
+
relationshipName: relName,
|
|
235
130
|
through: relDef.through,
|
|
236
131
|
foreignKey: relDef.foreignKey,
|
|
237
132
|
otherKey: relDef.otherKey
|
|
@@ -250,8 +145,13 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
250
145
|
relationshipType = 'hasMany'
|
|
251
146
|
relationshipField = relDef.foreignKey
|
|
252
147
|
if (!relationshipField) {
|
|
253
|
-
|
|
254
|
-
|
|
148
|
+
const error = new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${currentScope}'`)
|
|
149
|
+
try {
|
|
150
|
+
await log.error('[BUILD-JOIN] Missing foreignKey in hasMany relationship:', {
|
|
151
|
+
...getOperationDiagnosticContext({}, { phase: 'crossTableJoin', method: 'buildJoinChain', scopeName: currentScope }), relName, error
|
|
152
|
+
})
|
|
153
|
+
} catch { /* The configuration error remains primary. */ }
|
|
154
|
+
throw error
|
|
255
155
|
}
|
|
256
156
|
log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField })
|
|
257
157
|
break
|
|
@@ -281,9 +181,9 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
281
181
|
const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
282
182
|
|
|
283
183
|
if (relationshipType === 'manyToMany') {
|
|
284
|
-
const { through, foreignKey, otherKey } = relationshipField
|
|
184
|
+
const { relationshipName, through, foreignKey, otherKey } = relationshipField
|
|
285
185
|
const pivotTableName = scopes[through].vars.schemaInfo.tableName || through
|
|
286
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain
|
|
186
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain.at(-1).joinAlias
|
|
287
187
|
|
|
288
188
|
const pivotAlias = `${currentScope}_to_${through}_${through}`
|
|
289
189
|
const pivotJoinCondition = `${previousAlias}.id = ${pivotAlias}.${foreignKey}`
|
|
@@ -296,6 +196,10 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
296
196
|
isPolymorphic: false,
|
|
297
197
|
relationshipField: foreignKey,
|
|
298
198
|
relationshipType: 'manyToMany_pivot',
|
|
199
|
+
sourceScopeName: currentScope,
|
|
200
|
+
relationshipName,
|
|
201
|
+
foreignKey,
|
|
202
|
+
otherKey,
|
|
299
203
|
targetScopeName: through
|
|
300
204
|
})
|
|
301
205
|
|
|
@@ -318,13 +222,13 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
318
222
|
let joinCondition
|
|
319
223
|
if (relationshipType === 'hasManyPolymorphic') {
|
|
320
224
|
const { typeField, idField } = relationshipField
|
|
321
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain
|
|
225
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain.at(-1).joinAlias
|
|
322
226
|
joinCondition = `${joinAlias}.${typeField} = '${currentScope}' AND ${joinAlias}.${idField} = ${previousAlias}.id`
|
|
323
227
|
} else if (relationshipType === 'hasMany') {
|
|
324
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain
|
|
228
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain.at(-1).joinAlias
|
|
325
229
|
joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`
|
|
326
230
|
} else {
|
|
327
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain
|
|
231
|
+
const previousAlias = i === 0 ? sourceTableName : joinChain.at(-1).joinAlias
|
|
328
232
|
joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`
|
|
329
233
|
}
|
|
330
234
|
|
|
@@ -376,183 +280,3 @@ export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, sea
|
|
|
376
280
|
}
|
|
377
281
|
}
|
|
378
282
|
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Analyzes a search schema to identify which fields need database indexes
|
|
382
|
-
*
|
|
383
|
-
* @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
|
|
384
|
-
* @param {Object} log - Logger instance for debug and trace output
|
|
385
|
-
* @param {string} scopeName - The scope being analyzed
|
|
386
|
-
* @param {Object} searchSchema - Search schema definition with filter fields
|
|
387
|
-
* @returns {Array<Object>} Array of required indexes with scope, field, and reason
|
|
388
|
-
*
|
|
389
|
-
* @example
|
|
390
|
-
* // Input: Search schema with cross-table references
|
|
391
|
-
* const searchSchema = {
|
|
392
|
-
* authorName: {
|
|
393
|
-
* type: 'string',
|
|
394
|
-
* actualField: 'authors.name',
|
|
395
|
-
* filterOperator: 'like'
|
|
396
|
-
* },
|
|
397
|
-
* companyName: {
|
|
398
|
-
* type: 'string',
|
|
399
|
-
* actualField: 'authors.company.name'
|
|
400
|
-
* },
|
|
401
|
-
* search: {
|
|
402
|
-
* type: 'string',
|
|
403
|
-
* oneOf: ['title', 'authors.name', 'authors.company.name']
|
|
404
|
-
* }
|
|
405
|
-
* };
|
|
406
|
-
*
|
|
407
|
-
* const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
|
|
408
|
-
*
|
|
409
|
-
* // Output: List of fields that need indexes
|
|
410
|
-
* // [
|
|
411
|
-
* // { scope: 'authors', field: 'name', reason: 'Cross-table search from articles.authorName' },
|
|
412
|
-
* // { scope: 'authors', field: 'company', reason: 'Cross-table search from articles.companyName' },
|
|
413
|
-
* // { scope: 'authors', field: 'name', reason: 'Cross-table oneOf search from articles.search' },
|
|
414
|
-
* // { scope: 'authors', field: 'company', reason: 'Cross-table oneOf search from articles.search' }
|
|
415
|
-
* // ]
|
|
416
|
-
*
|
|
417
|
-
* @example
|
|
418
|
-
* // Input: No cross-table references
|
|
419
|
-
* const searchSchema = {
|
|
420
|
-
* title: { type: 'string' },
|
|
421
|
-
* status: { type: 'string' }
|
|
422
|
-
* };
|
|
423
|
-
*
|
|
424
|
-
* const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
|
|
425
|
-
*
|
|
426
|
-
* // Output: Empty array - no cross-table indexes needed
|
|
427
|
-
* // []
|
|
428
|
-
*
|
|
429
|
-
* @description
|
|
430
|
-
* Used by:
|
|
431
|
-
* - Called during API initialization to identify missing indexes
|
|
432
|
-
* - Used by developers to understand performance requirements
|
|
433
|
-
*
|
|
434
|
-
* Purpose:
|
|
435
|
-
* - Cross-table searches require indexes for acceptable performance
|
|
436
|
-
* - Helps identify configuration issues before they cause slow queries
|
|
437
|
-
* - Provides clear documentation of index requirements
|
|
438
|
-
*
|
|
439
|
-
* Data flow:
|
|
440
|
-
* - Runs during schema compilation phase
|
|
441
|
-
* - Analyzes search schemas to find cross-table references
|
|
442
|
-
* - Output can be used to create indexes manually or automatically
|
|
443
|
-
* - Prevents performance issues before queries are executed
|
|
444
|
-
*/
|
|
445
|
-
export const analyzeRequiredIndexes = (scopes, log, scopeName, schemaInfo) => {
|
|
446
|
-
const requiredIndexes = []
|
|
447
|
-
|
|
448
|
-
const schemaToAnalyze = schemaInfo.searchSchemaStructure
|
|
449
|
-
|
|
450
|
-
Object.entries(schemaToAnalyze).forEach(([filterKey, fieldDef]) => {
|
|
451
|
-
if (fieldDef.actualField && fieldDef.actualField.includes('.')) {
|
|
452
|
-
const [targetScopeName, targetFieldName] = fieldDef.actualField.split('.')
|
|
453
|
-
requiredIndexes.push({
|
|
454
|
-
scope: targetScopeName,
|
|
455
|
-
field: targetFieldName,
|
|
456
|
-
reason: `Cross-table search from ${scopeName}.${filterKey}`
|
|
457
|
-
})
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
|
|
461
|
-
fieldDef.oneOf.forEach(field => {
|
|
462
|
-
if (field.includes('.')) {
|
|
463
|
-
const [targetScopeName, targetFieldName] = field.split('.')
|
|
464
|
-
requiredIndexes.push({
|
|
465
|
-
scope: targetScopeName,
|
|
466
|
-
field: targetFieldName,
|
|
467
|
-
reason: `Cross-table oneOf search from ${scopeName}.${filterKey}`
|
|
468
|
-
})
|
|
469
|
-
}
|
|
470
|
-
})
|
|
471
|
-
}
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
return requiredIndexes
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Creates database indexes for fields identified by analyzeRequiredIndexes
|
|
479
|
-
*
|
|
480
|
-
* @async
|
|
481
|
-
* @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
|
|
482
|
-
* @param {Object} log - Logger instance for debug and trace output
|
|
483
|
-
* @param {Array<Object>} requiredIndexes - Index requirements from analyzeRequiredIndexes
|
|
484
|
-
* @param {Object} knex - Knex database connection instance
|
|
485
|
-
* @returns {Promise<Array<Object>>} Array of successfully created indexes
|
|
486
|
-
*
|
|
487
|
-
* @example
|
|
488
|
-
* // Input: Required indexes from analysis
|
|
489
|
-
* const requiredIndexes = [
|
|
490
|
-
* { scope: 'authors', field: 'name', reason: 'Cross-table search' },
|
|
491
|
-
* { scope: 'companies', field: 'name', reason: 'Cross-table search' }
|
|
492
|
-
* ];
|
|
493
|
-
*
|
|
494
|
-
* const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
|
|
495
|
-
*
|
|
496
|
-
* // Output: Successfully created indexes
|
|
497
|
-
* // [
|
|
498
|
-
* // { tableName: 'authors', field: 'name', indexName: 'idx_authors_name_search' },
|
|
499
|
-
* // { tableName: 'companies', field: 'name', indexName: 'idx_companies_name_search' }
|
|
500
|
-
* // ]
|
|
501
|
-
*
|
|
502
|
-
* // Database effect: CREATE INDEX idx_authors_name_search ON authors(name);
|
|
503
|
-
* // Database effect: CREATE INDEX idx_companies_name_search ON companies(name);
|
|
504
|
-
*
|
|
505
|
-
* @example
|
|
506
|
-
* // Input: Some indexes already exist
|
|
507
|
-
* const requiredIndexes = [
|
|
508
|
-
* { scope: 'authors', field: 'name', reason: 'Cross-table search' }, // Already exists
|
|
509
|
-
* { scope: 'authors', field: 'email', reason: 'Cross-table search' } // New
|
|
510
|
-
* ];
|
|
511
|
-
*
|
|
512
|
-
* const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
|
|
513
|
-
*
|
|
514
|
-
* // Output: Only newly created indexes
|
|
515
|
-
* // [
|
|
516
|
-
* // { tableName: 'authors', field: 'email', indexName: 'idx_authors_email_search' }
|
|
517
|
-
* // ]
|
|
518
|
-
*
|
|
519
|
-
* @description
|
|
520
|
-
* Used by:
|
|
521
|
-
* - Can be called during database migration or setup
|
|
522
|
-
* - Used by developers to automatically create performance indexes
|
|
523
|
-
*
|
|
524
|
-
* Purpose:
|
|
525
|
-
* - Automates index creation based on search schema requirements
|
|
526
|
-
* - Ensures consistent index naming across the application
|
|
527
|
-
* - Safely handles cases where indexes already exist
|
|
528
|
-
*
|
|
529
|
-
* Data flow:
|
|
530
|
-
* - Typically runs during database setup or migration
|
|
531
|
-
* - Creates indexes identified by analyzeRequiredIndexes
|
|
532
|
-
* - Improves query performance for cross-table searches
|
|
533
|
-
* - Part of the database optimization phase
|
|
534
|
-
*/
|
|
535
|
-
export const createRequiredIndexes = async (scopes, log, requiredIndexes, knex) => {
|
|
536
|
-
const createdIndexes = []
|
|
537
|
-
|
|
538
|
-
for (const indexInfo of requiredIndexes) {
|
|
539
|
-
const { scope, field } = indexInfo
|
|
540
|
-
const tableName = scopes[scope].vars.schemaInfo.tableName
|
|
541
|
-
const indexName = `idx_${tableName}_${field}_search`
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
const hasIndex = await knex.schema.hasIndex(tableName, [field])
|
|
545
|
-
if (!hasIndex) {
|
|
546
|
-
await knex.schema.table(tableName, table => {
|
|
547
|
-
table.index([field], indexName)
|
|
548
|
-
})
|
|
549
|
-
createdIndexes.push({ tableName, field, indexName })
|
|
550
|
-
log.info(`Created index: ${indexName} on ${tableName}.${field}`)
|
|
551
|
-
}
|
|
552
|
-
} catch (error) {
|
|
553
|
-
log.warn(`Failed to create index on ${tableName}.${field}:`, error.message)
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
return createdIndexes
|
|
558
|
-
}
|