json-rest-api 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/audit/00-charter.md +103 -0
  2. package/audit/01-scope-map.md +84 -0
  3. package/audit/02-coverage-matrix.csv +174 -0
  4. package/audit/03-invariants.md +123 -0
  5. package/audit/04-standards-rubric.md +56 -0
  6. package/audit/05-competitor-rubric.md +77 -0
  7. package/audit/06-findings.md +160 -0
  8. package/audit/07-risk-register.csv +10 -0
  9. package/audit/08-remediation-roadmap.md +177 -0
  10. package/audit/09-signoff-check.md +23 -0
  11. package/audit/10-final-synthesis.md +29 -0
  12. package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
  13. package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
  14. package/audit/evidence/code/boundary-leak-checks.txt +72 -0
  15. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
  16. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
  17. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
  18. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
  19. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
  20. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
  21. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
  22. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
  23. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
  24. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
  25. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
  26. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
  27. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
  28. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
  29. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
  30. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
  31. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
  32. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
  33. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
  34. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
  35. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
  36. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
  37. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
  38. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
  39. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
  40. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
  41. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
  42. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
  43. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
  44. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
  45. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
  46. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
  47. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
  48. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
  49. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
  50. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
  51. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
  52. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
  53. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
  54. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
  55. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
  56. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
  57. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
  58. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
  59. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
  60. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
  61. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
  62. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
  63. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
  64. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
  65. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
  66. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
  67. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
  68. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
  69. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
  70. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
  71. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
  72. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
  73. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
  74. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
  75. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
  76. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
  77. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
  78. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
  79. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
  80. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
  81. package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
  82. package/audit/evidence/code/critical-pass1-summary.csv +67 -0
  83. package/audit/evidence/code/critical-pass2-summary.csv +67 -0
  84. package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
  85. package/audit/evidence/code/error-model-signals.txt +98 -0
  86. package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
  87. package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
  88. package/audit/evidence/code/hook-flow-signals.txt +282 -0
  89. package/audit/evidence/code/idproperty-signals.txt +157 -0
  90. package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
  91. package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
  92. package/audit/evidence/code/imports-json-rest-api.txt +354 -0
  93. package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
  94. package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
  95. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
  96. package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
  97. package/audit/evidence/code/permission-signals.txt +193 -0
  98. package/audit/evidence/code/query-include-signals.txt +687 -0
  99. package/audit/evidence/code/transaction-signals.txt +28 -0
  100. package/audit/evidence/code/validation-signals.txt +650 -0
  101. package/audit/evidence/code/write-path-signals.txt +61 -0
  102. package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
  103. package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
  104. package/audit/evidence/external/corpus-freeze.md +21 -0
  105. package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
  106. package/audit/evidence/external/npm-download-signals.txt +23 -0
  107. package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
  108. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
  109. package/audit/evidence/external/readme-fortune-signals.txt +26 -0
  110. package/audit/evidence/external/readme-fortune.md +165 -0
  111. package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
  112. package/audit/evidence/external/readme-json-api-serializer.md +690 -0
  113. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  114. package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
  115. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
  116. package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
  117. package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
  118. package/audit/evidence/external/readme-jsonapi-server.md +104 -0
  119. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
  120. package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
  121. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
  122. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
  123. package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
  124. package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
  125. package/audit/evidence/external/standards-sources.txt +6 -0
  126. package/audit/evidence/runtime/baseline-commits.txt +2 -0
  127. package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
  128. package/audit/evidence/runtime/environment.txt +4 -0
  129. package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
  130. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
  131. package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
  132. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
  133. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
  134. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
  135. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
  136. package/audit/evidence/runtime/test-exitcodes.txt +4 -0
  137. package/audit/evidence/runtime/test-summary.md +107 -0
  138. package/audit/evidence/runtime/trace-anchors.txt +61 -0
  139. package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
  140. package/audit/evidence/tests/failing-tests-summary.txt +25 -0
  141. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
  142. package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
  143. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
  144. package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
  145. package/audit/evidence/tests/query-include-test-signals.txt +948 -0
  146. package/audit/evidence/tests/relationship-test-signals.txt +223 -0
  147. package/audit/evidence/tests/skipped-tests.txt +10 -0
  148. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
  149. package/audit/evidence/tests/test-suite-list.txt +32 -0
  150. package/audit/subsystems/docs-drift.md +49 -0
  151. package/audit/subsystems/pass2-cross-review.md +131 -0
  152. package/audit/subsystems/plugins-access.md +51 -0
  153. package/audit/subsystems/plugins-bulk.md +50 -0
  154. package/audit/subsystems/plugins-cors.md +48 -0
  155. package/audit/subsystems/plugins-file.md +53 -0
  156. package/audit/subsystems/plugins-multihome.md +50 -0
  157. package/audit/subsystems/plugins-positioning.md +54 -0
  158. package/audit/subsystems/plugins-socketio.md +50 -0
  159. package/audit/subsystems/query-engine.md +61 -0
  160. package/audit/subsystems/relationships.md +55 -0
  161. package/audit/subsystems/rest-orchestration.md +66 -0
  162. package/audit/subsystems/schema-core.md +64 -0
  163. package/audit/subsystems/storage-knex.md +57 -0
  164. package/audit/subsystems/tests.md +48 -0
  165. package/audit/subsystems/transport-express.md +55 -0
  166. package/audit/traces/trace-read-get.md +22 -0
  167. package/audit/traces/trace-read-query.md +25 -0
  168. package/audit/traces/trace-relationship-endpoints.md +31 -0
  169. package/audit/traces/trace-write-delete.md +17 -0
  170. package/audit/traces/trace-write-patch.md +26 -0
  171. package/audit/traces/trace-write-post.md +22 -0
  172. package/audit/traces/trace-write-put.md +23 -0
  173. package/package.json +5 -5
@@ -0,0 +1,565 @@
1
+ 1 // Cross-table search helper functions that enable filtering across related database tables
2
+ 2
3
+ 3 /**
4
+ 4 * Validates that a field exists and is indexed for cross-table search
5
+ 5 *
6
+ 6 * @async
7
+ 7 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
8
+ 8 * @param {Object} log - Logger instance for debug and trace output
9
+ 9 * @param {string} targetScopeName - The scope containing the field to validate
10
+ 10 * @param {string} fieldName - The field name to check
11
+ 11 * @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
12
+ 12 * @returns {Promise<void>}
13
+ 13 * @throws {Error} If scope not found, field not found, field not indexed, or circular reference detected
14
+ 14 *
15
+ 15 * @example
16
+ 16 * // Input: Validate indexed field
17
+ 17 * // Schema has: companies.name with indexed: true
18
+ 18 * await validateCrossTableField(scopes, log, 'companies', 'name');
19
+ 19 *
20
+ 20 * // Output: Validation succeeds (no error thrown)
21
+ 21 *
22
+ 22 * @example
23
+ 23 * // Input: Non-indexed field
24
+ 24 * // Schema has: companies.internal_code without indexed property
25
+ 25 * try {
26
+ 26 * await validateCrossTableField('companies', 'internal_code');
27
+ 27 * } catch (error) {
28
+ 28 * console.log(error.message);
29
+ 29 * // "Field 'companies.internal_code' is not indexed. Add 'indexed: true' to allow cross-table search"
30
+ 30 * }
31
+ 31 *
32
+ 32 * @description
33
+ 33 * Used by:
34
+ 34 * - buildJoinChain calls this to ensure target fields are properly indexed
35
+ 35 * - analyzeRequiredIndexes uses this to identify missing indexes
36
+ 36 *
37
+ 37 * Purpose:
38
+ 38 * - Cross-table searches can be slow without proper indexes
39
+ 39 * - Forces developers to explicitly mark searchable fields with indexed: true
40
+ 40 * - Provides clear error messages when configuration is missing
41
+ 41 *
42
+ 42 * Data flow:
43
+ 43 * - Called during query building to validate field configuration
44
+ 44 * - Ensures performance by requiring indexes on searchable fields
45
+ 45 * - Part of the validation phase before expensive JOIN operations
46
+ 46 */
47
+ 47 export const validateCrossTableField = async (scopes, log, targetScopeName, fieldName, searchedScopes = new Set()) => {
48
+ 48 log.trace('[VALIDATE] Starting validateCrossTableField:', { targetScopeName, fieldName, searchedScopes: Array.from(searchedScopes) })
49
+ 49
50
+ 50 if (searchedScopes.has(targetScopeName)) {
51
+ 51 throw new Error(`Circular reference detected: ${targetScopeName} -> ${Array.from(searchedScopes).join(' -> ')}`)
52
+ 52 }
53
+ 53
54
+ 54 log.trace('[VALIDATE] Getting target schema for:', targetScopeName)
55
+ 55 let targetSchemaInstance
56
+ 56 try {
57
+ 57 targetSchemaInstance = scopes[targetScopeName].vars.schemaInfo.schemaInstance
58
+ 58 log.trace('[VALIDATE] Got target schema:', { scopeName: targetScopeName, schemaKeys: Object.keys(targetSchemaInstance || {}) })
59
+ 59 } catch (error) {
60
+ 60 log.trace('[VALIDATE] Error getting target schema:', error.message)
61
+ 61 throw new Error(`Target scope '${targetScopeName}' not found`)
62
+ 62 }
63
+ 63
64
+ 64 if (!targetSchemaInstance) {
65
+ 65 log.trace('[VALIDATE] Target schema is null/undefined')
66
+ 66 throw new Error(`Target scope '${targetScopeName}' has no schema`)
67
+ 67 }
68
+ 68
69
+ 69 const fieldDef = targetSchemaInstance.structure[fieldName]
70
+ 70 log.trace('[VALIDATE] Field lookup result:', { fieldName, fieldExists: !!fieldDef })
71
+ 71 if (!fieldDef) {
72
+ 72 throw new Error(`Field '${fieldName}' not found in scope '${targetScopeName}'`)
73
+ 73 }
74
+ 74
75
+ 75 log.trace('[VALIDATE] Checking indexed status:', { fieldName, indexed: fieldDef.indexed })
76
+ 76 if (!fieldDef.indexed) {
77
+ 77 throw new Error(`Field '${targetScopeName}.${fieldName}' is not indexed. Add 'indexed: true' to allow cross-table search`)
78
+ 78 }
79
+ 79
80
+ 80 log.trace('[VALIDATE] Validation successful for field:', `${targetScopeName}.${fieldName}`)
81
+ 81 }
82
+ 82
83
+ 83 /**
84
+ 84 * Builds a chain of SQL JOINs to reach a field in a related table
85
+ 85 *
86
+ 86 * @async
87
+ 87 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
88
+ 88 * @param {Object} log - Logger instance for debug and trace output
89
+ 89 * @param {string} fromScopeName - Starting scope (e.g., 'articles')
90
+ 90 * @param {string} targetPath - Dot-separated path to target field (e.g., 'author.company.name')
91
+ 91 * @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
92
+ 92 * @returns {Promise<Object>} JOIN information for query builder
93
+ 93 * @throws {Error} If path is invalid or relationships not properly configured
94
+ 94 *
95
+ 95 * @example
96
+ 96 * // Input: Simple belongsTo relationship
97
+ 97 * // articles table has author_id field, authors schema defines belongsTo relationship
98
+ 98 * const joinInfo = await buildJoinChain('articles', 'author.name');
99
+ 99 *
100
+ 100 * // Output: Single JOIN configuration
101
+ 101 * // {
102
+ 102 * // joinAlias: 'articles_to_authors_authors',
103
+ 103 * // targetTableName: 'authors',
104
+ 104 * // sourceField: 'author_id',
105
+ 105 * // targetField: 'name',
106
+ 106 * // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
107
+ 107 * // isOneToMany: false,
108
+ 108 * // isPolymorphic: false
109
+ 109 * // }
110
+ 110 *
111
+ 111 * @example
112
+ 112 * // Input: Multi-level path through relationships
113
+ 113 * // articles → authors → companies
114
+ 114 * const joinInfo = await buildJoinChain('articles', 'author.company.name');
115
+ 115 *
116
+ 116 * // Output: Multi-level JOIN chain
117
+ 117 * // {
118
+ 118 * // targetTableName: 'companies',
119
+ 119 * // joinAlias: 'authors_to_companies_companies',
120
+ 120 * // joinCondition: 'articles.author_id = articles_to_authors_authors.id AND articles_to_authors_authors.company_id = authors_to_companies_companies.id',
121
+ 121 * // targetField: 'name',
122
+ 122 * // isOneToMany: false,
123
+ 123 * // isMultiLevel: true,
124
+ 124 * // joinChain: [
125
+ 125 * // {
126
+ 126 * // targetTableName: 'authors',
127
+ 127 * // joinAlias: 'articles_to_authors_authors',
128
+ 128 * // joinCondition: 'articles.author_id = articles_to_authors_authors.id',
129
+ 129 * // isOneToMany: false,
130
+ 130 * // relationshipField: 'author_id',
131
+ 131 * // relationshipType: 'belongsTo'
132
+ 132 * // },
133
+ 133 * // {
134
+ 134 * // targetTableName: 'companies',
135
+ 135 * // joinAlias: 'authors_to_companies_companies',
136
+ 136 * // joinCondition: 'articles_to_authors_authors.company_id = authors_to_companies_companies.id',
137
+ 137 * // isOneToMany: false,
138
+ 138 * // relationshipField: 'company_id',
139
+ 139 * // relationshipType: 'belongsTo'
140
+ 140 * // }
141
+ 141 * // ]
142
+ 142 * // }
143
+ 143 *
144
+ 144 * @example
145
+ 145 * // Input: One-to-many relationship (hasMany)
146
+ 146 * // authors have many articles
147
+ 147 * const joinInfo = await buildJoinChain('authors', 'articles.title');
148
+ 148 *
149
+ 149 * // Output: JOIN with isOneToMany flag
150
+ 150 * // {
151
+ 151 * // joinAlias: 'authors_to_articles_articles',
152
+ 152 * // targetTableName: 'articles',
153
+ 153 * // sourceField: 'author_id',
154
+ 154 * // targetField: 'title',
155
+ 155 * // joinCondition: 'authors.id = authors_to_articles_articles.author_id',
156
+ 156 * // isOneToMany: true,
157
+ 157 * // isPolymorphic: false
158
+ 158 * // }
159
+ 159 *
160
+ 160 * @description
161
+ 161 * Used by:
162
+ 162 * - rest-api-knex-plugin's dataQuery method when processing cross-table filters
163
+ 163 * - Called for each filter that references a related table field
164
+ 164 *
165
+ 165 * Purpose:
166
+ 166 * - Automatically constructs complex SQL JOINs from simple dot notation
167
+ 167 * - Supports many-to-one (belongsTo), one-to-many (hasMany), and many-to-many relationships
168
+ 168 * - Handles polymorphic relationships and multi-level paths
169
+ 169 *
170
+ 170 * Data flow:
171
+ 171 * 1. Filter parser identifies cross-table reference (contains dots)
172
+ 172 * 2. buildJoinChain analyzes relationship path segment by segment
173
+ 173 * 3. For each segment, finds the appropriate relationship definition
174
+ 174 * 4. Constructs JOIN conditions based on foreign keys
175
+ 175 * 5. Returns complete JOIN information for SQL query builder
176
+ 176 * 6. Query builder adds these JOINs to enable filtering on related data
177
+ 177 */
178
+ 178 export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, searchedScopes = new Set()) => {
179
+ 179 log.trace('[BUILD-JOIN] Starting buildJoinChain:', { fromScopeName, targetPath })
180
+ 180
181
+ 181 const pathSegments = targetPath.split('.')
182
+ 182 if (pathSegments.length < 2) {
183
+ 183 throw new Error(`Invalid cross-table path '${targetPath}'. Must contain at least 'scope.field'`)
184
+ 184 }
185
+ 185
186
+ 186 const targetFieldName = pathSegments[pathSegments.length - 1]
187
+ 187 const relationshipPath = pathSegments.slice(0, -1)
188
+ 188
189
+ 189 log.trace('[BUILD-JOIN] Parsed path:', { relationshipPath, targetFieldName })
190
+ 190
191
+ 191 const joinChain = []
192
+ 192 let currentScope = fromScopeName
193
+ 193
194
+ 194 for (let i = 0; i < relationshipPath.length; i++) {
195
+ 195 const targetScope = relationshipPath[i]
196
+ 196
197
+ 197 log.trace('[BUILD-JOIN] Looking for direct relationship:', { from: currentScope, to: targetScope })
198
+ 198
199
+ 199 if (searchedScopes.has(currentScope)) {
200
+ 200 throw new Error(`Circular reference detected in path: ${Array.from(searchedScopes).join(' -> ')} -> ${currentScope}`)
201
+ 201 }
202
+ 202 searchedScopes.add(currentScope)
203
+ 203
204
+ 204 const schemaInfo = scopes[currentScope].vars.schemaInfo
205
+ 205 const currentSchema = schemaInfo.schemaInstance
206
+ 206 const currentRelationships = schemaInfo.schemaRelationships
207
+ 207
208
+ 208 let foundRelationship = null
209
+ 209 let relationshipType = null
210
+ 210 let relationshipField = null
211
+ 211 let relationshipDef = null
212
+ 212
213
+ 213 if (currentRelationships) {
214
+ 214 for (const [relName, relDef] of Object.entries(currentRelationships)) {
215
+ 215 if (relDef.type === 'hasMany' && relDef.target === targetScope) {
216
+ 216 if (relDef.via) {
217
+ 217 const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships
218
+ 218 const viaRel = targetRelationships?.[relDef.via]
219
+ 219
220
+ 220 if (viaRel?.belongsToPolymorphic) {
221
+ 221 const { typeField, idField } = viaRel.belongsToPolymorphic
222
+ 222 foundRelationship = targetScope
223
+ 223 relationshipType = 'hasManyPolymorphic'
224
+ 224 relationshipField = { typeField, idField, via: relDef.via }
225
+ 225 relationshipDef = relDef
226
+ 226 log.trace('[BUILD-JOIN] Found polymorphic hasMany relationship:', {
227
+ 227 relName, targetScope, via: relDef.via, typeField, idField
228
+ 228 })
229
+ 229 break
230
+ 230 }
231
+ 231 }
232
+ 232
233
+ 233 if (relDef.through) {
234
+ 234 foundRelationship = targetScope
235
+ 235 relationshipType = 'manyToMany'
236
+ 236 relationshipField = {
237
+ 237 through: relDef.through,
238
+ 238 foreignKey: relDef.foreignKey,
239
+ 239 otherKey: relDef.otherKey
240
+ 240 }
241
+ 241 relationshipDef = relDef
242
+ 242 log.trace('[BUILD-JOIN] Found many-to-many relationship:', {
243
+ 243 relName,
244
+ 244 targetScope,
245
+ 245 through: relDef.through,
246
+ 246 foreignKey: relDef.foreignKey,
247
+ 247 otherKey: relDef.otherKey
248
+ 248 })
249
+ 249 break
250
+ 250 }
251
+ 251
252
+ 252 foundRelationship = targetScope
253
+ 253 relationshipType = 'hasMany'
254
+ 254 relationshipField = relDef.foreignKey
255
+ 255 if (!relationshipField) {
256
+ 256 log.error('[BUILD-JOIN] Missing foreignKey in hasMany relationship:', { relName, currentScope })
257
+ 257 throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${currentScope}'`)
258
+ 258 }
259
+ 259 relationshipDef = relDef
260
+ 260 log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField })
261
+ 261 break
262
+ 262 }
263
+ 263 }
264
+ 264 }
265
+ 265
266
+ 266 if (!foundRelationship) {
267
+ 267 for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
268
+ 268 if (fieldDef.belongsTo === targetScope) {
269
+ 269 foundRelationship = targetScope
270
+ 270 relationshipType = 'belongsTo'
271
+ 271 relationshipField = fieldName
272
+ 272 relationshipDef = fieldDef
273
+ 273 log.trace('[BUILD-JOIN] Found belongsTo relationship:', { fieldName, targetScope })
274
+ 274 break
275
+ 275 }
276
+ 276 }
277
+ 277 }
278
+ 278
279
+ 279 if (!foundRelationship) {
280
+ 280 throw new Error(
281
+ 281 `No searchable relationship from '${currentScope}' to '${targetScope}'. `
282
+ 282 )
283
+ 283 }
284
+ 284
285
+ 285 const sourceSchema = scopes[currentScope].vars.schemaInfo.schemaInstance
286
+ 286 const sourceTableName = scopes[currentScope].vars.schemaInfo.tableName
287
+ 287 const targetSchemaInstance = scopes[targetScope].vars.schemaInfo.schemaInstance
288
+ 288 const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
289
+ 289
290
+ 290 if (relationshipType === 'manyToMany') {
291
+ 291 const { through, foreignKey, otherKey } = relationshipField
292
+ 292 const pivotTableName = scopes[through].vars.schemaInfo.tableName || through
293
+ 293 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
294
+ 294
295
+ 295 const pivotAlias = `${currentScope}_to_${through}_${through}`
296
+ 296 const pivotJoinCondition = `${previousAlias}.id = ${pivotAlias}.${foreignKey}`
297
+ 297
298
+ 298 joinChain.push({
299
+ 299 targetTableName: pivotTableName,
300
+ 300 joinAlias: pivotAlias,
301
+ 301 joinCondition: pivotJoinCondition,
302
+ 302 isOneToMany: true,
303
+ 303 isPolymorphic: false,
304
+ 304 relationshipField: foreignKey,
305
+ 305 relationshipType: 'manyToMany_pivot',
306
+ 306 targetScopeName: through
307
+ 307 })
308
+ 308
309
+ 309 const targetAlias = `${through}_to_${targetScope}_${targetScope}`
310
+ 310 const targetJoinCondition = `${pivotAlias}.${otherKey} = ${targetAlias}.id`
311
+ 311
312
+ 312 joinChain.push({
313
+ 313 targetTableName,
314
+ 314 joinAlias: targetAlias,
315
+ 315 joinCondition: targetJoinCondition,
316
+ 316 isOneToMany: false,
317
+ 317 isPolymorphic: false,
318
+ 318 relationshipField: otherKey,
319
+ 319 relationshipType: 'manyToMany_target',
320
+ 320 targetScopeName: targetScope
321
+ 321 })
322
+ 322 } else {
323
+ 323 const joinAlias = `${currentScope}_to_${targetScope}_${targetScope}`
324
+ 324
325
+ 325 let joinCondition
326
+ 326 if (relationshipType === 'hasManyPolymorphic') {
327
+ 327 const { typeField, idField } = relationshipField
328
+ 328 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
329
+ 329 joinCondition = `${joinAlias}.${typeField} = '${currentScope}' AND ${joinAlias}.${idField} = ${previousAlias}.id`
330
+ 330 } else if (relationshipType === 'hasMany') {
331
+ 331 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
332
+ 332 joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`
333
+ 333 } else {
334
+ 334 const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
335
+ 335 joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`
336
+ 336 }
337
+ 337
338
+ 338 joinChain.push({
339
+ 339 targetTableName,
340
+ 340 joinAlias,
341
+ 341 joinCondition,
342
+ 342 isOneToMany: relationshipType === 'hasMany' || relationshipType === 'hasManyPolymorphic',
343
+ 343 isPolymorphic: relationshipType === 'hasManyPolymorphic',
344
+ 344 relationshipField,
345
+ 345 relationshipType,
346
+ 346 targetScopeName: targetScope
347
+ 347 })
348
+ 348 }
349
+ 349
350
+ 350 currentScope = targetScope
351
+ 351 }
352
+ 352
353
+ 353 const finalScope = relationshipPath[relationshipPath.length - 1]
354
+ 354 await validateCrossTableField(scopes, log, finalScope, targetFieldName, searchedScopes)
355
+ 355
356
+ 356 if (joinChain.length === 0) {
357
+ 357 throw new Error(`Invalid path '${targetPath}': no relationships to traverse`)
358
+ 358 }
359
+ 359
360
+ 360 const lastJoin = joinChain[joinChain.length - 1]
361
+ 361
362
+ 362 if (joinChain.length > 1) {
363
+ 363 return {
364
+ 364 targetTableName: lastJoin.targetTableName,
365
+ 365 joinAlias: lastJoin.joinAlias,
366
+ 366 joinCondition: joinChain.map(j => j.joinCondition).join(' AND '),
367
+ 367 targetField: targetFieldName,
368
+ 368 isOneToMany: joinChain.some(j => j.isOneToMany),
369
+ 369 isMultiLevel: true,
370
+ 370 joinChain,
371
+ 371 targetScopeName: finalScope
372
+ 372 }
373
+ 373 } else {
374
+ 374 return {
375
+ 375 joinAlias: lastJoin.joinAlias,
376
+ 376 targetTableName: lastJoin.targetTableName,
377
+ 377 sourceField: lastJoin.relationshipField,
378
+ 378 targetField: targetFieldName,
379
+ 379 joinCondition: lastJoin.joinCondition,
380
+ 380 isOneToMany: lastJoin.isOneToMany,
381
+ 381 isPolymorphic: lastJoin.isPolymorphic,
382
+ 382 targetScopeName: lastJoin.targetScopeName || finalScope
383
+ 383 }
384
+ 384 }
385
+ 385 }
386
+ 386
387
+ 387 /**
388
+ 388 * Analyzes a search schema to identify which fields need database indexes
389
+ 389 *
390
+ 390 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
391
+ 391 * @param {Object} log - Logger instance for debug and trace output
392
+ 392 * @param {string} scopeName - The scope being analyzed
393
+ 393 * @param {Object} searchSchema - Search schema definition with filter fields
394
+ 394 * @returns {Array<Object>} Array of required indexes with scope, field, and reason
395
+ 395 *
396
+ 396 * @example
397
+ 397 * // Input: Search schema with cross-table references
398
+ 398 * const searchSchema = {
399
+ 399 * authorName: {
400
+ 400 * type: 'string',
401
+ 401 * actualField: 'authors.name',
402
+ 402 * filterOperator: 'like'
403
+ 403 * },
404
+ 404 * companyName: {
405
+ 405 * type: 'string',
406
+ 406 * actualField: 'authors.company.name'
407
+ 407 * },
408
+ 408 * search: {
409
+ 409 * type: 'string',
410
+ 410 * oneOf: ['title', 'authors.name', 'authors.company.name']
411
+ 411 * }
412
+ 412 * };
413
+ 413 *
414
+ 414 * const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
415
+ 415 *
416
+ 416 * // Output: List of fields that need indexes
417
+ 417 * // [
418
+ 418 * // { scope: 'authors', field: 'name', reason: 'Cross-table search from articles.authorName' },
419
+ 419 * // { scope: 'authors', field: 'company', reason: 'Cross-table search from articles.companyName' },
420
+ 420 * // { scope: 'authors', field: 'name', reason: 'Cross-table oneOf search from articles.search' },
421
+ 421 * // { scope: 'authors', field: 'company', reason: 'Cross-table oneOf search from articles.search' }
422
+ 422 * // ]
423
+ 423 *
424
+ 424 * @example
425
+ 425 * // Input: No cross-table references
426
+ 426 * const searchSchema = {
427
+ 427 * title: { type: 'string' },
428
+ 428 * status: { type: 'string' }
429
+ 429 * };
430
+ 430 *
431
+ 431 * const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
432
+ 432 *
433
+ 433 * // Output: Empty array - no cross-table indexes needed
434
+ 434 * // []
435
+ 435 *
436
+ 436 * @description
437
+ 437 * Used by:
438
+ 438 * - Called during API initialization to identify missing indexes
439
+ 439 * - Used by developers to understand performance requirements
440
+ 440 *
441
+ 441 * Purpose:
442
+ 442 * - Cross-table searches require indexes for acceptable performance
443
+ 443 * - Helps identify configuration issues before they cause slow queries
444
+ 444 * - Provides clear documentation of index requirements
445
+ 445 *
446
+ 446 * Data flow:
447
+ 447 * - Runs during schema compilation phase
448
+ 448 * - Analyzes search schemas to find cross-table references
449
+ 449 * - Output can be used to create indexes manually or automatically
450
+ 450 * - Prevents performance issues before queries are executed
451
+ 451 */
452
+ 452 export const analyzeRequiredIndexes = (scopes, log, scopeName, schemaInfo) => {
453
+ 453 const requiredIndexes = []
454
+ 454
455
+ 455 const schemaToAnalyze = schemaInfo.searchSchemaStructure
456
+ 456
457
+ 457 Object.entries(schemaToAnalyze).forEach(([filterKey, fieldDef]) => {
458
+ 458 if (fieldDef.actualField && fieldDef.actualField.includes('.')) {
459
+ 459 const [targetScopeName, targetFieldName] = fieldDef.actualField.split('.')
460
+ 460 requiredIndexes.push({
461
+ 461 scope: targetScopeName,
462
+ 462 field: targetFieldName,
463
+ 463 reason: `Cross-table search from ${scopeName}.${filterKey}`
464
+ 464 })
465
+ 465 }
466
+ 466
467
+ 467 if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
468
+ 468 fieldDef.oneOf.forEach(field => {
469
+ 469 if (field.includes('.')) {
470
+ 470 const [targetScopeName, targetFieldName] = field.split('.')
471
+ 471 requiredIndexes.push({
472
+ 472 scope: targetScopeName,
473
+ 473 field: targetFieldName,
474
+ 474 reason: `Cross-table oneOf search from ${scopeName}.${filterKey}`
475
+ 475 })
476
+ 476 }
477
+ 477 })
478
+ 478 }
479
+ 479 })
480
+ 480
481
+ 481 return requiredIndexes
482
+ 482 }
483
+ 483
484
+ 484 /**
485
+ 485 * Creates database indexes for fields identified by analyzeRequiredIndexes
486
+ 486 *
487
+ 487 * @async
488
+ 488 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
489
+ 489 * @param {Object} log - Logger instance for debug and trace output
490
+ 490 * @param {Array<Object>} requiredIndexes - Index requirements from analyzeRequiredIndexes
491
+ 491 * @param {Object} knex - Knex database connection instance
492
+ 492 * @returns {Promise<Array<Object>>} Array of successfully created indexes
493
+ 493 *
494
+ 494 * @example
495
+ 495 * // Input: Required indexes from analysis
496
+ 496 * const requiredIndexes = [
497
+ 497 * { scope: 'authors', field: 'name', reason: 'Cross-table search' },
498
+ 498 * { scope: 'companies', field: 'name', reason: 'Cross-table search' }
499
+ 499 * ];
500
+ 500 *
501
+ 501 * const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
502
+ 502 *
503
+ 503 * // Output: Successfully created indexes
504
+ 504 * // [
505
+ 505 * // { tableName: 'authors', field: 'name', indexName: 'idx_authors_name_search' },
506
+ 506 * // { tableName: 'companies', field: 'name', indexName: 'idx_companies_name_search' }
507
+ 507 * // ]
508
+ 508 *
509
+ 509 * // Database effect: CREATE INDEX idx_authors_name_search ON authors(name);
510
+ 510 * // Database effect: CREATE INDEX idx_companies_name_search ON companies(name);
511
+ 511 *
512
+ 512 * @example
513
+ 513 * // Input: Some indexes already exist
514
+ 514 * const requiredIndexes = [
515
+ 515 * { scope: 'authors', field: 'name', reason: 'Cross-table search' }, // Already exists
516
+ 516 * { scope: 'authors', field: 'email', reason: 'Cross-table search' } // New
517
+ 517 * ];
518
+ 518 *
519
+ 519 * const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
520
+ 520 *
521
+ 521 * // Output: Only newly created indexes
522
+ 522 * // [
523
+ 523 * // { tableName: 'authors', field: 'email', indexName: 'idx_authors_email_search' }
524
+ 524 * // ]
525
+ 525 *
526
+ 526 * @description
527
+ 527 * Used by:
528
+ 528 * - Can be called during database migration or setup
529
+ 529 * - Used by developers to automatically create performance indexes
530
+ 530 *
531
+ 531 * Purpose:
532
+ 532 * - Automates index creation based on search schema requirements
533
+ 533 * - Ensures consistent index naming across the application
534
+ 534 * - Safely handles cases where indexes already exist
535
+ 535 *
536
+ 536 * Data flow:
537
+ 537 * - Typically runs during database setup or migration
538
+ 538 * - Creates indexes identified by analyzeRequiredIndexes
539
+ 539 * - Improves query performance for cross-table searches
540
+ 540 * - Part of the database optimization phase
541
+ 541 */
542
+ 542 export const createRequiredIndexes = async (scopes, log, requiredIndexes, knex) => {
543
+ 543 const createdIndexes = []
544
+ 544
545
+ 545 for (const indexInfo of requiredIndexes) {
546
+ 546 const { scope, field } = indexInfo
547
+ 547 const tableName = scopes[scope].vars.schemaInfo.tableName
548
+ 548 const indexName = `idx_${tableName}_${field}_search`
549
+ 549
550
+ 550 try {
551
+ 551 const hasIndex = await knex.schema.hasIndex(tableName, [field])
552
+ 552 if (!hasIndex) {
553
+ 553 await knex.schema.table(tableName, table => {
554
+ 554 table.index([field], indexName)
555
+ 555 })
556
+ 556 createdIndexes.push({ tableName, field, indexName })
557
+ 557 log.info(`Created index: ${indexName} on ${tableName}.${field}`)
558
+ 558 }
559
+ 559 } catch (error) {
560
+ 560 log.warn(`Failed to create index on ${tableName}.${field}:`, error.message)
561
+ 561 }
562
+ 562 }
563
+ 563
564
+ 564 return createdIndexes
565
+ 565 }