json-rest-api 1.0.7 → 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.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,609 @@
1
+ /**
2
+ * Creates cross-table search helper functions that enable filtering across related database tables
3
+ *
4
+ * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
5
+ * @param {Object} log - Logger instance for debug and trace output
6
+ * @returns {Object} Object containing validateCrossTableField, buildJoinChain, analyzeRequiredIndexes, and createRequiredIndexes functions
7
+ *
8
+ * @example
9
+ * // Input: API scopes with relationships defined
10
+ * const scopes = {
11
+ * articles: { vars: { schemaInfo: { schema: articlesSchema, tableName: 'articles' } } },
12
+ * authors: { vars: { schemaInfo: { schema: authorsSchema, tableName: 'authors' } } },
13
+ * companies: { vars: { schemaInfo: { schema: companiesSchema, tableName: 'companies' } } }
14
+ * };
15
+ *
16
+ * const helpers = createCrossTableSearchHelpers(scopes, logger);
17
+ *
18
+ * // Output: Object with helper functions
19
+ * // {
20
+ * // validateCrossTableField: [Function],
21
+ * // buildJoinChain: [Function],
22
+ * // analyzeRequiredIndexes: [Function],
23
+ * // createRequiredIndexes: [Function]
24
+ * // }
25
+ *
26
+ * @description
27
+ * Used by:
28
+ * - rest-api-knex-plugin uses this during dataQuery to enable cross-table filtering
29
+ * - Called when filter parameters reference fields from related tables (e.g., filter[author.name]=John)
30
+ *
31
+ * Purpose:
32
+ * - Enables powerful filtering across relationships without exposing SQL complexity to API consumers
33
+ * - Automatically builds optimal JOIN chains based on schema relationships
34
+ * - Prevents N+1 queries by using efficient SQL JOINs instead of multiple queries
35
+ *
36
+ * Data flow:
37
+ * 1. API receives filter like filter[author.company.name]=Acme
38
+ * 2. rest-api-knex-plugin detects cross-table reference (contains dots)
39
+ * 3. Calls buildJoinChain to construct SQL JOIN path from articles → authors → companies
40
+ * 4. Adds JOINs to main query, enabling filtering on company name while querying articles
41
+ * 5. Returns articles where author's company name matches 'Acme'
42
+ */
43
+ export const createCrossTableSearchHelpers = (scopes, log) => {
44
+
45
+ /**
46
+ * Validates that a field exists and is indexed for cross-table search
47
+ *
48
+ * @async
49
+ * @param {string} targetScopeName - The scope containing the field to validate
50
+ * @param {string} fieldName - The field name to check
51
+ * @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
52
+ * @returns {Promise<{targetSchema: Object, fieldDef: Object}>} The validated schema and field definition
53
+ * @throws {Error} If scope not found, field not found, field not indexed, or circular reference detected
54
+ *
55
+ * @example
56
+ * // Input: Validate indexed field
57
+ * // Schema has: companies.name with indexed: true
58
+ * const result = await validateCrossTableField('companies', 'name');
59
+ *
60
+ * // Output: Successfully returns schema and field definition
61
+ * // {
62
+ * // targetSchema: { structure: { name: { type: 'string', indexed: true }, ... } },
63
+ * // fieldDef: { type: 'string', indexed: true }
64
+ * // }
65
+ *
66
+ * @example
67
+ * // Input: Non-indexed field
68
+ * // Schema has: companies.internal_code without indexed property
69
+ * try {
70
+ * await validateCrossTableField('companies', 'internal_code');
71
+ * } catch (error) {
72
+ * console.log(error.message);
73
+ * // "Field 'companies.internal_code' is not indexed. Add 'indexed: true' to allow cross-table search"
74
+ * }
75
+ *
76
+ * @description
77
+ * Used by:
78
+ * - buildJoinChain calls this to ensure target fields are properly indexed
79
+ * - analyzeRequiredIndexes uses this to identify missing indexes
80
+ *
81
+ * Purpose:
82
+ * - Cross-table searches can be slow without proper indexes
83
+ * - Forces developers to explicitly mark searchable fields with indexed: true
84
+ * - Provides clear error messages when configuration is missing
85
+ *
86
+ * Data flow:
87
+ * - Called during query building to validate field configuration
88
+ * - Ensures performance by requiring indexes on searchable fields
89
+ * - Part of the validation phase before expensive JOIN operations
90
+ */
91
+ const validateCrossTableField = async (targetScopeName, fieldName, searchedScopes = new Set()) => {
92
+ log.trace('[VALIDATE] Starting validateCrossTableField:', { targetScopeName, fieldName, searchedScopes: Array.from(searchedScopes) });
93
+
94
+ if (searchedScopes.has(targetScopeName)) {
95
+ throw new Error(`Circular reference detected: ${targetScopeName} -> ${Array.from(searchedScopes).join(' -> ')}`);
96
+ }
97
+
98
+ log.trace('[VALIDATE] Getting target schema for:', targetScopeName);
99
+ let targetSchema;
100
+ try {
101
+ targetSchema = scopes[targetScopeName].vars.schemaInfo.schema;;
102
+ log.trace('[VALIDATE] Got target schema:', { scopeName: targetScopeName, schemaKeys: Object.keys(targetSchema || {}) });
103
+ } catch (error) {
104
+ log.trace('[VALIDATE] Error getting target schema:', error.message);
105
+ throw new Error(`Target scope '${targetScopeName}' not found`);
106
+ }
107
+
108
+ if (!targetSchema) {
109
+ log.trace('[VALIDATE] Target schema is null/undefined');
110
+ throw new Error(`Target scope '${targetScopeName}' has no schema`);
111
+ }
112
+
113
+ const fieldDef = targetSchema.structure[fieldName];
114
+ log.trace('[VALIDATE] Field lookup result:', { fieldName, fieldExists: !!fieldDef });
115
+ if (!fieldDef) {
116
+ throw new Error(`Field '${fieldName}' not found in scope '${targetScopeName}'`);
117
+ }
118
+
119
+ log.trace('[VALIDATE] Checking indexed status:', { fieldName, indexed: fieldDef.indexed });
120
+ if (!fieldDef.indexed) {
121
+ throw new Error(`Field '${targetScopeName}.${fieldName}' is not indexed. Add 'indexed: true' to allow cross-table search`);
122
+ }
123
+
124
+ const result = { targetSchema, fieldDef };
125
+ log.trace('[VALIDATE] Validation successful for field:', `${targetScopeName}.${fieldName}`);
126
+ return result;
127
+ };
128
+
129
+ /**
130
+ * Builds a chain of SQL JOINs to reach a field in a related table
131
+ *
132
+ * @async
133
+ * @param {string} fromScopeName - Starting scope (e.g., 'articles')
134
+ * @param {string} targetPath - Dot-separated path to target field (e.g., 'author.company.name')
135
+ * @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
136
+ * @returns {Promise<Object>} JOIN information for query builder
137
+ * @throws {Error} If path is invalid or relationships not properly configured
138
+ *
139
+ * @example
140
+ * // Input: Simple belongsTo relationship
141
+ * // articles table has author_id field, authors schema defines belongsTo relationship
142
+ * const joinInfo = await buildJoinChain('articles', 'author.name');
143
+ *
144
+ * // Output: Single JOIN configuration
145
+ * // {
146
+ * // joinAlias: 'articles_to_authors_authors',
147
+ * // targetTableName: 'authors',
148
+ * // sourceField: 'author_id',
149
+ * // targetField: 'name',
150
+ * // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
151
+ * // isOneToMany: false,
152
+ * // isPolymorphic: false
153
+ * // }
154
+ *
155
+ * @example
156
+ * // Input: Multi-level path through relationships
157
+ * // articles → authors → companies
158
+ * const joinInfo = await buildJoinChain('articles', 'author.company.name');
159
+ *
160
+ * // Output: Multi-level JOIN chain
161
+ * // {
162
+ * // targetTableName: 'companies',
163
+ * // joinAlias: 'authors_to_companies_companies',
164
+ * // joinCondition: 'articles.author_id = articles_to_authors_authors.id AND articles_to_authors_authors.company_id = authors_to_companies_companies.id',
165
+ * // targetField: 'name',
166
+ * // isOneToMany: false,
167
+ * // isMultiLevel: true,
168
+ * // joinChain: [
169
+ * // {
170
+ * // targetTableName: 'authors',
171
+ * // joinAlias: 'articles_to_authors_authors',
172
+ * // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
173
+ * // isOneToMany: false,
174
+ * // relationshipField: 'author_id',
175
+ * // relationshipType: 'belongsTo'
176
+ * // },
177
+ * // {
178
+ * // targetTableName: 'companies',
179
+ * // joinAlias: 'authors_to_companies_companies',
180
+ * // joinCondition: 'articles_to_authors_authors.company_id = authors_to_companies_companies.id',
181
+ * // isOneToMany: false,
182
+ * // relationshipField: 'company_id',
183
+ * // relationshipType: 'belongsTo'
184
+ * // }
185
+ * // ]
186
+ * // }
187
+ *
188
+ * @example
189
+ * // Input: One-to-many relationship (hasMany)
190
+ * // authors have many articles
191
+ * const joinInfo = await buildJoinChain('authors', 'articles.title');
192
+ *
193
+ * // Output: JOIN with isOneToMany flag
194
+ * // {
195
+ * // joinAlias: 'authors_to_articles_articles',
196
+ * // targetTableName: 'articles',
197
+ * // sourceField: 'author_id',
198
+ * // targetField: 'title',
199
+ * // joinCondition: 'authors.id = authors_to_articles_articles.author_id',
200
+ * // isOneToMany: true,
201
+ * // isPolymorphic: false
202
+ * // }
203
+ *
204
+ * @description
205
+ * Used by:
206
+ * - rest-api-knex-plugin's dataQuery method when processing cross-table filters
207
+ * - Called for each filter that references a related table field
208
+ *
209
+ * Purpose:
210
+ * - Automatically constructs complex SQL JOINs from simple dot notation
211
+ * - Supports many-to-one (belongsTo), one-to-many (hasMany), and many-to-many relationships
212
+ * - Handles polymorphic relationships and multi-level paths
213
+ *
214
+ * Data flow:
215
+ * 1. Filter parser identifies cross-table reference (contains dots)
216
+ * 2. buildJoinChain analyzes relationship path segment by segment
217
+ * 3. For each segment, finds the appropriate relationship definition
218
+ * 4. Constructs JOIN conditions based on foreign keys
219
+ * 5. Returns complete JOIN information for SQL query builder
220
+ * 6. Query builder adds these JOINs to enable filtering on related data
221
+ */
222
+ const buildJoinChain = async (fromScopeName, targetPath, searchedScopes = new Set()) => {
223
+ log.trace('[BUILD-JOIN] Starting buildJoinChain:', { fromScopeName, targetPath });
224
+
225
+ const pathSegments = targetPath.split('.');
226
+ if (pathSegments.length < 2) {
227
+ throw new Error(`Invalid cross-table path '${targetPath}'. Must contain at least 'scope.field'`);
228
+ }
229
+
230
+ const targetFieldName = pathSegments[pathSegments.length - 1];
231
+ const relationshipPath = pathSegments.slice(0, -1);
232
+
233
+ log.trace('[BUILD-JOIN] Parsed path:', { relationshipPath, targetFieldName });
234
+
235
+ const joinChain = [];
236
+ let currentScope = fromScopeName;
237
+
238
+ for (let i = 0; i < relationshipPath.length; i++) {
239
+ const targetScope = relationshipPath[i];
240
+
241
+ log.trace('[BUILD-JOIN] Looking for direct relationship:', { from: currentScope, to: targetScope });
242
+
243
+ if (searchedScopes.has(currentScope)) {
244
+ throw new Error(`Circular reference detected in path: ${Array.from(searchedScopes).join(' -> ')} -> ${currentScope}`);
245
+ }
246
+ searchedScopes.add(currentScope);
247
+
248
+ const schemaInfo = scopes[currentScope].vars.schemaInfo
249
+ const currentSchema = schemaInfo.schema;
250
+ const currentRelationships = schemaInfo.schemaRelationships;
251
+
252
+ let foundRelationship = null;
253
+ let relationshipType = null;
254
+ let relationshipField = null;
255
+ let relationshipDef = null;
256
+
257
+ if (currentRelationships) {
258
+ for (const [relName, relDef] of Object.entries(currentRelationships)) {
259
+ if (relDef.hasMany === targetScope) {
260
+ if (relDef.via) {
261
+ const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships;
262
+ const viaRel = targetRelationships?.[relDef.via];
263
+
264
+ if (viaRel?.belongsToPolymorphic) {
265
+ const { typeField, idField } = viaRel.belongsToPolymorphic;
266
+ foundRelationship = targetScope;
267
+ relationshipType = 'hasManyPolymorphic';
268
+ relationshipField = { typeField, idField, via: relDef.via };
269
+ relationshipDef = relDef;
270
+ log.trace('[BUILD-JOIN] Found polymorphic hasMany relationship:', {
271
+ relName, targetScope, via: relDef.via, typeField, idField
272
+ });
273
+ break;
274
+ }
275
+ }
276
+
277
+ if (relDef.through) {
278
+ foundRelationship = targetScope;
279
+ relationshipType = 'manyToMany';
280
+ relationshipField = {
281
+ through: relDef.through,
282
+ foreignKey: relDef.foreignKey,
283
+ otherKey: relDef.otherKey
284
+ };
285
+ relationshipDef = relDef;
286
+ log.trace('[BUILD-JOIN] Found many-to-many relationship:', {
287
+ relName,
288
+ targetScope,
289
+ through: relDef.through,
290
+ foreignKey: relDef.foreignKey,
291
+ otherKey: relDef.otherKey
292
+ });
293
+ break;
294
+ }
295
+
296
+ foundRelationship = targetScope;
297
+ relationshipType = 'hasMany';
298
+ relationshipField = relDef.foreignKey;
299
+ if (!relationshipField) {
300
+ log.error('[BUILD-JOIN] Missing foreignKey in hasMany relationship:', { relName, currentScope });
301
+ throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${currentScope}'`);
302
+ }
303
+ relationshipDef = relDef;
304
+ log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField });
305
+ break;
306
+ }
307
+ }
308
+ }
309
+
310
+ if (!foundRelationship) {
311
+ for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
312
+ if (fieldDef.belongsTo === targetScope) {
313
+ foundRelationship = targetScope;
314
+ relationshipType = 'belongsTo';
315
+ relationshipField = fieldName;
316
+ relationshipDef = fieldDef;
317
+ log.trace('[BUILD-JOIN] Found belongsTo relationship:', { fieldName, targetScope });
318
+ break;
319
+ }
320
+ }
321
+ }
322
+
323
+ if (!foundRelationship) {
324
+ throw new Error(
325
+ `No searchable relationship from '${currentScope}' to '${targetScope}'. `
326
+ );
327
+ }
328
+
329
+ const sourceSchema = scopes[currentScope].vars.schemaInfo.schema;
330
+ const sourceTableName = scopes[currentScope].vars.schemaInfo.tableName
331
+ const targetSchema = scopes[targetScope].vars.schemaInfo.schema;
332
+ const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
333
+
334
+ if (relationshipType === 'manyToMany') {
335
+ const { through, foreignKey, otherKey } = relationshipField;
336
+ const pivotTableName = scopes[through].vars.schemaInfo.tableName || through;
337
+ const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
338
+
339
+ const pivotAlias = `${currentScope}_to_${through}_${through}`;
340
+ const pivotJoinCondition = `${previousAlias}.id = ${pivotAlias}.${foreignKey}`;
341
+
342
+ joinChain.push({
343
+ targetTableName: pivotTableName,
344
+ joinAlias: pivotAlias,
345
+ joinCondition: pivotJoinCondition,
346
+ isOneToMany: true,
347
+ isPolymorphic: false,
348
+ relationshipField: foreignKey,
349
+ relationshipType: 'manyToMany_pivot'
350
+ });
351
+
352
+ const targetAlias = `${through}_to_${targetScope}_${targetScope}`;
353
+ const targetJoinCondition = `${pivotAlias}.${otherKey} = ${targetAlias}.id`;
354
+
355
+ joinChain.push({
356
+ targetTableName,
357
+ joinAlias: targetAlias,
358
+ joinCondition: targetJoinCondition,
359
+ isOneToMany: false,
360
+ isPolymorphic: false,
361
+ relationshipField: otherKey,
362
+ relationshipType: 'manyToMany_target'
363
+ });
364
+ } else {
365
+ const joinAlias = `${currentScope}_to_${targetScope}_${targetScope}`;
366
+
367
+ let joinCondition;
368
+ if (relationshipType === 'hasManyPolymorphic') {
369
+ const { typeField, idField } = relationshipField;
370
+ const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
371
+ joinCondition = `${joinAlias}.${typeField} = '${currentScope}' AND ${joinAlias}.${idField} = ${previousAlias}.id`;
372
+ } else if (relationshipType === 'hasMany') {
373
+ const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
374
+ joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`;
375
+ } else {
376
+ const previousAlias = i === 0 ? sourceTableName : joinChain[i-1].joinAlias;
377
+ joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`;
378
+ }
379
+
380
+ joinChain.push({
381
+ targetTableName,
382
+ joinAlias,
383
+ joinCondition,
384
+ isOneToMany: relationshipType === 'hasMany' || relationshipType === 'hasManyPolymorphic',
385
+ isPolymorphic: relationshipType === 'hasManyPolymorphic',
386
+ relationshipField,
387
+ relationshipType
388
+ });
389
+ }
390
+
391
+ currentScope = targetScope;
392
+ }
393
+
394
+ const finalScope = relationshipPath[relationshipPath.length - 1];
395
+ await validateCrossTableField(finalScope, targetFieldName, searchedScopes);
396
+
397
+ if (joinChain.length === 0) {
398
+ throw new Error(`Invalid path '${targetPath}': no relationships to traverse`);
399
+ }
400
+
401
+ const lastJoin = joinChain[joinChain.length - 1];
402
+
403
+ if (joinChain.length > 1) {
404
+ return {
405
+ targetTableName: lastJoin.targetTableName,
406
+ joinAlias: lastJoin.joinAlias,
407
+ joinCondition: joinChain.map(j => j.joinCondition).join(' AND '),
408
+ targetField: targetFieldName,
409
+ isOneToMany: joinChain.some(j => j.isOneToMany),
410
+ isMultiLevel: true,
411
+ joinChain
412
+ };
413
+ } else {
414
+ return {
415
+ joinAlias: lastJoin.joinAlias,
416
+ targetTableName: lastJoin.targetTableName,
417
+ sourceField: lastJoin.relationshipField,
418
+ targetField: targetFieldName,
419
+ joinCondition: lastJoin.joinCondition,
420
+ isOneToMany: lastJoin.isOneToMany,
421
+ isPolymorphic: lastJoin.isPolymorphic
422
+ };
423
+ }
424
+ };
425
+
426
+ /**
427
+ * Analyzes a search schema to identify which fields need database indexes
428
+ *
429
+ * @param {string} scopeName - The scope being analyzed
430
+ * @param {Object} searchSchema - Search schema definition with filter fields
431
+ * @returns {Array<Object>} Array of required indexes with scope, field, and reason
432
+ *
433
+ * @example
434
+ * // Input: Search schema with cross-table references
435
+ * const searchSchema = {
436
+ * authorName: {
437
+ * type: 'string',
438
+ * actualField: 'authors.name',
439
+ * filterOperator: 'like'
440
+ * },
441
+ * companyName: {
442
+ * type: 'string',
443
+ * actualField: 'authors.company.name'
444
+ * },
445
+ * search: {
446
+ * type: 'string',
447
+ * oneOf: ['title', 'authors.name', 'authors.company.name']
448
+ * }
449
+ * };
450
+ *
451
+ * const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
452
+ *
453
+ * // Output: List of fields that need indexes
454
+ * // [
455
+ * // { scope: 'authors', field: 'name', reason: 'Cross-table search from articles.authorName' },
456
+ * // { scope: 'authors', field: 'company', reason: 'Cross-table search from articles.companyName' },
457
+ * // { scope: 'authors', field: 'name', reason: 'Cross-table oneOf search from articles.search' },
458
+ * // { scope: 'authors', field: 'company', reason: 'Cross-table oneOf search from articles.search' }
459
+ * // ]
460
+ *
461
+ * @example
462
+ * // Input: No cross-table references
463
+ * const searchSchema = {
464
+ * title: { type: 'string' },
465
+ * status: { type: 'string' }
466
+ * };
467
+ *
468
+ * const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
469
+ *
470
+ * // Output: Empty array - no cross-table indexes needed
471
+ * // []
472
+ *
473
+ * @description
474
+ * Used by:
475
+ * - Called during API initialization to identify missing indexes
476
+ * - Used by developers to understand performance requirements
477
+ *
478
+ * Purpose:
479
+ * - Cross-table searches require indexes for acceptable performance
480
+ * - Helps identify configuration issues before they cause slow queries
481
+ * - Provides clear documentation of index requirements
482
+ *
483
+ * Data flow:
484
+ * - Runs during schema compilation phase
485
+ * - Analyzes search schemas to find cross-table references
486
+ * - Output can be used to create indexes manually or automatically
487
+ * - Prevents performance issues before queries are executed
488
+ */
489
+ const analyzeRequiredIndexes = (scopeName, searchSchema) => {
490
+ const requiredIndexes = [];
491
+
492
+ const schemaToAnalyze = searchSchema.structure || searchSchema;
493
+
494
+ Object.entries(schemaToAnalyze).forEach(([filterKey, fieldDef]) => {
495
+ if (fieldDef.actualField && fieldDef.actualField.includes('.')) {
496
+ const [targetScopeName, targetFieldName] = fieldDef.actualField.split('.');
497
+ requiredIndexes.push({
498
+ scope: targetScopeName,
499
+ field: targetFieldName,
500
+ reason: `Cross-table search from ${scopeName}.${filterKey}`
501
+ });
502
+ }
503
+
504
+ if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
505
+ fieldDef.oneOf.forEach(field => {
506
+ if (field.includes('.')) {
507
+ const [targetScopeName, targetFieldName] = field.split('.');
508
+ requiredIndexes.push({
509
+ scope: targetScopeName,
510
+ field: targetFieldName,
511
+ reason: `Cross-table oneOf search from ${scopeName}.${filterKey}`
512
+ });
513
+ }
514
+ });
515
+ }
516
+ });
517
+
518
+ return requiredIndexes;
519
+ };
520
+
521
+ /**
522
+ * Creates database indexes for fields identified by analyzeRequiredIndexes
523
+ *
524
+ * @async
525
+ * @param {Array<Object>} requiredIndexes - Index requirements from analyzeRequiredIndexes
526
+ * @param {Object} knex - Knex database connection instance
527
+ * @returns {Promise<Array<Object>>} Array of successfully created indexes
528
+ *
529
+ * @example
530
+ * // Input: Required indexes from analysis
531
+ * const requiredIndexes = [
532
+ * { scope: 'authors', field: 'name', reason: 'Cross-table search' },
533
+ * { scope: 'companies', field: 'name', reason: 'Cross-table search' }
534
+ * ];
535
+ *
536
+ * const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
537
+ *
538
+ * // Output: Successfully created indexes
539
+ * // [
540
+ * // { tableName: 'authors', field: 'name', indexName: 'idx_authors_name_search' },
541
+ * // { tableName: 'companies', field: 'name', indexName: 'idx_companies_name_search' }
542
+ * // ]
543
+ *
544
+ * // Database effect: CREATE INDEX idx_authors_name_search ON authors(name);
545
+ * // Database effect: CREATE INDEX idx_companies_name_search ON companies(name);
546
+ *
547
+ * @example
548
+ * // Input: Some indexes already exist
549
+ * const requiredIndexes = [
550
+ * { scope: 'authors', field: 'name', reason: 'Cross-table search' }, // Already exists
551
+ * { scope: 'authors', field: 'email', reason: 'Cross-table search' } // New
552
+ * ];
553
+ *
554
+ * const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
555
+ *
556
+ * // Output: Only newly created indexes
557
+ * // [
558
+ * // { tableName: 'authors', field: 'email', indexName: 'idx_authors_email_search' }
559
+ * // ]
560
+ *
561
+ * @description
562
+ * Used by:
563
+ * - Can be called during database migration or setup
564
+ * - Used by developers to automatically create performance indexes
565
+ *
566
+ * Purpose:
567
+ * - Automates index creation based on search schema requirements
568
+ * - Ensures consistent index naming across the application
569
+ * - Safely handles cases where indexes already exist
570
+ *
571
+ * Data flow:
572
+ * - Typically runs during database setup or migration
573
+ * - Creates indexes identified by analyzeRequiredIndexes
574
+ * - Improves query performance for cross-table searches
575
+ * - Part of the database optimization phase
576
+ */
577
+ const createRequiredIndexes = async (requiredIndexes, knex) => {
578
+ const createdIndexes = [];
579
+
580
+ for (const indexInfo of requiredIndexes) {
581
+ const { scope, field } = indexInfo;
582
+ const schema = scopes[scope].vars.schemaInfo.schema;
583
+ const tableName = scopes[scope].vars.schemaInfo.tableName
584
+ const indexName = `idx_${tableName}_${field}_search`;
585
+
586
+ try {
587
+ const hasIndex = await knex.schema.hasIndex(tableName, [field]);
588
+ if (!hasIndex) {
589
+ await knex.schema.table(tableName, table => {
590
+ table.index([field], indexName);
591
+ });
592
+ createdIndexes.push({ tableName, field, indexName });
593
+ log.info(`Created index: ${indexName} on ${tableName}.${field}`);
594
+ }
595
+ } catch (error) {
596
+ log.warn(`Failed to create index on ${tableName}.${field}:`, error.message);
597
+ }
598
+ }
599
+
600
+ return createdIndexes;
601
+ };
602
+
603
+ return {
604
+ validateCrossTableField,
605
+ buildJoinChain,
606
+ analyzeRequiredIndexes,
607
+ createRequiredIndexes
608
+ };
609
+ };