json-rest-api 1.0.29 → 2.1.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 (294) 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 +744 -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 +733 -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 +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -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 +37 -0
  44. package/index.js +3 -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 +70 -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 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  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 +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. 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
- }