json-rest-api 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module cross-table-search
|
|
3
|
-
* @description SQL JOIN-based cross-table search utilities for REST API Knex Plugin
|
|
4
|
-
*
|
|
5
|
-
* This module provides utilities for building complex SQL queries that search across
|
|
6
|
-
* related database tables using JOINs. It supports:
|
|
7
|
-
* - Many-to-one relationships (belongsTo with sideSearch: true)
|
|
8
|
-
* - One-to-many relationships (hasMany with sideSearch: true)
|
|
9
|
-
* - Multi-level relationship traversal (e.g., articles → people → companies)
|
|
10
|
-
* - Automatic JOIN aliasing to prevent conflicts
|
|
11
|
-
* - Field validation and index verification
|
|
12
|
-
*
|
|
13
|
-
* @example <caption>Basic cross-table search setup</caption>
|
|
14
|
-
* // In your schema definitions:
|
|
15
|
-
* const peopleSchema = {
|
|
16
|
-
* id: { type: 'id' },
|
|
17
|
-
* name: { type: 'string', indexed: true },
|
|
18
|
-
* company_id: {
|
|
19
|
-
* belongsTo: 'companies',
|
|
20
|
-
* as: 'company',
|
|
21
|
-
* sideSearch: true // Enable cross-table search
|
|
22
|
-
* }
|
|
23
|
-
* };
|
|
24
|
-
*
|
|
25
|
-
* const peopleSearchSchema = {
|
|
26
|
-
* // Search people by their company name
|
|
27
|
-
* companyName: {
|
|
28
|
-
* type: 'string',
|
|
29
|
-
* actualField: 'companies.name', // Cross-table reference
|
|
30
|
-
* filterUsing: 'like'
|
|
31
|
-
* }
|
|
32
|
-
* };
|
|
33
|
-
*
|
|
34
|
-
* @example <caption>Multi-level cross-table search</caption>
|
|
35
|
-
* const articlesSearchSchema = {
|
|
36
|
-
* // Search articles by the company of their author (3 levels)
|
|
37
|
-
* companyName: {
|
|
38
|
-
* type: 'string',
|
|
39
|
-
* actualField: 'companies.name', // articles → people → companies
|
|
40
|
-
* filterUsing: 'like'
|
|
41
|
-
* }
|
|
42
|
-
* };
|
|
43
|
-
*
|
|
44
|
-
* @example <caption>One-to-many cross-table search</caption>
|
|
45
|
-
* const peopleSearchSchema = {
|
|
46
|
-
* // Search people by their article titles (one person has many articles)
|
|
47
|
-
* articleTitle: {
|
|
48
|
-
* type: 'string',
|
|
49
|
-
* actualField: 'articles.title',
|
|
50
|
-
* filterUsing: 'like'
|
|
51
|
-
* }
|
|
52
|
-
* };
|
|
53
|
-
*
|
|
54
|
-
* @example <caption>Why this is useful upstream</caption>
|
|
55
|
-
* // The REST API Knex plugin uses this module to:
|
|
56
|
-
* // 1. Enable powerful cross-table filtering without exposing SQL complexity
|
|
57
|
-
* // 2. Automatically build optimal JOIN chains based on relationship definitions
|
|
58
|
-
* // 3. Prevent N+1 queries by using efficient SQL JOINs
|
|
59
|
-
* // 4. Support deep relationship traversal (e.g., article.author.company.country)
|
|
60
|
-
* // 5. Validate that cross-table fields are properly indexed for performance
|
|
61
|
-
* // 6. Generate unique aliases to prevent JOIN conflicts
|
|
62
|
-
* // 7. Handle both many-to-one and one-to-many relationships seamlessly
|
|
63
|
-
* // 8. Provide clear error messages when relationships are misconfigured
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Creates cross-table search helper functions with injected dependencies
|
|
68
|
-
*
|
|
69
|
-
* @param {Object} scopes - The hooked-api scopes object containing all registered scopes
|
|
70
|
-
* @param {Object} log - The logging instance for debug/trace output
|
|
71
|
-
* @returns {Object} Object containing all cross-table search helper functions
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* import { createCrossTableSearchHelpers } from './lib/cross-table-search.js';
|
|
75
|
-
*
|
|
76
|
-
* const crossTableSearchHelpers = createCrossTableSearchHelpers(scopes, log);
|
|
77
|
-
*
|
|
78
|
-
* // Validate a cross-table field reference
|
|
79
|
-
* await crossTableSearchHelpers.validateCrossTableField('people', 'name');
|
|
80
|
-
*
|
|
81
|
-
* // Build JOIN chain for complex path
|
|
82
|
-
* const joinInfo = await crossTableSearchHelpers.buildJoinChain('articles', 'companies.name');
|
|
83
|
-
*/
|
|
84
|
-
export const createCrossTableSearchHelpers = (scopes, log) => {
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Validates that a field is properly configured for cross-table search
|
|
88
|
-
*
|
|
89
|
-
* @async
|
|
90
|
-
* @param {string} targetScopeName - The scope containing the field
|
|
91
|
-
* @param {string} fieldName - The field to validate
|
|
92
|
-
* @param {Set<string>} [searchedScopes=new Set()] - Used internally to prevent circular references
|
|
93
|
-
* @returns {Promise<{targetSchema: Object, fieldDef: Object}>} The validated schema and field definition
|
|
94
|
-
* @throws {Error} If scope not found, field not found, field not indexed, or circular reference detected
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* // Validate that 'people.name' is searchable
|
|
98
|
-
* try {
|
|
99
|
-
* const { targetSchema, fieldDef } = await validateCrossTableField('people', 'name');
|
|
100
|
-
* console.log('Field is indexed:', fieldDef.indexed);
|
|
101
|
-
* } catch (error) {
|
|
102
|
-
* console.error('Field validation failed:', error.message);
|
|
103
|
-
* }
|
|
104
|
-
*/
|
|
105
|
-
const validateCrossTableField = async (targetScopeName, fieldName, searchedScopes = new Set()) => {
|
|
106
|
-
log.trace('[VALIDATE] Starting validateCrossTableField:', { targetScopeName, fieldName, searchedScopes: Array.from(searchedScopes) });
|
|
107
|
-
|
|
108
|
-
// Prevent circular references
|
|
109
|
-
if (searchedScopes.has(targetScopeName)) {
|
|
110
|
-
throw new Error(`Circular reference detected: ${targetScopeName} -> ${Array.from(searchedScopes).join(' -> ')}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Check if target scope exists by trying to call getSchema
|
|
114
|
-
log.trace('[VALIDATE] Getting target schema for:', targetScopeName);
|
|
115
|
-
let targetSchema;
|
|
116
|
-
try {
|
|
117
|
-
targetSchema = scopes[targetScopeName].vars.schemaInfo.schema;;
|
|
118
|
-
log.trace('[VALIDATE] Got target schema:', { scopeName: targetScopeName, schemaKeys: Object.keys(targetSchema || {}) });
|
|
119
|
-
} catch (error) {
|
|
120
|
-
log.trace('[VALIDATE] Error getting target schema:', error.message);
|
|
121
|
-
throw new Error(`Target scope '${targetScopeName}' not found`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!targetSchema) {
|
|
125
|
-
log.trace('[VALIDATE] Target schema is null/undefined');
|
|
126
|
-
throw new Error(`Target scope '${targetScopeName}' has no schema`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const fieldDef = targetSchema.structure[fieldName];
|
|
130
|
-
log.trace('[VALIDATE] Field lookup result:', { fieldName, fieldExists: !!fieldDef });
|
|
131
|
-
if (!fieldDef) {
|
|
132
|
-
throw new Error(`Field '${fieldName}' not found in scope '${targetScopeName}'`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Check if field is marked as indexed (required for cross-table search)
|
|
136
|
-
log.trace('[VALIDATE] Checking indexed status:', { fieldName, indexed: fieldDef.indexed });
|
|
137
|
-
if (!fieldDef.indexed) {
|
|
138
|
-
throw new Error(`Field '${targetScopeName}.${fieldName}' is not indexed. Add 'indexed: true' to allow cross-table search`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const result = { targetSchema, fieldDef };
|
|
142
|
-
log.trace('[VALIDATE] Validation successful for field:', `${targetScopeName}.${fieldName}`);
|
|
143
|
-
return result;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Builds a JOIN chain for cross-table search using explicit paths only
|
|
148
|
-
*
|
|
149
|
-
* @async
|
|
150
|
-
* @param {string} fromScopeName - The starting scope (e.g., 'articles')
|
|
151
|
-
* @param {string} targetPath - The target field path (e.g., 'comments.body' or 'comments.tags.name')
|
|
152
|
-
* @param {Set<string>} [searchedScopes=new Set()] - Used internally to prevent circular references
|
|
153
|
-
* @returns {Promise<Object>} JOIN information including alias, condition, and chain details
|
|
154
|
-
* @throws {Error} If path is invalid or relationship not found
|
|
155
|
-
*
|
|
156
|
-
* @property {string} returns.joinAlias - The alias to use for the joined table
|
|
157
|
-
* @property {string} returns.targetTableName - The actual database table name
|
|
158
|
-
* @property {string} returns.joinCondition - The SQL JOIN condition
|
|
159
|
-
* @property {string} returns.targetField - The field name in the target table
|
|
160
|
-
* @property {boolean} returns.isOneToMany - Whether this is a one-to-many relationship
|
|
161
|
-
* @property {boolean} returns.isMultiLevel - Whether this requires multiple JOINs
|
|
162
|
-
* @property {Array} [returns.joinChain] - Array of JOIN steps for multi-level paths
|
|
163
|
-
*
|
|
164
|
-
* @example <caption>Simple cross-table search</caption>
|
|
165
|
-
* // Build JOIN for searching articles by comment body
|
|
166
|
-
* const joinInfo = await buildJoinChain('articles', 'comments.body');
|
|
167
|
-
* // Returns: {
|
|
168
|
-
* // joinAlias: 'articles_to_comments_comments',
|
|
169
|
-
* // targetTableName: 'comments',
|
|
170
|
-
* // joinCondition: 'articles.id = articles_to_comments_comments.article_id',
|
|
171
|
-
* // targetField: 'body',
|
|
172
|
-
* // isOneToMany: true
|
|
173
|
-
* // }
|
|
174
|
-
*
|
|
175
|
-
* @example <caption>Multi-level explicit path</caption>
|
|
176
|
-
* // Build JOIN chain for searching articles by comment tags
|
|
177
|
-
* const joinInfo = await buildJoinChain('articles', 'comments.tags.name');
|
|
178
|
-
* // Returns: {
|
|
179
|
-
* // joinAlias: 'comments_to_tags_tags',
|
|
180
|
-
* // targetTableName: 'tags',
|
|
181
|
-
* // targetField: 'name',
|
|
182
|
-
* // isOneToMany: true,
|
|
183
|
-
* // isMultiLevel: true,
|
|
184
|
-
* // joinChain: [
|
|
185
|
-
* // { targetTableName: 'comments', joinAlias: 'articles_to_comments_comments', ... },
|
|
186
|
-
* // { targetTableName: 'tags', joinAlias: 'comments_to_tags_tags', ... }
|
|
187
|
-
* // ]
|
|
188
|
-
* // }
|
|
189
|
-
*/
|
|
190
|
-
const buildJoinChain = async (fromScopeName, targetPath, searchedScopes = new Set()) => {
|
|
191
|
-
log.trace('[BUILD-JOIN] Starting buildJoinChain:', { fromScopeName, targetPath });
|
|
192
|
-
|
|
193
|
-
// Parse the path into segments
|
|
194
|
-
const pathSegments = targetPath.split('.');
|
|
195
|
-
if (pathSegments.length < 2) {
|
|
196
|
-
throw new Error(`Invalid cross-table path '${targetPath}'. Must contain at least 'scope.field'`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// The last segment is always the field name
|
|
200
|
-
const targetFieldName = pathSegments[pathSegments.length - 1];
|
|
201
|
-
const relationshipPath = pathSegments.slice(0, -1);
|
|
202
|
-
|
|
203
|
-
log.trace('[BUILD-JOIN] Parsed path:', { relationshipPath, targetFieldName });
|
|
204
|
-
|
|
205
|
-
// Build the join chain by following each relationship in the path
|
|
206
|
-
const joinChain = [];
|
|
207
|
-
let currentScope = fromScopeName;
|
|
208
|
-
|
|
209
|
-
for (let i = 0; i < relationshipPath.length; i++) {
|
|
210
|
-
const targetScope = relationshipPath[i];
|
|
211
|
-
|
|
212
|
-
log.trace('[BUILD-JOIN] Looking for direct relationship:', { from: currentScope, to: targetScope });
|
|
213
|
-
|
|
214
|
-
// Prevent circular references
|
|
215
|
-
if (searchedScopes.has(currentScope)) {
|
|
216
|
-
throw new Error(`Circular reference detected in path: ${Array.from(searchedScopes).join(' -> ')} -> ${currentScope}`);
|
|
217
|
-
}
|
|
218
|
-
searchedScopes.add(currentScope);
|
|
219
|
-
|
|
220
|
-
// Get current scope's schema and relationships
|
|
221
|
-
const schemaInfo = scopes[currentScope].vars.schemaInfo
|
|
222
|
-
const currentSchema = schemaInfo.schema;
|
|
223
|
-
const currentRelationships = schemaInfo.schemaRelationships;
|
|
224
|
-
|
|
225
|
-
let foundRelationship = null;
|
|
226
|
-
let relationshipType = null;
|
|
227
|
-
let relationshipField = null;
|
|
228
|
-
let relationshipDef = null;
|
|
229
|
-
|
|
230
|
-
// First check hasMany relationships (one-to-many)
|
|
231
|
-
if (currentRelationships) {
|
|
232
|
-
for (const [relName, relDef] of Object.entries(currentRelationships)) {
|
|
233
|
-
if (relDef.hasMany === targetScope && (relDef.sideSearchMany === true)) {
|
|
234
|
-
foundRelationship = targetScope;
|
|
235
|
-
relationshipType = 'hasMany';
|
|
236
|
-
relationshipField = relDef.foreignKey || `${currentScope.slice(0, -1)}_id`;
|
|
237
|
-
relationshipDef = relDef;
|
|
238
|
-
log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField });
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// If not found, check belongsTo relationships (many-to-one)
|
|
245
|
-
if (!foundRelationship) {
|
|
246
|
-
for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
|
|
247
|
-
if (fieldDef.belongsTo === targetScope && (fieldDef.sideSearchSingle !== false)) {
|
|
248
|
-
foundRelationship = targetScope;
|
|
249
|
-
relationshipType = 'belongsTo';
|
|
250
|
-
relationshipField = fieldName;
|
|
251
|
-
relationshipDef = fieldDef;
|
|
252
|
-
log.trace('[BUILD-JOIN] Found belongsTo relationship:', { fieldName, targetScope });
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (!foundRelationship) {
|
|
259
|
-
throw new Error(
|
|
260
|
-
`No searchable relationship from '${currentScope}' to '${targetScope}'. ` +
|
|
261
|
-
`Ensure a direct relationship exists with 'sideSearchSingle: true' or 'sideSearchMany: true'`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Get table names
|
|
266
|
-
const sourceSchema = scopes[currentScope].vars.schemaInfo.schema;
|
|
267
|
-
const sourceTableName = scopes[currentScope].vars.schemaInfo.tableName
|
|
268
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.schema;
|
|
269
|
-
const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
270
|
-
|
|
271
|
-
// Generate unique alias
|
|
272
|
-
const joinAlias = `${currentScope}_to_${targetScope}_${targetScope}`;
|
|
273
|
-
|
|
274
|
-
// Build join condition
|
|
275
|
-
let joinCondition;
|
|
276
|
-
if (relationshipType === 'hasMany') {
|
|
277
|
-
// One-to-many: source.id = target.foreign_key
|
|
278
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
|
|
279
|
-
joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`;
|
|
280
|
-
} else {
|
|
281
|
-
// Many-to-one: source.foreign_key = target.id
|
|
282
|
-
const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
|
|
283
|
-
joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
joinChain.push({
|
|
287
|
-
targetTableName,
|
|
288
|
-
joinAlias,
|
|
289
|
-
joinCondition,
|
|
290
|
-
isOneToMany: relationshipType === 'hasMany',
|
|
291
|
-
relationshipField,
|
|
292
|
-
relationshipType
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
currentScope = targetScope;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Validate the final field exists and is indexed
|
|
299
|
-
const finalScope = relationshipPath[relationshipPath.length - 1];
|
|
300
|
-
await validateCrossTableField(finalScope, targetFieldName, searchedScopes);
|
|
301
|
-
|
|
302
|
-
// Return the complete join information
|
|
303
|
-
if (joinChain.length === 0) {
|
|
304
|
-
throw new Error(`Invalid path '${targetPath}': no relationships to traverse`);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const lastJoin = joinChain[joinChain.length - 1];
|
|
308
|
-
|
|
309
|
-
if (joinChain.length > 1) {
|
|
310
|
-
// Multi-level path
|
|
311
|
-
return {
|
|
312
|
-
targetTableName: lastJoin.targetTableName,
|
|
313
|
-
joinAlias: lastJoin.joinAlias,
|
|
314
|
-
joinCondition: joinChain.map(j => j.joinCondition).join(' AND '),
|
|
315
|
-
targetField: targetFieldName,
|
|
316
|
-
isOneToMany: joinChain.some(j => j.isOneToMany),
|
|
317
|
-
isMultiLevel: true,
|
|
318
|
-
joinChain
|
|
319
|
-
};
|
|
320
|
-
} else {
|
|
321
|
-
// Single-level path
|
|
322
|
-
return {
|
|
323
|
-
joinAlias: lastJoin.joinAlias,
|
|
324
|
-
targetTableName: lastJoin.targetTableName,
|
|
325
|
-
sourceField: lastJoin.relationshipField,
|
|
326
|
-
targetField: targetFieldName,
|
|
327
|
-
joinCondition: lastJoin.joinCondition,
|
|
328
|
-
isOneToMany: lastJoin.isOneToMany
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Analyzes searchSchema to identify fields that need database indexes
|
|
335
|
-
*
|
|
336
|
-
* @param {string} scopeName - The scope to analyze
|
|
337
|
-
* @param {Object} searchSchema - The search schema definition
|
|
338
|
-
* @returns {Array<Object>} Array of required indexes with scope, field, and reason
|
|
339
|
-
*
|
|
340
|
-
* @example
|
|
341
|
-
* const searchSchema = {
|
|
342
|
-
* companyName: {
|
|
343
|
-
* actualField: 'companies.name',
|
|
344
|
-
* filterUsing: 'like'
|
|
345
|
-
* },
|
|
346
|
-
* search: {
|
|
347
|
-
* likeOneOf: ['name', 'email', 'companies.name']
|
|
348
|
-
* }
|
|
349
|
-
* };
|
|
350
|
-
*
|
|
351
|
-
* const requiredIndexes = analyzeRequiredIndexes('people', searchSchema);
|
|
352
|
-
* // Returns: [
|
|
353
|
-
* // { scope: 'companies', field: 'name', reason: 'Cross-table search from people.companyName' },
|
|
354
|
-
* // { scope: 'companies', field: 'name', reason: 'Cross-table likeOneOf search from people.search' }
|
|
355
|
-
* // ]
|
|
356
|
-
*/
|
|
357
|
-
const analyzeRequiredIndexes = (scopeName, searchSchema) => {
|
|
358
|
-
const requiredIndexes = [];
|
|
359
|
-
|
|
360
|
-
// Handle both raw schema and schema object
|
|
361
|
-
const schemaToAnalyze = searchSchema.structure || searchSchema;
|
|
362
|
-
|
|
363
|
-
Object.entries(schemaToAnalyze).forEach(([filterKey, fieldDef]) => {
|
|
364
|
-
// Check actualField for cross-table references
|
|
365
|
-
if (fieldDef.actualField && fieldDef.actualField.includes('.')) {
|
|
366
|
-
const [targetScopeName, targetFieldName] = fieldDef.actualField.split('.');
|
|
367
|
-
requiredIndexes.push({
|
|
368
|
-
scope: targetScopeName,
|
|
369
|
-
field: targetFieldName,
|
|
370
|
-
reason: `Cross-table search from ${scopeName}.${filterKey}`
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Check for likeOneOf cross-table references
|
|
375
|
-
if (fieldDef.likeOneOf && Array.isArray(fieldDef.likeOneOf)) {
|
|
376
|
-
fieldDef.likeOneOf.forEach(field => {
|
|
377
|
-
if (field.includes('.')) {
|
|
378
|
-
const [targetScopeName, targetFieldName] = field.split('.');
|
|
379
|
-
requiredIndexes.push({
|
|
380
|
-
scope: targetScopeName,
|
|
381
|
-
field: targetFieldName,
|
|
382
|
-
reason: `Cross-table likeOneOf search from ${scopeName}.${filterKey}`
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
return requiredIndexes;
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Auto-generates database indexes for cross-table search fields
|
|
394
|
-
*
|
|
395
|
-
* @async
|
|
396
|
-
* @param {Array<Object>} requiredIndexes - Array of index requirements from analyzeRequiredIndexes
|
|
397
|
-
* @param {Object} knex - The Knex instance for database operations
|
|
398
|
-
* @returns {Promise<Array<Object>>} Array of successfully created indexes
|
|
399
|
-
*
|
|
400
|
-
* @example
|
|
401
|
-
* const requiredIndexes = analyzeRequiredIndexes('people', searchSchema);
|
|
402
|
-
* const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
|
|
403
|
-
*
|
|
404
|
-
* createdIndexes.forEach(idx => {
|
|
405
|
-
* console.log(`Created index ${idx.indexName} on ${idx.tableName}.${idx.field}`);
|
|
406
|
-
* });
|
|
407
|
-
*/
|
|
408
|
-
const createRequiredIndexes = async (requiredIndexes, knex) => {
|
|
409
|
-
const createdIndexes = [];
|
|
410
|
-
|
|
411
|
-
for (const indexInfo of requiredIndexes) {
|
|
412
|
-
const { scope, field } = indexInfo;
|
|
413
|
-
const schema = scopes[scope].vars.schemaInfo.schema;
|
|
414
|
-
const tableName = scopes[scope].vars.schemaInfo.tableName
|
|
415
|
-
const indexName = `idx_${tableName}_${field}_search`;
|
|
416
|
-
|
|
417
|
-
try {
|
|
418
|
-
// Check if index already exists
|
|
419
|
-
const hasIndex = await knex.schema.hasIndex(tableName, [field]);
|
|
420
|
-
if (!hasIndex) {
|
|
421
|
-
await knex.schema.table(tableName, table => {
|
|
422
|
-
table.index([field], indexName);
|
|
423
|
-
});
|
|
424
|
-
createdIndexes.push({ tableName, field, indexName });
|
|
425
|
-
log.info(`Created index: ${indexName} on ${tableName}.${field}`);
|
|
426
|
-
}
|
|
427
|
-
} catch (error) {
|
|
428
|
-
log.warn(`Failed to create index on ${tableName}.${field}:`, error.message);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return createdIndexes;
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
// Return all helper functions
|
|
436
|
-
return {
|
|
437
|
-
validateCrossTableField,
|
|
438
|
-
buildJoinChain,
|
|
439
|
-
analyzeRequiredIndexes,
|
|
440
|
-
createRequiredIndexes
|
|
441
|
-
};
|
|
442
|
-
};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module knex-json-api-helpers
|
|
3
|
-
* @description JSON:API transformation helpers for database records
|
|
4
|
-
*
|
|
5
|
-
* This module provides functions to transform database records into
|
|
6
|
-
* JSON:API format, filtering out foreign keys and internal fields.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { getForeignKeyFields } from '../utils/field-utils.js';
|
|
10
|
-
import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY } from '../utils/knex-constants.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Converts a database record to JSON:API format
|
|
14
|
-
*
|
|
15
|
-
* This function transforms a flat database record into JSON:API structure,
|
|
16
|
-
* filtering out foreign keys and polymorphic fields from attributes.
|
|
17
|
-
* Foreign keys are represented as relationships, not attributes in JSON:API.
|
|
18
|
-
*
|
|
19
|
-
* @param {Object} scope - The scope object containing schema info
|
|
20
|
-
* @param {Object} record - The database record to transform
|
|
21
|
-
* @param {Object} deps - Dependencies object
|
|
22
|
-
* @param {Object} deps.context - Request context containing scopeName and polymorphicFields
|
|
23
|
-
* @returns {Object|null} JSON:API formatted resource object or null if no record
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* const scope = api.resources['articles'];
|
|
27
|
-
* const record = { id: 1, title: 'Hello', author_id: 2 };
|
|
28
|
-
* const deps = {
|
|
29
|
-
* context: {
|
|
30
|
-
* scopeName: 'articles',
|
|
31
|
-
* schemaInfo: { idProperty: 'id' },
|
|
32
|
-
* polymorphicFields: new Set()
|
|
33
|
-
* }
|
|
34
|
-
* };
|
|
35
|
-
* const jsonApiRecord = toJsonApi(scope, record, deps);
|
|
36
|
-
* // Returns: {
|
|
37
|
-
* // type: 'articles',
|
|
38
|
-
* // id: '1',
|
|
39
|
-
* // attributes: { title: 'Hello' }
|
|
40
|
-
* // // Note: author_id is filtered out as it's a foreign key
|
|
41
|
-
* // }
|
|
42
|
-
*/
|
|
43
|
-
export const toJsonApi = (scope, record, deps) => {
|
|
44
|
-
if (!record) return null;
|
|
45
|
-
|
|
46
|
-
// Extract values from scope
|
|
47
|
-
const {
|
|
48
|
-
vars: {
|
|
49
|
-
schemaInfo: { schema }
|
|
50
|
-
}
|
|
51
|
-
} = scope;
|
|
52
|
-
|
|
53
|
-
// Extract values from deps
|
|
54
|
-
const { context } = deps;
|
|
55
|
-
const scopeName = context.scopeName;
|
|
56
|
-
const idProperty = context.schemaInfo?.idProperty || 'id';
|
|
57
|
-
const polymorphicFields = context.polymorphicFields || new Set();
|
|
58
|
-
|
|
59
|
-
const { [idProperty]: id, ...allAttributes } = record;
|
|
60
|
-
|
|
61
|
-
// Get foreign keys to filter out
|
|
62
|
-
const foreignKeys = schema ? getForeignKeyFields(schema) : new Set();
|
|
63
|
-
|
|
64
|
-
// Define internal fields that should never appear in attributes
|
|
65
|
-
const internalFields = new Set([
|
|
66
|
-
RELATIONSHIPS_KEY, // '__$jsonrestapi_relationships$__'
|
|
67
|
-
RELATIONSHIP_METADATA_KEY, // '__$jsonrestapi_metadata$__'
|
|
68
|
-
ROW_NUMBER_KEY // '__$jsonrestapi_rn$__'
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
// Build attributes excluding foreign keys, polymorphic fields, and internal fields
|
|
72
|
-
const attributes = {};
|
|
73
|
-
Object.entries(allAttributes).forEach(([key, value]) => {
|
|
74
|
-
if (!foreignKeys.has(key) && !polymorphicFields.has(key) && !internalFields.has(key)) {
|
|
75
|
-
attributes[key] = value;
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
type: scopeName,
|
|
81
|
-
id: String(id),
|
|
82
|
-
attributes
|
|
83
|
-
};
|
|
84
|
-
};
|