json-rest-api 1.0.28 → 2.0.0

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