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.
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 +99 -33
  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
@@ -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
- };