json-rest-api 1.0.29 → 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 (292) 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 +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} +74 -30
  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 -177
  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 +48 -317
  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 +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -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 +179 -194
  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 +100 -290
  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 +93 -38
  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 +128 -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 -2485
  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 -1077
  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 -395
  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 -148
  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 +36 -68
  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 +37 -81
  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 -1247
  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 -148
  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 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  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-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,70 +1,26 @@
1
+ import { createCanonicalDataHelpers } from './lib/storage/canonical-data-helpers.js'
2
+ import { createCanonicalLinkStore } from './lib/anyapi/canonical-link-store.js'
3
+ import { createCanonicalRelationshipReader } from './lib/anyapi/canonical-relationship-reader.js'
4
+ import { createKnexTransaction } from '../../lib/knex-transaction.js'
5
+ import { createEnhancedLogger } from '../../lib/enhanced-logger.js'
1
6
  import { ensureAnyApiSchema } from './lib/anyapi/schema-utils.js'
2
7
  import { AnyapiRegistry } from './lib/anyapi/anyapi-registry.js'
3
- import { RestApiValidationError } from '../../lib/rest-api-errors.js'
4
- import { createSchema } from 'json-rest-schema'
5
- import {
6
- DEFAULT_QUERY_LIMIT,
7
- DEFAULT_MAX_QUERY_LIMIT,
8
- COMPUTED_DEPENDENCIES_KEY,
9
- } from './lib/querying-writing/knex-constants.js'
10
- import {
11
- calculatePaginationMeta,
12
- generatePaginationLinks,
13
- generateCursorPaginationLinks,
14
- buildCursorMeta,
15
- parseCursor,
16
- } from './lib/querying/knex-pagination-helpers.js'
17
- import { getUrlPrefix, buildResourceUrl, buildRelationshipUrl } from './lib/querying/url-helpers.js'
18
- import {
19
- normalizeId,
20
- resolveFieldInfo,
21
- normalizeFilterValues,
22
- ensureFilterableField,
23
- } from './lib/anyapi/utils/descriptor-helpers.js'
24
- import {
25
- AnyapiQueryAdapter,
26
- preloadRelatedDescriptors,
27
- } from './lib/anyapi/query/anyapi-query-adapter.js'
28
- import {
29
- ensureSearchFieldsAreIndexed,
30
- generateSearchSchemaFromSchema,
31
- } from './lib/querying-writing/schema-helpers.js'
32
- import { applyFieldSelectionToQuery, buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
33
- import {
34
- polymorphicFiltersHook,
35
- crossTableFiltersHook,
36
- basicFiltersHook,
37
- resolveSearchOperator,
38
- applyWhereForOperator,
39
- } from './lib/querying/knex-query-helpers.js'
40
- import {
41
- createStorageAdapter,
42
- } from './lib/storage/storage-adapter.js'
43
- import {
44
- translateCanonicalAttributesForStorage,
45
- getCanonicalResourceId,
46
- getCanonicalResourceIdColumn,
47
- translateCanonicalRecordFromStorage,
48
- } from './lib/storage/canonical-storage-mapping.js'
49
- import {
50
- applyCursorPredicate,
51
- validateCursorValues,
52
- applyQueryFieldOrder,
53
- buildEffectiveSortList,
54
- parseSortEntry
55
- } from './lib/querying/query-field-sort-helpers.js'
56
- import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
57
- import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
58
- import { normalizeValueForDatabaseStorage } from './lib/querying-writing/database-value-normalizers.js'
8
+ import { RestApiResourceError } from '../../lib/rest-api-errors.js'
9
+ import { compileSchemas } from './lib/querying-writing/compile-schemas.js'
10
+ import { snapshotResourceConfiguration } from './lib/querying-writing/schema-helpers.js'
11
+ import { assertFieldNameMap } from './lib/querying-writing/field-utils.js'
12
+ import { polymorphicFiltersHook, crossTableFiltersHook, basicFiltersHook } from './lib/querying/knex-query-helpers.js'
13
+ import { createStorageAdapterLookup } from './lib/storage/storage-adapter.js'
14
+ import { getDatabaseCapabilities } from './lib/querying-writing/database-capabilities.js'
59
15
 
60
16
  const DEFAULT_TENANT = 'default'
61
- const LINKS_TABLE = 'any_links'
62
17
 
63
18
  export const RestApiAnyapiKnexPlugin = {
64
19
  name: 'rest-api-anyapi-knex',
65
20
  dependencies: ['rest-api'],
66
21
 
67
- async install ({ helpers, vars, pluginOptions, api, log, addHook, addScopeMethod, scopes }) {
22
+ async install ({ helpers, pluginOptions, api, log, addHook, addResourceMethod, scopes }) {
23
+ log = createEnhancedLogger(log)
68
24
  const options = pluginOptions || {}
69
25
  const knex = options.knex
70
26
  const tenantId = options.tenantId || DEFAULT_TENANT
@@ -83,1984 +39,75 @@ export const RestApiAnyapiKnexPlugin = {
83
39
  api.knex = {
84
40
  instance: knex,
85
41
  helpers: {},
42
+ capabilities: await getDatabaseCapabilities(knex, log),
86
43
  }
87
44
 
88
- const storageAdapterCache = new Map()
89
-
90
- const getScopeStorageAdapter = (scopeName) => {
91
- if (!scopeName) return null
92
- const resource = api.resources?.[scopeName] || scopes?.[scopeName]
93
- const schemaInfo = resource?.vars?.schemaInfo
94
- if (!schemaInfo) return null
95
-
96
- const cached = storageAdapterCache.get(scopeName)
97
- if (cached && cached.schemaInfo === schemaInfo) {
98
- return cached.adapter
99
- }
100
-
101
- const adapter = createStorageAdapter({ knex, schemaInfo })
102
- storageAdapterCache.set(scopeName, { adapter, schemaInfo })
103
- if (resource?.vars) {
104
- resource.vars.storageAdapter = adapter
105
- }
106
- return adapter
107
- }
108
-
109
- api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
110
- helpers.getStorageAdapter = getScopeStorageAdapter
111
-
112
- const scopeOptionsRegistry = new Map()
113
-
114
- helpers.newTransaction = async () => knex.transaction()
115
-
116
- addHook('release', 'anyapi-knex-release', {}, async ({ api }) => {
117
- if (api.knex?.instance) {
118
- await api.knex.instance.destroy()
119
- }
120
- })
121
-
122
- const getDescriptor = async (scopeName) => {
123
- const descriptor = await registry.getDescriptor(tenantId, scopeName)
124
- if (!descriptor) {
125
- throw new Error(`Descriptor not found for resource '${scopeName}'`)
126
- }
127
- const scope = api.resources?.[scopeName] || scopes?.[scopeName]
128
- const stored = scopeOptionsRegistry.get(scopeName) || {}
129
- descriptor.idProperty = stored.idProperty ||
130
- scope?.vars?.schemaInfo?.idProperty ||
131
- scope?.scopeOptions?.idProperty ||
132
- descriptor.idProperty ||
133
- vars.idProperty ||
134
- 'id'
135
- return descriptor
136
- }
137
-
138
- const decorateResourceLinks = ({ resource, scope, scopeName, context }) => {
139
- if (!resource || !scope || !scopeName || !context) return
140
-
141
- resource.links = resource.links || {}
142
- resource.links.self = buildResourceUrl(context, scope, scopeName, resource.id)
143
-
144
- if (!resource.relationships || typeof resource.relationships !== 'object') {
145
- return
146
- }
147
-
148
- for (const [relationshipName, relationshipObject] of Object.entries(resource.relationships)) {
149
- if (!relationshipObject || typeof relationshipObject !== 'object') {
150
- continue
151
- }
152
-
153
- relationshipObject.links = {
154
- self: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, true),
155
- related: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, false),
156
- }
157
- }
158
- }
159
-
160
- const getAllowedQueryFieldNames = (scopeName) => (
161
- Object.keys(api.resources?.[scopeName]?.vars?.queryFields || {})
162
- )
163
-
164
- const getLogicalResourceIdColumn = (descriptor) => getCanonicalResourceIdColumn(descriptor)
165
-
166
- const getLogicalResourceId = (row, descriptor) => getCanonicalResourceId(row, descriptor)
167
-
168
- const applyFiltersToQuery = ({ query, filters, descriptor, searchSchema, adapter }) => {
169
- if (!filters || Object.keys(filters).length === 0) return
170
-
171
- const normalizeValue = (field, value) => {
172
- if (!adapter?.translateFilterValue) {
173
- return value
174
- }
175
- if (Array.isArray(value)) {
176
- return value.map((entry) => adapter.translateFilterValue(field, entry))
177
- }
178
- return adapter.translateFilterValue(field, value)
179
- }
180
-
181
- for (const [filterKey, rawValue] of Object.entries(filters)) {
182
- const fieldDef = searchSchema?.[filterKey]
183
-
184
- if (fieldDef?.polymorphicField) {
185
- continue
186
- }
187
-
188
- if (fieldDef?.actualField?.includes('.')) {
189
- continue
190
- }
191
-
192
- if (fieldDef?.oneOf?.some((field) => field.includes('.'))) {
193
- continue
194
- }
195
-
196
- if (fieldDef?.oneOf && Array.isArray(fieldDef.oneOf)) {
197
- continue
198
- }
199
-
200
- if (fieldDef?.applyFilter && typeof fieldDef.applyFilter === 'function') {
201
- continue
202
- }
203
-
204
- const actualField = fieldDef?.actualField || filterKey
205
- const fieldInfo = ensureFilterableField(descriptor, filterKey)
206
- const columnRef = adapter ? adapter.translateColumn(actualField) : fieldInfo.column
207
- const operator = resolveSearchOperator(fieldDef || fieldInfo.definition)
208
- const values = normalizeFilterValues(rawValue, fieldInfo.definition, {
209
- isRelationship: fieldInfo.isRelationship,
210
- })
211
- const normalizedValues = values.map((value) => normalizeValue(actualField, value))
212
- const nonNullValues = normalizedValues.filter((value) => value !== null)
213
- const hasNull = normalizedValues.length !== nonNullValues.length
214
-
215
- if (nonNullValues.length === 0 && hasNull) {
216
- query.whereNull(columnRef)
217
- continue
218
- }
219
- if (nonNullValues.length === 1 && !hasNull) {
220
- applyWhereForOperator({
221
- builder: query,
222
- columnRef,
223
- operator,
224
- value: nonNullValues[0],
225
- knex,
226
- })
227
- continue
228
- }
229
- if (nonNullValues.length > 1 && !hasNull) {
230
- query.whereIn(columnRef, nonNullValues)
231
- continue
232
- }
233
- if (nonNullValues.length > 0 && hasNull) {
234
- query.where(function anyApiNullFilterGroup () {
235
- this.whereIn(columnRef, nonNullValues).orWhereNull(columnRef)
236
- })
237
- }
238
- }
239
- }
240
-
241
- const applySortingToQuery = ({ query, sort, descriptor, scope, adapter, before = false, queryFieldRuntimeByField = new Map() }) => {
242
- const effectiveSort = buildEffectiveSortList(sort, {
243
- defaultSort: scope?.vars?.defaultSort,
244
- idField: 'id'
245
- })
246
- const descriptors = []
247
-
248
- for (const entry of effectiveSort) {
249
- const { field, direction } = parseSortEntry(entry)
250
- const queryDirection = before ? (direction === 'asc' ? 'desc' : 'asc') : direction
251
- const queryFieldRuntime = queryFieldRuntimeByField.get(field)
252
- if (queryFieldRuntime) {
253
- applyQueryFieldOrder(query, queryFieldRuntime, queryDirection.toUpperCase(), before ? 'first' : 'last')
254
- descriptors.push({
255
- field,
256
- direction,
257
- definition: queryFieldRuntime.definition,
258
- isRelationship: false,
259
- queryFieldRuntime,
260
- })
261
- continue
262
- }
263
-
264
- const fieldInfo = resolveFieldInfo(descriptor, field)
265
- if (!fieldInfo?.column) continue
266
- query.orderByRaw(`?? IS NULL ${before ? 'DESC' : 'ASC'}`, [adapter.translateColumn(fieldInfo.column)])
267
- query.orderBy(fieldInfo.column, queryDirection)
268
- descriptors.push({
269
- field,
270
- column: fieldInfo.column,
271
- direction,
272
- definition: fieldInfo.definition || null,
273
- isRelationship: fieldInfo.isRelationship || false,
274
- })
275
- }
276
-
277
- if (descriptors.length === 0) {
278
- descriptors.push({
279
- field: 'id',
280
- column: 'id',
281
- direction: 'asc',
282
- definition: { type: 'id' },
283
- isRelationship: false,
284
- })
285
- }
286
-
287
- return descriptors
288
- }
289
-
290
- const applyPaginationToQuery = ({
291
- query,
292
- scope,
293
- queryParams,
294
- sortDescriptors,
295
- adapter,
296
- }) => {
297
- const pageParams = queryParams?.page || {}
298
- const scopeVars = scope?.vars || {}
299
- const defaultLimit = scopeVars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
300
- const maxLimit = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
301
-
302
- const descriptors = (sortDescriptors && sortDescriptors.length > 0)
303
- ? sortDescriptors
304
- : [{
305
- field: 'id',
306
- column: 'id',
307
- direction: 'asc',
308
- definition: { type: 'id' },
309
- isRelationship: false,
310
- }]
311
-
312
- const hasCursorParam = pageParams.after !== undefined || pageParams.before !== undefined
313
- const hasPageSize = pageParams.size !== undefined
314
- const hasPageNumber = pageParams.number !== undefined
315
-
316
- const parseCursorOrThrow = (rawCursor, paramName) => {
317
- try {
318
- return parseCursor(rawCursor)
319
- } catch (error) {
320
- throw new RestApiValidationError(
321
- `Invalid cursor format in ${paramName} parameter`,
322
- {
323
- fields: [`page.${paramName}`],
324
- violations: [{
325
- field: `page.${paramName}`,
326
- rule: 'invalid_cursor',
327
- message: error.message,
328
- }],
329
- }
330
- )
331
- }
332
- }
333
-
334
- const applyRawCursorPredicate = (cursorValues, operatorSelector) => {
335
- applyCursorPredicate(
336
- query,
337
- descriptors,
338
- cursorValues,
339
- operatorSelector,
340
- (builder, descriptor, operator, value) => builder.where(
341
- descriptor.column,
342
- operator,
343
- normalizeValueForDatabaseStorage(
344
- value, descriptor.isRelationship ? null : descriptor.definition?.type,
345
- { temporalPrecision: descriptor.definition?.temporalPrecision }
346
- )
347
- )
348
- )
349
- }
350
-
351
- const ensurePageSize = () => {
352
- const requestedSize = Number(pageParams.size ?? defaultLimit)
353
- if (!Number.isFinite(requestedSize) || requestedSize <= 0) {
354
- throw new RestApiValidationError('Page size must be greater than 0', {
355
- fields: ['page.size'],
356
- violations: [{
357
- field: 'page.size',
358
- rule: 'min_value',
359
- message: 'Page size must be a positive number',
360
- }],
361
- })
362
- }
363
- return Math.min(Math.trunc(requestedSize), maxLimit)
364
- }
365
-
366
- if (hasCursorParam || (hasPageSize && !hasPageNumber)) {
367
- if (pageParams.after && pageParams.before) {
368
- throw new RestApiValidationError(
369
- 'page[after] and page[before] cannot be used together',
370
- {
371
- fields: ['page.after', 'page.before'],
372
- violations: [{
373
- field: 'page.after',
374
- rule: 'conflict',
375
- message: 'Provide either page[after] or page[before], not both',
376
- }],
377
- }
378
- )
379
- }
380
-
381
- const pageSize = ensurePageSize()
382
- query.limit(pageSize + 1)
383
-
384
- if (pageParams.after) {
385
- const cursorMap = parseCursorOrThrow(pageParams.after, 'after')
386
- const cursorValues = validateCursorValues(descriptors, cursorMap, 'after')
387
- applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'))
388
- } else if (pageParams.before) {
389
- const cursorMap = parseCursorOrThrow(pageParams.before, 'before')
390
- const cursorValues = validateCursorValues(descriptors, cursorMap, 'before')
391
- applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'))
392
- }
393
-
394
- return {
395
- mode: 'cursor',
396
- pageSize,
397
- sortDescriptors: descriptors,
398
- }
399
- }
400
-
401
- if (hasPageSize || hasPageNumber) {
402
- const pageSize = ensurePageSize()
403
- const pageNumber = Math.trunc(Number(pageParams.number ?? 1))
404
- const safePageNumber = Number.isFinite(pageNumber) && pageNumber > 0 ? pageNumber : 1
405
- const offset = (safePageNumber - 1) * pageSize
406
-
407
- query.limit(pageSize).offset(offset)
408
-
409
- return {
410
- mode: 'offset',
411
- page: safePageNumber,
412
- pageSize,
413
- sortDescriptors: descriptors,
414
- }
415
- }
416
-
417
- query.limit(Math.min(defaultLimit, maxLimit))
418
- return {
419
- mode: 'default',
420
- page: 1,
421
- pageSize: Math.min(defaultLimit, maxLimit),
422
- sortDescriptors: descriptors,
423
- }
424
- }
425
-
426
- const canonicalizeLinkPair = ({
427
- tenantId,
428
- relationshipKey,
429
- inverseRelationshipKey,
430
- leftResource,
431
- leftId,
432
- rightResource,
433
- rightId,
434
- }) => {
435
- const normalizedLeftId = normalizeId(leftId)
436
- const normalizedRightId = normalizeId(rightId)
437
-
438
- if (normalizedLeftId == null || normalizedRightId == null) {
439
- return null
440
- }
441
-
442
- if (inverseRelationshipKey && inverseRelationshipKey < relationshipKey) {
443
- return {
444
- tenant_id: tenantId,
445
- relationship: inverseRelationshipKey,
446
- inverse_relationship: relationshipKey,
447
- left_resource: rightResource,
448
- left_id: normalizedRightId,
449
- right_resource: leftResource,
450
- right_id: normalizedLeftId,
451
- }
452
- }
453
-
454
- return {
455
- tenant_id: tenantId,
456
- relationship: relationshipKey,
457
- inverse_relationship: inverseRelationshipKey || null,
458
- left_resource: leftResource,
459
- left_id: normalizedLeftId,
460
- right_resource: rightResource,
461
- right_id: normalizedRightId,
462
- }
463
- }
464
-
465
- const buildLinkIdentity = (canonicalRow) => ({
466
- tenant_id: canonicalRow.tenant_id,
467
- relationship: canonicalRow.relationship,
468
- left_resource: canonicalRow.left_resource,
469
- left_id: canonicalRow.left_id,
470
- right_resource: canonicalRow.right_resource,
471
- right_id: canonicalRow.right_id,
472
- })
473
-
474
- const findInverseManyToMany = async ({ descriptor, relInfo }) => {
475
- if (!relInfo?.target) return null
476
-
477
- let targetDescriptor
478
- try {
479
- targetDescriptor = await getDescriptor(relInfo.target)
480
- } catch (error) {
481
- return null
482
- }
483
-
484
- const entries = Object.entries(targetDescriptor.manyToMany || {})
485
- for (const [candidateName, candidateInfo] of entries) {
486
- const candidateTarget = candidateInfo.target || descriptor.resource
487
- if (candidateTarget !== descriptor.resource) continue
488
-
489
- if (relInfo.through && candidateInfo.through && relInfo.through !== candidateInfo.through) {
490
- continue
491
- }
492
-
493
- if (
494
- relInfo.foreignKey && relInfo.otherKey &&
495
- candidateInfo.foreignKey && candidateInfo.otherKey
496
- ) {
497
- const foreignMatches = relInfo.foreignKey === candidateInfo.otherKey
498
- const otherMatches = relInfo.otherKey === candidateInfo.foreignKey
499
- if (!foreignMatches || !otherMatches) continue
500
- }
501
-
502
- const relationshipKey = candidateInfo.relationship ||
503
- `${targetDescriptor.tenant}:${targetDescriptor.resource}:${candidateName}`
504
-
505
- return {
506
- descriptor: targetDescriptor,
507
- relName: candidateName,
508
- relInfo: candidateInfo,
509
- relationshipKey,
510
- }
511
- }
512
-
513
- return null
514
- }
515
-
516
- const getManyToManyInfo = async (scopeName, relName) => {
517
- const descriptor = await getDescriptor(scopeName)
518
- const relInfo = descriptor.manyToMany?.[relName]
519
- if (!relInfo) return null
520
- const relationshipKey = relInfo.relationship || `${descriptor.tenant}:${descriptor.resource}:${relName}`
521
- const inverse = await findInverseManyToMany({ descriptor, relInfo })
522
- const inverseRelationshipKey = inverse?.relationshipKey || null
523
- return { descriptor, relInfo, relationshipKey, inverseRelationshipKey }
524
- }
525
-
526
- const loadLinkRowsForResource = async ({ descriptor, relationshipKey, resourceId, db }) => {
527
- const ownerId = normalizeId(resourceId)
528
- if (ownerId == null) return new Map()
529
-
530
- const rows = await db(LINKS_TABLE)
531
- .where('tenant_id', descriptor.tenant)
532
- .andWhere((builder) => {
533
- builder
534
- .where((q) => {
535
- q.where('relationship', relationshipKey)
536
- .andWhere('left_resource', descriptor.resource)
537
- .andWhere('left_id', ownerId)
538
- })
539
- .orWhere((q) => {
540
- q.where('inverse_relationship', relationshipKey)
541
- .andWhere('right_resource', descriptor.resource)
542
- .andWhere('right_id', ownerId)
543
- })
544
- })
545
- .select('id', 'relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id')
546
-
547
- const map = new Map()
548
-
549
- for (const row of rows) {
550
- let relatedResource
551
- let relatedId
552
- let otherKey = null
553
-
554
- if (row.relationship === relationshipKey && row.left_resource === descriptor.resource) {
555
- relatedResource = row.right_resource
556
- relatedId = normalizeId(row.right_id)
557
- otherKey = row.inverse_relationship || null
558
- } else if (row.inverse_relationship === relationshipKey && row.right_resource === descriptor.resource) {
559
- relatedResource = row.left_resource
560
- relatedId = normalizeId(row.left_id)
561
- otherKey = row.relationship || null
562
- } else {
563
- continue
564
- }
565
-
566
- if (relatedId == null) continue
567
-
568
- const canonical = canonicalizeLinkPair({
569
- tenantId: descriptor.tenant,
570
- relationshipKey,
571
- inverseRelationshipKey: otherKey,
572
- leftResource: descriptor.resource,
573
- leftId: ownerId,
574
- rightResource: relatedResource,
575
- rightId: relatedId,
576
- })
577
-
578
- if (!canonical) continue
579
-
580
- map.set(relatedId, {
581
- rowId: row.id,
582
- relatedResource,
583
- canonical,
584
- otherKey,
585
- })
586
- }
587
-
588
- return map
589
- }
590
-
591
- const fetchLinksForParents = async ({ descriptor, relationshipKey, parentIds, db }) => {
592
- const normalizedIds = parentIds
593
- .map((id) => normalizeId(id))
594
- .filter((id) => id !== null)
595
-
596
- if (normalizedIds.length === 0) return []
597
-
598
- const rows = await db(LINKS_TABLE)
599
- .where('tenant_id', descriptor.tenant)
600
- .andWhere((builder) => {
601
- builder
602
- .where((q) => {
603
- q.where('relationship', relationshipKey)
604
- .andWhere('left_resource', descriptor.resource)
605
- .whereIn('left_id', normalizedIds)
606
- })
607
- .orWhere((q) => {
608
- q.where('inverse_relationship', relationshipKey)
609
- .andWhere('right_resource', descriptor.resource)
610
- .whereIn('right_id', normalizedIds)
611
- })
612
- })
613
- .select('relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id')
614
-
615
- const results = []
616
-
617
- for (const row of rows) {
618
- let parentId
619
- let childId
620
- let childType
621
-
622
- if (row.relationship === relationshipKey && row.left_resource === descriptor.resource) {
623
- parentId = normalizeId(row.left_id)
624
- childId = normalizeId(row.right_id)
625
- childType = row.right_resource
626
- } else if (row.inverse_relationship === relationshipKey && row.right_resource === descriptor.resource) {
627
- parentId = normalizeId(row.right_id)
628
- childId = normalizeId(row.left_id)
629
- childType = row.left_resource
630
- } else {
631
- continue
632
- }
633
-
634
- if (parentId == null || childId == null) continue
635
-
636
- results.push({ parentId, childId, childType })
637
- }
638
-
639
- return results
640
- }
641
-
642
- const applyTargetScopeFilters = async ({
643
- query,
644
- resourceName,
645
- tableName,
646
- context,
647
- filters,
648
- queryPurpose = 'include'
649
- }) => {
650
- const targetScope = api.resources?.[resourceName]
651
- if (!targetScope?.applyQueryFilters) {
652
- return { query }
653
- }
654
-
655
- const storageAdapter = getScopeStorageAdapter(resourceName)
656
- const queryState = await targetScope.applyQueryFilters({
657
- query,
658
- filters,
659
- schemaInfo: targetScope.vars?.schemaInfo,
660
- scopeName: resourceName,
661
- tableName,
662
- db: context.db || context.transaction || api.knex.instance,
663
- isAnyApi: true,
664
- queryPurpose,
665
- storageAdapter
666
- }, {
667
- ...context,
668
- scopeName: resourceName,
669
- schemaInfo: targetScope.vars?.schemaInfo,
670
- storageAdapter
671
- })
672
-
673
- return {
674
- query: unwrapQueryBuilderState(queryState, query)
675
- }
676
- }
677
-
678
- const filterVisibleIdentifiers = async ({ identifiers, context }) => {
679
- if (!identifiers || identifiers.length === 0) return []
680
-
681
- const db = context.db || context.transaction || api.knex.instance
682
- const identifiersByType = new Map()
683
-
684
- for (const identifier of identifiers) {
685
- if (!identifier?.type || identifier.id === undefined || identifier.id === null) continue
686
- if (!identifiersByType.has(identifier.type)) {
687
- identifiersByType.set(identifier.type, new Set())
688
- }
689
- identifiersByType.get(identifier.type).add(normalizeId(identifier.id))
690
- }
691
-
692
- const visibleKeys = new Set()
693
-
694
- for (const [resourceName, idSet] of identifiersByType.entries()) {
695
- const descriptor = await getDescriptor(resourceName)
696
- const idColumn = getLogicalResourceIdColumn(descriptor)
697
- let query = db(descriptor.canonical.tableName)
698
- .where(descriptor.canonical.tenantColumn, descriptor.tenant)
699
- .where(descriptor.canonical.resourceColumn, descriptor.resource)
700
- .whereIn(idColumn, [...idSet])
701
- .select(idColumn)
702
-
703
- const scopedQueryState = await applyTargetScopeFilters({
704
- query,
705
- resourceName,
706
- tableName: descriptor.canonical.tableName,
707
- context,
708
- queryPurpose: 'relationship-identifiers'
709
- })
710
- query = scopedQueryState.query
711
-
712
- const rows = await query
713
- for (const row of rows) {
714
- visibleKeys.add(`${resourceName}:${String(getLogicalResourceId(row, descriptor))}`)
715
- }
716
- }
717
-
718
- return identifiers.filter((identifier) => (
719
- visibleKeys.has(`${identifier.type}:${String(identifier.id)}`)
720
- ))
721
- }
722
-
723
- const ensureTargetsExist = async ({ relInfo, relData, context }) => {
724
- const relArray = Array.isArray(relData) ? relData : []
725
- if (relArray.length === 0) return
726
- const targetScope = api.resources[relInfo.target]
727
- if (!targetScope) {
728
- throw new Error(`Target resource '${relInfo.target}' not found`)
729
- }
730
- for (const identifier of relArray) {
731
- if (identifier?.id === undefined || identifier.id === null) {
732
- throw new Error('Relationship data requires resource identifier with id')
733
- }
734
- if (identifier.type && identifier.type !== relInfo.target) {
735
- throw new Error(`Relationship expects type '${relInfo.target}' but received '${identifier.type}'`)
736
- }
737
- await targetScope.get({
738
- id: identifier.id,
739
- transaction: context.transaction,
740
- simplified: false
741
- }, { ...context })
742
- }
743
- }
744
-
745
- const attachLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
746
- const relArray = Array.isArray(relData) ? relData : []
747
- if (relArray.length === 0) return
748
-
749
- for (const identifier of relArray) {
750
- const rightId = normalizeId(identifier.id)
751
- if (rightId == null) continue
752
-
753
- const canonical = canonicalizeLinkPair({
754
- tenantId: descriptor.tenant,
755
- relationshipKey,
756
- inverseRelationshipKey,
757
- leftResource: descriptor.resource,
758
- leftId,
759
- rightResource: relInfo.target,
760
- rightId,
761
- })
762
-
763
- if (!canonical) continue
764
-
765
- const identity = buildLinkIdentity(canonical)
766
-
767
- const existing = await db(LINKS_TABLE)
768
- .where(identity)
769
- .first()
770
-
771
- if (existing) {
772
- if (!existing.inverse_relationship && canonical.inverse_relationship) {
773
- await db(LINKS_TABLE)
774
- .where({ id: existing.id })
775
- .update({
776
- inverse_relationship: canonical.inverse_relationship,
777
- updated_at: db.fn.now(),
778
- })
779
- }
780
- continue
781
- }
782
-
783
- await db(LINKS_TABLE).insert({
784
- ...canonical,
785
- payload: null,
786
- created_at: db.fn.now(),
787
- updated_at: db.fn.now(),
788
- })
789
- }
790
- }
791
-
792
- api.anyapi.links = {
793
- attachMany: async ({ context, scopeName, relName, relDef, relData }) => {
794
- const info = await getManyToManyInfo(scopeName, relName)
795
- if (!info) {
796
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
797
- }
798
- const db = context.transaction || context.db || api.knex.instance
799
- const leftId = normalizeId(context.id)
800
- await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
801
- await attachLinks({
802
- descriptor: info.descriptor,
803
- relInfo: info.relInfo,
804
- relationshipKey: info.relationshipKey,
805
- inverseRelationshipKey: info.inverseRelationshipKey,
806
- leftId,
807
- relData,
808
- db,
809
- })
810
- },
811
- syncMany: async ({ context, scopeName, relName, relDef, relData, isUpdate }) => {
812
- const info = await getManyToManyInfo(scopeName, relName)
813
- if (!info) {
814
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
815
- }
816
- const db = context.transaction || context.db || api.knex.instance
817
- const leftId = normalizeId(context.id)
818
- await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
819
- if (isUpdate) {
820
- await syncLinks({
821
- descriptor: info.descriptor,
822
- relInfo: info.relInfo,
823
- relationshipKey: info.relationshipKey,
824
- inverseRelationshipKey: info.inverseRelationshipKey,
825
- leftId,
826
- relData,
827
- db,
828
- })
829
- } else {
830
- await attachLinks({
831
- descriptor: info.descriptor,
832
- relInfo: info.relInfo,
833
- relationshipKey: info.relationshipKey,
834
- inverseRelationshipKey: info.inverseRelationshipKey,
835
- leftId,
836
- relData,
837
- db,
838
- })
839
- }
840
- },
841
- removeMany: async ({ context, scopeName, relName, relData }) => {
842
- const info = await getManyToManyInfo(scopeName, relName)
843
- if (!info) {
844
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
845
- }
846
- const db = context.transaction || context.db || api.knex.instance
847
- const leftId = normalizeId(context.id)
848
- await removeLinks({
849
- descriptor: info.descriptor,
850
- relInfo: info.relInfo,
851
- relationshipKey: info.relationshipKey,
852
- inverseRelationshipKey: info.inverseRelationshipKey,
853
- leftId,
854
- relData,
855
- db,
856
- })
857
- },
858
- listMany: async ({ context, scopeName, relName }) => {
859
- const info = await getManyToManyInfo(scopeName, relName)
860
- if (!info) {
861
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
862
- }
863
- const db = context.transaction || context.db || api.knex.instance
864
- const leftId = normalizeId(context.id)
865
- const identifiers = await listLinks({
866
- descriptor: info.descriptor,
867
- relInfo: info.relInfo,
868
- relationshipKey: info.relationshipKey,
869
- inverseRelationshipKey: info.inverseRelationshipKey,
870
- leftId,
871
- db,
872
- })
873
- return filterVisibleIdentifiers({
874
- identifiers,
875
- context
876
- })
877
- },
878
- fetchManyToManyRows: async ({ scopeName, relName, parentIds, context }) => {
879
- const info = await getManyToManyInfo(scopeName, relName)
880
- if (!info) return []
881
- const db = context.db || context.transaction || api.knex.instance
882
- return fetchLinksForParents({
883
- descriptor: info.descriptor,
884
- relationshipKey: info.relationshipKey,
885
- parentIds,
886
- db,
887
- })
888
- },
889
- }
890
-
891
- const syncLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
892
- const relArray = Array.isArray(relData) ? relData : []
893
- const existingMap = await loadLinkRowsForResource({
894
- descriptor,
895
- relationshipKey,
896
- resourceId: leftId,
897
- db,
898
- })
899
-
900
- const desired = new Set(
901
- relArray
902
- .map((identifier) => normalizeId(identifier.id))
903
- .filter((id) => id !== null)
904
- )
905
-
906
- const existingIds = new Set(existingMap.keys())
907
-
908
- const toAdd = [...desired].filter((id) => !existingIds.has(id))
909
- const toRemove = [...existingIds].filter((id) => !desired.has(id))
910
-
911
- if (toAdd.length > 0) {
912
- const data = toAdd.map((id) => ({ id }))
913
- await attachLinks({
914
- descriptor,
915
- relInfo,
916
- relationshipKey,
917
- inverseRelationshipKey,
918
- leftId,
919
- relData: data,
920
- db,
921
- })
922
- }
923
-
924
- if (toRemove.length > 0) {
925
- for (const id of toRemove) {
926
- const entry = existingMap.get(id)
927
- if (!entry?.canonical) continue
928
- await db(LINKS_TABLE)
929
- .where(buildLinkIdentity(entry.canonical))
930
- .delete()
931
- }
932
- }
933
- }
934
-
935
- const removeLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
936
- const relArray = Array.isArray(relData) ? relData : []
937
- if (relArray.length === 0) return
938
-
939
- const existingMap = await loadLinkRowsForResource({
940
- descriptor,
941
- relationshipKey,
942
- resourceId: leftId,
943
- db,
944
- })
945
-
946
- for (const identifier of relArray) {
947
- const relatedId = normalizeId(identifier.id)
948
- if (relatedId == null) continue
949
-
950
- const entry = existingMap.get(relatedId)
951
- if (entry?.canonical) {
952
- await db(LINKS_TABLE)
953
- .where(buildLinkIdentity(entry.canonical))
954
- .delete()
955
- continue
956
- }
957
-
958
- const canonical = canonicalizeLinkPair({
959
- tenantId: descriptor.tenant,
960
- relationshipKey,
961
- inverseRelationshipKey,
962
- leftResource: descriptor.resource,
963
- leftId,
964
- rightResource: relInfo.target,
965
- rightId: relatedId,
966
- })
967
-
968
- if (!canonical) continue
969
-
970
- await db(LINKS_TABLE)
971
- .where(buildLinkIdentity(canonical))
972
- .delete()
973
- }
974
- }
975
-
976
- const listLinks = async ({ descriptor, relInfo, relationshipKey, leftId, db }) => {
977
- const existingMap = await loadLinkRowsForResource({
978
- descriptor,
979
- relationshipKey,
980
- resourceId: leftId,
981
- db,
982
- })
983
-
984
- return [...existingMap.entries()].map(([relatedId, entry]) => ({
985
- type: entry.relatedResource || relInfo.target,
986
- id: relatedId,
987
- }))
988
- }
989
-
990
- helpers.dataExists = async ({ scopeName, context }) => {
991
- const storageAdapter = getScopeStorageAdapter(scopeName)
992
- if (storageAdapter) {
993
- context.storageAdapter = storageAdapter
994
- }
995
- const descriptor = await getDescriptor(scopeName)
996
- const { canonical } = descriptor
997
- const id = context.id
998
- const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
999
-
1000
- const row = await context.db(canonical.tableName)
1001
- .select('id')
1002
- .where(canonical.tenantColumn, descriptor.tenant)
1003
- .where(canonical.resourceColumn, descriptor.resource)
1004
- .where(logicalIdColumn, id)
1005
- .first()
1006
-
1007
- return !!row
1008
- }
1009
-
1010
- helpers.dataPost = async ({ scopeName, context }) => {
1011
- const storageAdapter = getScopeStorageAdapter(scopeName)
1012
- if (storageAdapter) {
1013
- context.storageAdapter = storageAdapter
1014
- }
1015
- const descriptor = await getDescriptor(scopeName)
1016
- const { canonical } = descriptor
1017
- const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1018
- const explicitId = context.inputRecord?.data?.id
1019
- const attributes = context.inputRecord?.data?.attributes || {}
1020
- const row = storageAdapter?.toStorageRow
1021
- ? storageAdapter.toStorageRow(attributes)
1022
- : translateCanonicalAttributesForStorage(attributes, descriptor)
1023
-
1024
- if (explicitId !== undefined && explicitId !== null) {
1025
- row[logicalIdColumn] = explicitId
1026
- }
1027
-
1028
- row[canonical.tenantColumn] = descriptor.tenant
1029
- row[canonical.resourceColumn] = descriptor.resource
1030
-
1031
- const result = await context.db(canonical.tableName)
1032
- .insert(row)
1033
- .returning('id')
1034
-
1035
- const inserted = Array.isArray(result) ? result[0] : result
1036
- const insertedRowId = inserted && typeof inserted === 'object' && 'id' in inserted
1037
- ? inserted.id
1038
- : inserted
1039
-
1040
- if (explicitId !== undefined && explicitId !== null) {
1041
- return explicitId
1042
- }
1043
-
1044
- if (insertedRowId === undefined || insertedRowId === null) {
1045
- return insertedRowId
1046
- }
1047
-
1048
- const generatedLogicalId = String(insertedRowId)
1049
-
1050
- await context.db(canonical.tableName)
1051
- .where({ id: insertedRowId })
1052
- .update({ [logicalIdColumn]: generatedLogicalId })
1053
-
1054
- return generatedLogicalId
1055
- }
1056
-
1057
- helpers.dataPut = async ({ scopeName, context }) => {
1058
- const storageAdapter = getScopeStorageAdapter(scopeName)
1059
- if (storageAdapter) {
1060
- context.storageAdapter = storageAdapter
1061
- }
1062
- const descriptor = await getDescriptor(scopeName)
1063
- const { canonical } = descriptor
1064
- const id = context.id
1065
- const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1066
- const attributes = context.inputRecord?.data?.attributes || {}
1067
- const row = storageAdapter?.toStorageRow
1068
- ? storageAdapter.toStorageRow(attributes)
1069
- : translateCanonicalAttributesForStorage(attributes, descriptor)
1070
-
1071
- row[canonical.resourceColumn] = descriptor.resource
1072
- row[canonical.tenantColumn] = descriptor.tenant
1073
-
1074
- const writeRow = Object.fromEntries(
1075
- Object.entries(row).filter(([, value]) => value !== undefined)
1076
- )
1077
-
1078
- if (context.isCreate) {
1079
- await context.db(canonical.tableName).insert({
1080
- ...writeRow,
1081
- [logicalIdColumn]: id,
1082
- })
1083
- return 1
1084
- }
1085
-
1086
- if (Object.keys(writeRow).length === 0) {
1087
- return 0
1088
- }
1089
-
1090
- const result = await context.db(canonical.tableName)
1091
- .where(logicalIdColumn, id)
1092
- .where(canonical.resourceColumn, descriptor.resource)
1093
- .where(canonical.tenantColumn, descriptor.tenant)
1094
- .update(writeRow)
1095
-
1096
- return result
1097
- }
1098
-
1099
- helpers.dataPatch = async ({ scopeName, context }) => {
1100
- const storageAdapter = getScopeStorageAdapter(scopeName)
1101
- if (storageAdapter) {
1102
- context.storageAdapter = storageAdapter
1103
- }
1104
- const descriptor = await getDescriptor(scopeName)
1105
- const { canonical } = descriptor
1106
- const id = context.id
1107
- const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1108
- const attributes = context.inputRecord?.data?.attributes || {}
1109
- const row = storageAdapter?.toStorageRow
1110
- ? storageAdapter.toStorageRow(attributes)
1111
- : translateCanonicalAttributesForStorage(attributes, descriptor)
1112
-
1113
- const updateRow = Object.fromEntries(
1114
- Object.entries(row).filter(([, value]) => value !== undefined)
1115
- )
1116
-
1117
- if (Object.keys(updateRow).length === 0) {
1118
- return 0
1119
- }
1120
-
1121
- const result = await context.db(canonical.tableName)
1122
- .where(logicalIdColumn, id)
1123
- .where(canonical.resourceColumn, descriptor.resource)
1124
- .where(canonical.tenantColumn, descriptor.tenant)
1125
- .update(updateRow)
1126
-
1127
- return result
1128
- }
1129
-
1130
- helpers.dataDelete = async ({ scopeName, context }) => {
1131
- const storageAdapter = getScopeStorageAdapter(scopeName)
1132
- if (storageAdapter) {
1133
- context.storageAdapter = storageAdapter
1134
- }
1135
- const descriptor = await getDescriptor(scopeName)
1136
- const { canonical } = descriptor
1137
- const id = context.id
1138
- const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1139
-
1140
- const result = await context.db(canonical.tableName)
1141
- .where(logicalIdColumn, id)
1142
- .where(canonical.resourceColumn, descriptor.resource)
1143
- .where(canonical.tenantColumn, descriptor.tenant)
1144
- .delete()
1145
-
1146
- return result
1147
- }
1148
-
1149
- helpers.dataGetMinimal = async ({
1150
- scopeName,
1151
- context,
1152
- runHooks,
1153
- applyQueryFilters,
1154
- filters = context.queryParams?.filters,
1155
- queryPurpose = 'single'
1156
- }) => {
1157
- const storageAdapter = getScopeStorageAdapter(scopeName)
1158
- if (storageAdapter) {
1159
- context.storageAdapter = storageAdapter
1160
- }
1161
- const descriptor = await getDescriptor(scopeName)
1162
- const { canonical } = descriptor
1163
- const id = context.id
1164
- const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1165
- const scope = api.resources?.[scopeName]
1166
-
1167
- let query = context.db(canonical.tableName)
1168
- .where(logicalIdColumn, id)
1169
- .where(canonical.resourceColumn, descriptor.resource)
1170
- .where(canonical.tenantColumn, descriptor.tenant)
1171
-
1172
- if (typeof applyQueryFilters === 'function') {
1173
- const scopedQueryState = await applyQueryFilters({
1174
- query,
1175
- filters,
1176
- tableName: canonical.tableName,
1177
- db: context.db,
1178
- scopeName,
1179
- queryPurpose,
1180
- storageAdapter,
1181
- isAnyApi: true
1182
- })
1183
- query = unwrapQueryBuilderState(scopedQueryState, query)
1184
- } else if (runHooks) {
1185
- const previousKnexQuery = context.knexQuery
1186
- context.knexQuery = {
1187
- query,
1188
- filters,
1189
- schemaInfo: context.schemaInfo,
1190
- scopeName,
1191
- tableName: canonical.tableName,
1192
- db: context.db,
1193
- queryPurpose,
1194
- isAnyApi: true,
1195
- adapter: storageAdapter,
1196
- storageAdapter,
1197
- }
1198
-
1199
- try {
1200
- await applyBuiltInAnyApiQueryFilters(context)
1201
- await runHooks('knexQueryFiltering')
1202
- query = context.knexQuery?.query || query
1203
- } finally {
1204
- if (previousKnexQuery === undefined) {
1205
- delete context.knexQuery
1206
- } else {
1207
- context.knexQuery = previousKnexQuery
1208
- }
1209
- }
1210
- }
1211
-
1212
- const row = await query.first()
1213
-
1214
- if (!row) return null
1215
- const translated = translateCanonicalRecordFromStorage(
1216
- row,
1217
- descriptor,
1218
- { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1219
- )
1220
- const resourceId = getLogicalResourceId(row, descriptor)
1221
- const minimal = {
1222
- type: descriptor.resource,
1223
- id: resourceId,
1224
- attributes: translated.attributes,
1225
- relationships: translated.relationships,
1226
- }
1227
- if (scope) {
1228
- decorateResourceLinks({ resource: minimal, scope, scopeName, context })
1229
- }
1230
- return minimal
1231
- }
1232
-
1233
- const parseIncludeTree = (includeParam) => {
1234
- const tree = {}
1235
- if (!includeParam) return tree
1236
-
1237
- const pushPath = (raw) => {
1238
- if (!raw) return
1239
- const segments = raw.split('.').map((segment) => segment.trim()).filter(Boolean)
1240
- if (segments.length === 0) return
1241
- let node = tree
1242
- for (const segment of segments) {
1243
- node[segment] = node[segment] || {}
1244
- node = node[segment]
1245
- }
1246
- }
1247
-
1248
- if (Array.isArray(includeParam)) {
1249
- for (const entry of includeParam) {
1250
- String(entry).split(',').forEach((part) => pushPath(part.trim()))
1251
- }
1252
- } else {
1253
- String(includeParam).split(',').forEach((part) => pushPath(part.trim()))
1254
- }
1255
-
1256
- return tree
1257
- }
1258
-
1259
- const applyIncludeFieldSelection = async ({ query, resourceName, tableName, context }) => {
1260
- const targetScope = api.resources?.[resourceName]
1261
- if (!targetScope) {
1262
- return { query, fieldSelectionInfo: null }
1263
- }
1264
-
1265
- const storageAdapter = getScopeStorageAdapter(resourceName)
1266
- const fieldSelectionInfo = await buildFieldSelection(targetScope, {
1267
- context: {
1268
- ...context,
1269
- scopeName: resourceName,
1270
- schemaInfo: targetScope.vars.schemaInfo,
1271
- storageAdapter,
1272
- }
1273
- })
1274
-
1275
- const selectionState = await applyFieldSelectionToQuery({
1276
- query,
1277
- scope: targetScope,
1278
- fieldSelectionInfo,
1279
- tableName,
1280
- useTablePrefix: false,
1281
- storageAdapter,
1282
- db: context.db || context.transaction || api.knex.instance,
1283
- context,
1284
- scopeName: resourceName
1285
- })
1286
-
1287
- return {
1288
- query: selectionState.query,
1289
- fieldSelectionInfo
1290
- }
1291
- }
1292
-
1293
- const buildIncludedResource = ({ row, descriptor, context, fieldSelectionInfo = null }) => {
1294
- const translated = translateCanonicalRecordFromStorage(
1295
- row,
1296
- descriptor,
1297
- { allowedExtraFields: getAllowedQueryFieldNames(descriptor.resource) }
1298
- )
1299
- const resourceId = getLogicalResourceId(row, descriptor)
1300
- const includeResource = {
1301
- type: descriptor.resource,
1302
- id: resourceId,
1303
- attributes: translated.attributes,
1304
- }
1305
-
1306
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1307
- includeResource.relationships = translated.relationships
1308
- }
1309
-
1310
- if (fieldSelectionInfo?.computedDependencies?.length) {
1311
- includeResource[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
1312
- }
1313
-
1314
- const targetScope = api.resources?.[descriptor.resource]
1315
- if (targetScope) {
1316
- decorateResourceLinks({
1317
- resource: includeResource,
1318
- scope: targetScope,
1319
- scopeName: descriptor.resource,
1320
- context,
1321
- })
1322
- }
1323
-
1324
- return includeResource
1325
- }
1326
-
1327
- const collectIncludes = async ({ descriptor, resources, includeTree, context, includes, seen }) => {
1328
- const entries = Object.entries(includeTree || {})
1329
- if (entries.length === 0 || !resources || resources.length === 0) return
1330
-
1331
- const db = context.db || context.transaction || api.knex.instance
1332
-
1333
- for (const [relName, childTree] of entries) {
1334
- const childKeys = Object.keys(childTree || {})
1335
-
1336
- const belongsToInfo = descriptor.belongsTo?.[relName]
1337
- if (belongsToInfo) {
1338
- const ids = [...new Set(resources
1339
- .map((resource) => resource.relationships?.[relName]?.data?.id)
1340
- .filter((id) => id !== undefined && id !== null))]
1341
-
1342
- if (ids.length === 0) continue
1343
-
1344
- const targetDescriptor = await registry.getDescriptor(tenantId, belongsToInfo.target)
1345
- if (!targetDescriptor) continue
1346
-
1347
- let query = db(targetDescriptor.canonical.tableName)
1348
- .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1349
- .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1350
- .whereIn(getLogicalResourceIdColumn(targetDescriptor), ids)
1351
- const selectionState = await applyIncludeFieldSelection({
1352
- query,
1353
- resourceName: targetDescriptor.resource,
1354
- tableName: targetDescriptor.canonical.tableName,
1355
- context
1356
- })
1357
- query = selectionState.query
1358
- const scopedQueryState = await applyTargetScopeFilters({
1359
- query,
1360
- resourceName: targetDescriptor.resource,
1361
- tableName: targetDescriptor.canonical.tableName,
1362
- context
1363
- })
1364
- query = scopedQueryState.query
1365
- const rows = await query
1366
-
1367
- const newResources = []
1368
- for (const row of rows) {
1369
- const includeId = getLogicalResourceId(row, targetDescriptor)
1370
- const includeKey = `${targetDescriptor.resource}:${includeId}`
1371
- if (seen.has(includeKey)) continue
1372
- seen.add(includeKey)
1373
- const includeResource = buildIncludedResource({
1374
- row,
1375
- descriptor: targetDescriptor,
1376
- context,
1377
- fieldSelectionInfo: selectionState.fieldSelectionInfo
1378
- })
1379
- includes.push(includeResource)
1380
- newResources.push(includeResource)
1381
- }
1382
-
1383
- if (childKeys.length > 0 && newResources.length > 0) {
1384
- await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1385
- await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1386
- await collectIncludes({
1387
- descriptor: targetDescriptor,
1388
- resources: newResources,
1389
- includeTree: childTree,
1390
- context,
1391
- includes,
1392
- seen,
1393
- })
1394
- }
1395
- continue
1396
- }
1397
-
1398
- const polymorphicInfo = descriptor.polymorphicBelongsTo?.[relName]
1399
- if (polymorphicInfo) {
1400
- const typeToIds = new Map()
1401
- for (const resource of resources) {
1402
- const relData = resource.relationships?.[relName]?.data
1403
- if (!relData?.type || relData?.id == null) continue
1404
- const targetType = String(relData.type)
1405
- const targetId = normalizeId(relData.id)
1406
- if (targetId == null) continue
1407
- if (!typeToIds.has(targetType)) {
1408
- typeToIds.set(targetType, new Set())
1409
- }
1410
- typeToIds.get(targetType).add(targetId)
1411
- }
1412
-
1413
- const newResources = []
1414
- for (const [targetType, idSet] of typeToIds.entries()) {
1415
- if (idSet.size === 0) continue
1416
- let targetDescriptor
1417
- try {
1418
- targetDescriptor = await getDescriptor(targetType)
1419
- } catch (error) {
1420
- continue
1421
- }
1422
-
1423
- let query = db(targetDescriptor.canonical.tableName)
1424
- .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1425
- .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1426
- .whereIn(getLogicalResourceIdColumn(targetDescriptor), [...idSet])
1427
- const selectionState = await applyIncludeFieldSelection({
1428
- query,
1429
- resourceName: targetDescriptor.resource,
1430
- tableName: targetDescriptor.canonical.tableName,
1431
- context
1432
- })
1433
- query = selectionState.query
1434
- const scopedQueryState = await applyTargetScopeFilters({
1435
- query,
1436
- resourceName: targetDescriptor.resource,
1437
- tableName: targetDescriptor.canonical.tableName,
1438
- context
1439
- })
1440
- query = scopedQueryState.query
1441
- const rows = await query
1442
-
1443
- for (const row of rows) {
1444
- const includeId = getLogicalResourceId(row, targetDescriptor)
1445
- const includeKey = `${targetDescriptor.resource}:${includeId}`
1446
- if (seen.has(includeKey)) continue
1447
- seen.add(includeKey)
1448
- const includeResource = buildIncludedResource({
1449
- row,
1450
- descriptor: targetDescriptor,
1451
- context,
1452
- fieldSelectionInfo: selectionState.fieldSelectionInfo
1453
- })
1454
- includes.push(includeResource)
1455
- newResources.push(includeResource)
1456
- }
1457
- }
1458
-
1459
- if (childKeys.length > 0 && newResources.length > 0) {
1460
- const resourcesByType = new Map()
1461
- for (const includeResource of newResources) {
1462
- if (!resourcesByType.has(includeResource.type)) {
1463
- resourcesByType.set(includeResource.type, [])
1464
- }
1465
- resourcesByType.get(includeResource.type).push(includeResource)
1466
- }
1467
-
1468
- for (const [targetType, groupedResources] of resourcesByType.entries()) {
1469
- let targetDescriptor
1470
- try {
1471
- targetDescriptor = await getDescriptor(targetType)
1472
- } catch (error) {
1473
- continue
1474
- }
1475
-
1476
- await attachHasManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context })
1477
- await attachManyToManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context })
1478
- await collectIncludes({
1479
- descriptor: targetDescriptor,
1480
- resources: groupedResources,
1481
- includeTree: childTree,
1482
- context,
1483
- includes,
1484
- seen,
1485
- })
1486
- }
1487
- }
1488
-
1489
- continue
1490
- }
1491
-
1492
- const hasManyInfo = descriptor.relationships?.[relName]
1493
- if (hasManyInfo?.type === 'hasMany' && hasManyInfo.target && (hasManyInfo.foreignKey || hasManyInfo.via)) {
1494
- let targetDescriptor
1495
- try {
1496
- targetDescriptor = await getDescriptor(hasManyInfo.target)
1497
- } catch (error) {
1498
- continue
1499
- }
1500
-
1501
- const parentIds = resources
1502
- .map((resource) => normalizeId(resource.id))
1503
- .filter((id) => id !== null)
1504
-
1505
- if (parentIds.length === 0) continue
1506
-
1507
- const queryIds = Array.from(new Set([
1508
- ...parentIds,
1509
- ...parentIds
1510
- .map((id) => {
1511
- const numeric = Number(id)
1512
- return Number.isFinite(numeric) ? numeric : null
1513
- })
1514
- .filter((value) => value !== null),
1515
- ]))
1516
-
1517
- let rows = []
1518
- let groupingColumn = null
1519
- let fieldSelectionInfo = null
1520
-
1521
- if (hasManyInfo.foreignKey) {
1522
- const foreignField = targetDescriptor.fields?.[hasManyInfo.foreignKey]
1523
- if (!foreignField?.slot) {
1524
- continue
1525
- }
1526
- groupingColumn = foreignField.slot
1527
- let query = db(targetDescriptor.canonical.tableName)
1528
- .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1529
- .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1530
- .whereIn(groupingColumn, parentIds)
1531
- const selectionState = await applyIncludeFieldSelection({
1532
- query,
1533
- resourceName: targetDescriptor.resource,
1534
- tableName: targetDescriptor.canonical.tableName,
1535
- context
1536
- })
1537
- query = selectionState.query
1538
- const scopedQueryState = await applyTargetScopeFilters({
1539
- query,
1540
- resourceName: targetDescriptor.resource,
1541
- tableName: targetDescriptor.canonical.tableName,
1542
- context
1543
- })
1544
- query = scopedQueryState.query
1545
- fieldSelectionInfo = selectionState.fieldSelectionInfo
1546
- rows = await query
1547
- } else if (hasManyInfo.via) {
1548
- const polyInfo = targetDescriptor.polymorphicBelongsTo?.[hasManyInfo.via]
1549
- if (!polyInfo?.idColumn || !polyInfo?.typeColumn) {
1550
- continue
1551
- }
1552
- groupingColumn = polyInfo.idColumn
1553
- let query = db(targetDescriptor.canonical.tableName)
1554
- .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1555
- .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1556
- .where(polyInfo.typeColumn, descriptor.resource)
1557
- .whereIn(groupingColumn, queryIds)
1558
- const selectionState = await applyIncludeFieldSelection({
1559
- query,
1560
- resourceName: targetDescriptor.resource,
1561
- tableName: targetDescriptor.canonical.tableName,
1562
- context
1563
- })
1564
- query = selectionState.query
1565
- const scopedQueryState = await applyTargetScopeFilters({
1566
- query,
1567
- resourceName: targetDescriptor.resource,
1568
- tableName: targetDescriptor.canonical.tableName,
1569
- context
1570
- })
1571
- query = scopedQueryState.query
1572
- fieldSelectionInfo = selectionState.fieldSelectionInfo
1573
- rows = await query
1574
- }
1575
-
1576
- if (!groupingColumn || rows.length === 0) {
1577
- continue
1578
- }
1579
-
1580
- const newResources = []
1581
- for (const row of rows) {
1582
- const includeId = getLogicalResourceId(row, targetDescriptor)
1583
- const includeKey = `${targetDescriptor.resource}:${includeId}`
1584
- if (seen.has(includeKey)) continue
1585
- seen.add(includeKey)
1586
- const includeResource = buildIncludedResource({
1587
- row,
1588
- descriptor: targetDescriptor,
1589
- context,
1590
- fieldSelectionInfo
1591
- })
1592
- includes.push(includeResource)
1593
- newResources.push(includeResource)
1594
- }
1595
-
1596
- if (childKeys.length > 0 && newResources.length > 0) {
1597
- await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1598
- await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1599
- await collectIncludes({
1600
- descriptor: targetDescriptor,
1601
- resources: newResources,
1602
- includeTree: childTree,
1603
- context,
1604
- includes,
1605
- seen,
1606
- })
1607
- }
1608
-
1609
- continue
1610
- }
1611
-
1612
- const manyInfo = descriptor.manyToMany?.[relName]
1613
- if (manyInfo) {
1614
- const info = await getManyToManyInfo(descriptor.resource, relName)
1615
- if (!info) continue
1616
- const linkRows = await fetchLinksForParents({
1617
- descriptor: info.descriptor,
1618
- relationshipKey: info.relationshipKey,
1619
- parentIds: resources.map((resource) => resource.id),
1620
- db,
1621
- })
1622
-
1623
- const childIds = [...new Set(linkRows.map((row) => row.childId))]
1624
- if (childIds.length === 0) continue
1625
-
1626
- const targetDescriptor = await registry.getDescriptor(tenantId, info.relInfo.target)
1627
- if (!targetDescriptor) continue
1628
-
1629
- let query = db(targetDescriptor.canonical.tableName)
1630
- .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1631
- .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1632
- .whereIn(getLogicalResourceIdColumn(targetDescriptor), childIds)
1633
- const selectionState = await applyIncludeFieldSelection({
1634
- query,
1635
- resourceName: targetDescriptor.resource,
1636
- tableName: targetDescriptor.canonical.tableName,
1637
- context
1638
- })
1639
- query = selectionState.query
1640
- const scopedQueryState = await applyTargetScopeFilters({
1641
- query,
1642
- resourceName: targetDescriptor.resource,
1643
- tableName: targetDescriptor.canonical.tableName,
1644
- context
1645
- })
1646
- query = scopedQueryState.query
1647
- const rows = await query
1648
-
1649
- const newResources = []
1650
- for (const row of rows) {
1651
- const includeId = getLogicalResourceId(row, targetDescriptor)
1652
- const includeKey = `${targetDescriptor.resource}:${includeId}`
1653
- if (seen.has(includeKey)) continue
1654
- seen.add(includeKey)
1655
- const includeResource = buildIncludedResource({
1656
- row,
1657
- descriptor: targetDescriptor,
1658
- context,
1659
- fieldSelectionInfo: selectionState.fieldSelectionInfo
1660
- })
1661
- includes.push(includeResource)
1662
- newResources.push(includeResource)
1663
- }
1664
-
1665
- if (childKeys.length > 0 && newResources.length > 0) {
1666
- await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1667
- await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1668
- await collectIncludes({
1669
- descriptor: targetDescriptor,
1670
- resources: newResources,
1671
- includeTree: childTree,
1672
- context,
1673
- includes,
1674
- seen,
1675
- })
1676
- }
1677
- }
1678
- }
1679
- }
1680
-
1681
- const buildIncludes = async ({ parentResources, descriptor, context }) => {
1682
- const includeTree = parseIncludeTree(context.queryParams?.include)
1683
- if (Object.keys(includeTree).length === 0) return []
1684
-
1685
- const includes = []
1686
- const seen = new Set()
1687
-
1688
- await collectIncludes({
1689
- descriptor,
1690
- resources: parentResources,
1691
- includeTree,
1692
- context,
1693
- includes,
1694
- seen,
1695
- })
1696
-
1697
- return includes
1698
- }
1699
-
1700
- const attachHasManyRelationships = async ({ resources, descriptor, context }) => {
1701
- if (!resources || resources.length === 0) return
1702
- const hasManyEntries = Object.entries(descriptor.relationships || {})
1703
- .filter(([, relDef]) => relDef?.type === 'hasMany' && relDef.target && (relDef.foreignKey || relDef.via))
1704
-
1705
- if (hasManyEntries.length === 0) return
1706
-
1707
- const db = context.db || context.transaction || api.knex.instance
1708
- const parentIds = resources.map((resource) => resource.id)
1709
- const normalizedParentIds = parentIds
1710
- .map((id) => normalizeId(id))
1711
- .filter((id) => id !== null)
1712
-
1713
- if (normalizedParentIds.length === 0) return
1714
-
1715
- for (const [relName, relDef] of hasManyEntries) {
1716
- let targetDescriptor
1717
- try {
1718
- targetDescriptor = await getDescriptor(relDef.target)
1719
- } catch (error) {
1720
- continue
1721
- }
1722
-
1723
- if (relDef.foreignKey) {
1724
- const foreignField = targetDescriptor.fields?.[relDef.foreignKey]
1725
- if (!foreignField?.slot) continue
1726
- const column = foreignField.slot
1727
-
1728
- let query = db(targetDescriptor.canonical.tableName)
1729
- .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1730
- .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1731
- .whereIn(column, normalizedParentIds)
1732
- const scopedQueryState = await applyTargetScopeFilters({
1733
- query,
1734
- resourceName: targetDescriptor.resource,
1735
- tableName: targetDescriptor.canonical.tableName,
1736
- context,
1737
- queryPurpose: 'relationship-identifiers'
1738
- })
1739
- query = scopedQueryState.query
1740
- const rows = await query
1741
-
1742
- const grouped = rows.reduce((acc, row) => {
1743
- const parentId = row[column]
1744
- if (parentId == null) return acc
1745
- const key = String(parentId)
1746
- acc[key] = acc[key] || []
1747
- acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
1748
- return acc
1749
- }, {})
1750
-
1751
- for (const resource of resources) {
1752
- resource.relationships = resource.relationships || {}
1753
- const related = grouped[String(resource.id)] || []
1754
- resource.relationships[relName] = { data: related }
1755
- }
1756
-
1757
- continue
1758
- }
1759
-
1760
- if (relDef.via) {
1761
- const polyInfo = targetDescriptor.polymorphicBelongsTo?.[relDef.via]
1762
- if (!polyInfo?.idColumn || !polyInfo?.typeColumn) continue
1763
-
1764
- const queryIds = Array.from(new Set([
1765
- ...normalizedParentIds,
1766
- ...normalizedParentIds
1767
- .map((id) => {
1768
- const numeric = Number(id)
1769
- return Number.isFinite(numeric) ? numeric : null
1770
- })
1771
- .filter((value) => value !== null),
1772
- ]))
1773
-
1774
- let query = db(targetDescriptor.canonical.tableName)
1775
- .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1776
- .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1777
- .where(polyInfo.typeColumn, descriptor.resource)
1778
- .whereIn(polyInfo.idColumn, queryIds)
1779
- const scopedQueryState = await applyTargetScopeFilters({
1780
- query,
1781
- resourceName: targetDescriptor.resource,
1782
- tableName: targetDescriptor.canonical.tableName,
1783
- context,
1784
- queryPurpose: 'relationship-identifiers'
1785
- })
1786
- query = scopedQueryState.query
1787
- const rows = await query
1788
-
1789
- const grouped = rows.reduce((acc, row) => {
1790
- const parentId = row[polyInfo.idColumn]
1791
- if (parentId == null) return acc
1792
- const key = String(parentId)
1793
- acc[key] = acc[key] || []
1794
- acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
1795
- return acc
1796
- }, {})
1797
-
1798
- for (const resource of resources) {
1799
- resource.relationships = resource.relationships || {}
1800
- const related = grouped[String(resource.id)] || []
1801
- resource.relationships[relName] = { data: related }
1802
- }
1803
- }
1804
- }
1805
- }
1806
-
1807
- const attachManyToManyRelationships = async ({ resources, descriptor, context }) => {
1808
- if (!resources || resources.length === 0) return
1809
- const manyEntries = Object.entries(descriptor.manyToMany || {})
1810
- if (manyEntries.length === 0) return
1811
-
1812
- const db = context.db || context.transaction || api.knex.instance
1813
- const parentIds = resources.map((resource) => resource.id)
1814
-
1815
- for (const [relName] of manyEntries) {
1816
- const info = await getManyToManyInfo(descriptor.resource, relName)
1817
- if (!info) continue
1818
- let rows = await fetchLinksForParents({
1819
- descriptor: info.descriptor,
1820
- relationshipKey: info.relationshipKey,
1821
- parentIds,
1822
- db,
1823
- })
1824
-
1825
- const visibleIdentifiers = await filterVisibleIdentifiers({
1826
- identifiers: rows.map((row) => ({ type: row.childType, id: row.childId })),
1827
- context
1828
- })
1829
- const visibleKeys = new Set(
1830
- visibleIdentifiers.map((identifier) => `${identifier.type}:${String(identifier.id)}`)
1831
- )
1832
- rows = rows.filter((row) => visibleKeys.has(`${row.childType}:${String(row.childId)}`))
1833
-
1834
- const grouped = rows.reduce((acc, row) => {
1835
- const key = String(row.parentId)
1836
- acc[key] = acc[key] || []
1837
- acc[key].push({ type: row.childType, id: String(row.childId) })
1838
- return acc
1839
- }, {})
1840
-
1841
- for (const resource of resources) {
1842
- const related = grouped[resource.id] || []
1843
- resource.relationships = resource.relationships || {}
1844
- resource.relationships[relName] = { data: related }
1845
- }
1846
- }
1847
- }
1848
-
1849
- helpers.dataGet = async ({ scopeName, context }) => {
1850
- const storageAdapter = getScopeStorageAdapter(scopeName)
1851
- if (storageAdapter) {
1852
- context.storageAdapter = storageAdapter
1853
- }
1854
- const descriptor = await getDescriptor(scopeName)
1855
- const { canonical } = descriptor
1856
- const id = context.id
1857
- const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1858
- const scope = api.resources?.[scopeName]
1859
-
1860
- let fieldSelectionInfo = null
1861
- if (scope) {
1862
- fieldSelectionInfo = await buildFieldSelection(scope, { context })
1863
- context.computedDependencies = fieldSelectionInfo.computedDependencies
1864
- } else {
1865
- context.computedDependencies = []
1866
- }
1867
-
1868
- let query = context.db(canonical.tableName)
1869
- .where(logicalIdColumn, id)
1870
- .where(canonical.resourceColumn, descriptor.resource)
1871
- .where(canonical.tenantColumn, descriptor.tenant)
1872
-
1873
- if (scope && fieldSelectionInfo) {
1874
- const selectionState = await applyFieldSelectionToQuery({
1875
- query,
1876
- scope,
1877
- fieldSelectionInfo,
1878
- tableName: canonical.tableName,
1879
- useTablePrefix: false,
1880
- storageAdapter,
1881
- db: context.db,
1882
- context,
1883
- scopeName
1884
- })
1885
- query = selectionState.query
1886
- }
1887
-
1888
- const row = await query.first()
1889
-
1890
- if (!row) return null
1891
-
1892
- const record = translateCanonicalRecordFromStorage(
1893
- row,
1894
- descriptor,
1895
- { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1896
- )
1897
- const resourceId = getLogicalResourceId(row, descriptor)
1898
- const data = {
1899
- type: descriptor.resource,
1900
- id: resourceId,
1901
- attributes: record.attributes,
1902
- }
1903
-
1904
- if (record.relationships && Object.keys(record.relationships).length > 0) {
1905
- data.relationships = record.relationships
1906
- }
1907
-
1908
- await attachHasManyRelationships({
1909
- resources: [data],
1910
- descriptor,
1911
- context,
1912
- })
45
+ const getScopeStorageAdapter = createStorageAdapterLookup({
46
+ knex,
47
+ getResource: scopeName => api.resources?.[scopeName] || scopes?.[scopeName]
48
+ })
1913
49
 
1914
- await attachManyToManyRelationships({
1915
- resources: [data],
1916
- descriptor,
1917
- context,
1918
- })
50
+ api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
51
+ helpers.getStorageAdapter = getScopeStorageAdapter
1919
52
 
1920
- const included = await buildIncludes({
1921
- parentResources: [data],
1922
- descriptor,
1923
- context,
1924
- })
53
+ const scopeOptionsRegistry = new Map()
1925
54
 
1926
- const response = { data }
1927
- if (included.length > 0) {
1928
- response.included = included
1929
- }
55
+ /** @type {import('../../lib/transaction-types.js').TransactionFactory} */
56
+ helpers.newTransaction = context => createKnexTransaction(knex, context)
1930
57
 
1931
- if (scope) {
1932
- decorateResourceLinks({ resource: data, scope, scopeName, context })
1933
- response.links = { self: data.links.self }
58
+ addHook('release', 'anyapi-knex-release', {}, async ({ api }) => {
59
+ if (api.knex?.instance) {
60
+ await api.knex.instance.destroy()
1934
61
  }
62
+ })
1935
63
 
1936
- if (included.length > 0) {
1937
- for (const includeResource of included) {
1938
- const targetScope = api.resources?.[includeResource.type]
1939
- if (!targetScope) continue
1940
- decorateResourceLinks({
1941
- resource: includeResource,
1942
- scope: targetScope,
1943
- scopeName: includeResource.type,
1944
- context,
1945
- })
1946
- }
64
+ const getDescriptor = (scopeName) => {
65
+ const descriptor = api.resources?.[scopeName]?.vars?.schemaInfo?.descriptor
66
+ if (!descriptor) {
67
+ throw new Error(`Descriptor not found for resource '${scopeName}'`)
1947
68
  }
1948
-
1949
- return response
69
+ return descriptor
1950
70
  }
1951
71
 
1952
- const rehydrateSchemaInfo = async (scopeName) => {
1953
- const scope = api.resources?.[scopeName] || scopes?.[scopeName] || {}
1954
- const storedOptions = scopeOptionsRegistry.get(scopeName) || {}
1955
- const rawSchema = scope.scopeOptions?.schema || storedOptions.schema || {}
1956
-
1957
- if (!scope.scopeOptions) {
1958
- scope.scopeOptions = {
1959
- schema: rawSchema,
1960
- relationships: storedOptions.relationships || {},
1961
- searchSchema: storedOptions.searchSchema || null,
1962
- tableName: storedOptions.tableName || scopeName,
1963
- idProperty: storedOptions.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
1964
- canonicalFieldsMap: storedOptions.canonicalFieldsMap || {},
1965
- }
1966
- }
72
+ const linkStore = createCanonicalLinkStore({ api, getDescriptor, getScopeStorageAdapter })
73
+ api.anyapi.links = linkStore.methods
74
+ helpers.dataRelatedIdsQuery = linkStore.dataRelatedIdsQuery
75
+ const { buildIncludes, attachReverseRelationships, attachManyToManyRelationships } = createCanonicalRelationshipReader({
76
+ api, knex, getDescriptor, getScopeStorageAdapter, linkStore
77
+ })
1967
78
 
79
+ const refreshStorageDescriptor = async (scopeName, compiledSchemaInfo) => {
80
+ const scope = api.resources[scopeName]
81
+ const schemaInfo = compiledSchemaInfo || scope.vars.schemaInfo
1968
82
  const descriptor = await registry.getDescriptor(tenantId, scopeName)
1969
- if (!descriptor) return
1970
-
1971
- const existingInfo = scope.vars?.schemaInfo || {}
1972
-
1973
- const mergedSchema = { ...(existingInfo.schemaInstance?.structure || {}) }
1974
- const computed = { ...(existingInfo.computed || {}) }
1975
- for (const [fieldName, definition] of Object.entries(descriptor.schema || {})) {
1976
- if (definition?.computed) {
1977
- if (!computed[fieldName]) {
1978
- computed[fieldName] = { ...definition }
1979
- }
1980
- continue
1981
- }
1982
- mergedSchema[fieldName] = {
1983
- ...(mergedSchema[fieldName] || {}),
1984
- ...definition,
1985
- }
1986
- }
1987
-
1988
- if (!mergedSchema.id) {
1989
- mergedSchema.id = { type: 'id' }
1990
- }
1991
-
1992
- const schemaInstance = createSchema(mergedSchema)
1993
-
1994
- const rawSearchFields = generateSearchSchemaFromSchema(
1995
- mergedSchema,
1996
- scope.scopeOptions?.searchSchema || storedOptions.searchSchema || null
1997
- )
1998
- if (rawSearchFields) {
1999
- ensureSearchFieldsAreIndexed(rawSearchFields)
2000
- }
2001
- const searchSchemaInstance = createSchema(rawSearchFields || {})
2002
-
2003
- const tableName = storedOptions.tableName ||
2004
- existingInfo.tableName ||
2005
- scope.scopeOptions?.tableName ||
2006
- scopeName
2007
-
2008
- const idProperty = storedOptions.idProperty ||
2009
- existingInfo.idProperty ||
2010
- scope.scopeOptions?.idProperty ||
2011
- scope.vars?.idProperty ||
2012
- vars.idProperty ||
2013
- 'id'
2014
- descriptor.idProperty = idProperty
2015
-
83
+ if (!descriptor) throw new Error(`Descriptor not found for resource '${scopeName}'`)
84
+ descriptor.idProperty = schemaInfo.idProperty
2016
85
  scope.vars.schemaInfo = {
2017
- ...existingInfo,
2018
- schemaInstance,
2019
- schemaStructure: schemaInstance.structure,
2020
- searchSchemaInstance,
2021
- searchSchemaStructure: searchSchemaInstance.structure,
2022
- schemaRelationships: descriptor.relationships ||
2023
- scope.scopeOptions?.relationships ||
2024
- storedOptions.relationships ||
2025
- existingInfo.schemaRelationships ||
2026
- {},
2027
- computed,
2028
- tableName,
2029
- idProperty,
86
+ ...schemaInfo,
2030
87
  descriptor,
2031
- canonicalFieldMap: descriptor.canonicalFieldMap || {},
88
+ canonicalFieldMap: descriptor.canonicalFieldMap,
2032
89
  }
2033
-
2034
90
  getScopeStorageAdapter(scopeName)
2035
-
2036
- scope.scopeOptions = {
2037
- ...(scope.scopeOptions || {}),
2038
- canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
2039
- }
2040
-
2041
91
  scopeOptionsRegistry.set(scopeName, {
2042
- schema: storedOptions.schema || scope.scopeOptions?.schema || {},
2043
- relationships: storedOptions.relationships || scope.scopeOptions?.relationships || {},
2044
- searchSchema: scope.scopeOptions?.searchSchema || storedOptions.searchSchema || null,
2045
- tableName,
2046
- idProperty,
2047
- canonicalFieldsMap: descriptor.canonicalFieldMap || {},
92
+ ...scopeOptionsRegistry.get(scopeName),
93
+ canonicalFieldsMap: descriptor.canonicalFieldMap,
2048
94
  })
2049
95
  }
2050
96
 
2051
- const buildTableNameMaps = () => {
2052
- const resourceToTableName = new Map()
2053
- const tableNameToResource = new Map()
2054
- for (const [resourceName, resourceScope] of Object.entries(api.resources || {})) {
2055
- const tableName = resourceScope?.vars?.schemaInfo?.tableName
2056
- if (!tableName) continue
2057
- resourceToTableName.set(resourceName, tableName)
2058
- tableNameToResource.set(tableName, resourceName)
97
+ const buildSearchMembershipQuery = ({ sourceScopeName, relationshipName, targetScopeName, foreignKey, otherKey, db }) => {
98
+ const info = linkStore.getManyToManyInfo(sourceScopeName, relationshipName)
99
+ if (!info) throw new RestApiResourceError(`Relationship '${sourceScopeName}.${relationshipName}' not found`, { subtype: 'related_type_not_found' })
100
+ const links = linkStore.buildRelatedLinkQuery({ ...info, db, allParents: true })
101
+ const pivotAdapter = getScopeStorageAdapter(targetScopeName)
102
+ return {
103
+ query: db.select({
104
+ [pivotAdapter.translateColumn(foreignKey)]: 'parentId',
105
+ [pivotAdapter.translateColumn(otherKey)]: 'childId'
106
+ }).from(links.as('search_membership'))
2059
107
  }
2060
- return { resourceToTableName, tableNameToResource }
2061
108
  }
2062
109
 
2063
- const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter }
110
+ const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter, buildSearchMembershipQuery }
2064
111
 
2065
112
  const applyBuiltInAnyApiQueryFilters = async (context) => {
2066
113
  const hookParams = { context }
@@ -2069,496 +116,80 @@ export const RestApiAnyapiKnexPlugin = {
2069
116
  await basicFiltersHook(hookParams, queryHookDependencies)
2070
117
  }
2071
118
 
2072
- helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
2073
- const storageAdapter = getScopeStorageAdapter(scopeName)
2074
- if (storageAdapter) {
2075
- context.storageAdapter = storageAdapter
2076
- }
2077
- const descriptor = await getDescriptor(scopeName)
2078
- const db = context.db || context.transaction || api.knex.instance
2079
- const scope = api.resources?.[scopeName]
2080
- const queryParams = context.queryParams || {}
2081
-
2082
- let fieldSelectionInfo = null
2083
- if (scope) {
2084
- fieldSelectionInfo = await buildFieldSelection(scope, { context })
2085
- context.computedDependencies = fieldSelectionInfo.computedDependencies
2086
- } else {
2087
- context.computedDependencies = []
2088
- }
2089
-
2090
- const { resourceToTableName, tableNameToResource } = buildTableNameMaps()
2091
- context.resourceToTableName = resourceToTableName
2092
- const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor })
2093
- const adapter = new AnyapiQueryAdapter({
2094
- descriptor,
2095
- db,
2096
- registry,
2097
- descriptorsMap,
2098
- resourceToTableName,
2099
- tableNameToResource,
2100
- log,
2101
- })
2102
- const queryBuilder = adapter.query
2103
-
2104
- const schemaInfo = scope?.vars?.schemaInfo
2105
- const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias
2106
-
2107
- applyFiltersToQuery({
2108
- query: queryBuilder,
2109
- filters: queryParams.filters,
2110
- descriptor,
2111
- searchSchema: schemaInfo?.searchSchemaStructure,
2112
- adapter,
2113
- })
2114
-
2115
- context.knexQuery = {
2116
- query: queryBuilder,
2117
- filters: queryParams.filters,
2118
- schemaInfo,
2119
- scopeName,
2120
- tableName: tableNameForHooks,
2121
- db,
2122
- queryPurpose: 'collection',
2123
- isAnyApi: true,
2124
- adapter,
2125
- storageAdapter,
2126
- }
2127
-
2128
- await applyBuiltInAnyApiQueryFilters(context)
2129
-
2130
- if (runHooks) {
2131
- await runHooks('knexQueryFiltering')
2132
- }
2133
-
2134
- delete context.knexQuery
2135
-
2136
- const countQuery = queryBuilder.clone()
2137
-
2138
- let selectionState = { query: queryBuilder, queryFieldRuntimeByField: new Map() }
2139
- if (scope && fieldSelectionInfo) {
2140
- selectionState = await applyFieldSelectionToQuery({
2141
- query: queryBuilder,
2142
- scope,
2143
- fieldSelectionInfo,
2144
- tableName: adapter.tableAlias,
2145
- useTablePrefix: false,
2146
- storageAdapter,
2147
- db,
2148
- context,
2149
- scopeName
2150
- })
2151
- }
2152
-
2153
- const sortDescriptors = applySortingToQuery({
2154
- query: selectionState.query,
2155
- sort: queryParams.sort,
2156
- descriptor,
2157
- scope,
2158
- adapter,
2159
- before: Boolean(queryParams.page?.before),
2160
- queryFieldRuntimeByField: selectionState.queryFieldRuntimeByField
2161
- })
2162
-
2163
- const paginationInfo = applyPaginationToQuery({
2164
- query: queryBuilder,
2165
- scope,
2166
- queryParams,
2167
- sortDescriptors,
2168
- adapter,
2169
- })
2170
-
2171
- let rows = await queryBuilder
2172
- let cursorRecords = null
2173
- let hasMore = false
2174
-
2175
- if (paginationInfo.mode === 'cursor') {
2176
- const { pageSize, sortDescriptors: cursorDescriptors } = paginationInfo
2177
- cursorRecords = rows.map((row) => {
2178
- const record = {}
2179
- for (const descriptorEntry of cursorDescriptors) {
2180
- if (descriptorEntry.field === 'id') {
2181
- record.id = getLogicalResourceId(row, descriptor)
2182
- } else if (descriptorEntry.queryFieldRuntime) {
2183
- record[descriptorEntry.field] = row[descriptorEntry.field]
2184
- } else if (descriptorEntry.column) {
2185
- record[descriptorEntry.field] = row[descriptorEntry.column]
2186
- }
2187
- }
2188
- return record
2189
- })
2190
-
2191
- if (rows.length > pageSize) {
2192
- hasMore = true
2193
- rows = rows.slice(0, pageSize)
2194
- cursorRecords = cursorRecords.slice(0, pageSize)
2195
- }
2196
- if (queryParams.page?.before) {
2197
- rows.reverse()
2198
- cursorRecords.reverse()
2199
- }
2200
- }
2201
-
2202
- const data = rows.map((row) => {
2203
- const translated = translateCanonicalRecordFromStorage(
2204
- row,
2205
- descriptor,
2206
- { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
2207
- )
2208
- const resourceId = getLogicalResourceId(row, descriptor)
2209
- const resource = {
2210
- type: descriptor.resource,
2211
- id: resourceId,
2212
- attributes: translated.attributes,
2213
- }
2214
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
2215
- resource.relationships = translated.relationships
2216
- }
2217
- const resourceScope = scope
2218
- if (resourceScope) {
2219
- decorateResourceLinks({ resource, scope: resourceScope, scopeName, context })
2220
- }
2221
- if (fieldSelectionInfo?.computedDependencies?.length) {
2222
- resource.__$jsonrestapi_computed_deps$__ = fieldSelectionInfo.computedDependencies
2223
- }
2224
- return resource
2225
- })
2226
-
2227
- await attachHasManyRelationships({
2228
- resources: data,
2229
- descriptor,
2230
- context,
2231
- })
2232
-
2233
- await attachManyToManyRelationships({
2234
- resources: data,
2235
- descriptor,
2236
- context,
2237
- })
2238
-
2239
- const included = await buildIncludes({
2240
- parentResources: data,
2241
- descriptor,
2242
- context,
2243
- })
2244
-
2245
- const response = { data }
2246
- if (included.length > 0) {
2247
- response.included = included
2248
- }
2249
-
2250
- if (included.length > 0) {
2251
- for (const includeResource of included) {
2252
- const targetScope = api.resources?.[includeResource.type]
2253
- if (!targetScope) continue
2254
- decorateResourceLinks({
2255
- resource: includeResource,
2256
- scope: targetScope,
2257
- scopeName: includeResource.type,
2258
- context,
2259
- })
2260
- }
2261
- }
2262
-
2263
- context.returnMeta = context.returnMeta || {}
2264
- const queryString = serializeJsonApiQuery(queryParams)
2265
- context.returnMeta.queryString = queryString ? `?${queryString}` : ''
2266
- delete context.returnMeta.paginationMeta
2267
- delete context.returnMeta.paginationLinks
2268
-
2269
- if (paginationInfo.mode === 'offset') {
2270
- const { page, pageSize } = paginationInfo
2271
- let paginationMeta
2272
-
2273
- if (scope?.vars?.enablePaginationCounts) {
2274
- const countResult = await countQuery.count({ count: '*' }).first()
2275
- const total = Number(countResult?.count ?? countResult?.total ?? 0)
2276
- paginationMeta = calculatePaginationMeta(total, page, pageSize)
2277
- } else {
2278
- paginationMeta = { page, pageSize }
2279
- }
2280
-
2281
- context.returnMeta.paginationMeta = paginationMeta
2282
- const urlPrefix = getUrlPrefix(context, scope)
2283
- context.returnMeta.paginationLinks = generatePaginationLinks(
2284
- urlPrefix,
2285
- scopeName,
2286
- queryParams,
2287
- paginationMeta
2288
- )
2289
- } else if (paginationInfo.mode === 'cursor' && cursorRecords) {
2290
- const cursorFields = paginationInfo.sortDescriptors.map((descriptorEntry) => descriptorEntry.field)
2291
- const cursorOptions = {
2292
- schemaInfo,
2293
- definitions: Object.fromEntries(paginationInfo.sortDescriptors.map(({ field, definition }) => [field, definition])),
2294
- before: Boolean(queryParams.page?.before)
2295
- }
2296
- const paginationMeta = buildCursorMeta(
2297
- cursorRecords,
2298
- paginationInfo.pageSize,
2299
- hasMore,
2300
- cursorFields,
2301
- cursorOptions
2302
- )
2303
- context.returnMeta.paginationMeta = paginationMeta
2304
- const urlPrefix = getUrlPrefix(context, scope)
2305
- context.returnMeta.paginationLinks = generateCursorPaginationLinks(
2306
- urlPrefix,
2307
- scopeName,
2308
- queryParams,
2309
- cursorRecords,
2310
- paginationInfo.pageSize,
2311
- hasMore,
2312
- cursorFields,
2313
- cursorOptions
2314
- )
2315
- }
2316
-
2317
- if (context.returnMeta.paginationMeta) {
2318
- response.meta = {
2319
- pagination: context.returnMeta.paginationMeta,
2320
- }
2321
- }
2322
-
2323
- if (context.returnMeta.paginationLinks) {
2324
- response.links = context.returnMeta.paginationLinks
2325
- } else if (scope) {
2326
- const urlPrefix = getUrlPrefix(context, scope)
2327
- response.links = {
2328
- self: `${urlPrefix}/${scopeName}${context.returnMeta.queryString || ''}`,
2329
- }
2330
- }
2331
-
2332
- return response
2333
- }
2334
-
2335
- helpers.dataQueryCount = async ({ scopeName, context }) => {
2336
- const storageAdapter = getScopeStorageAdapter(scopeName)
2337
- if (storageAdapter) {
2338
- context.storageAdapter = storageAdapter
2339
- }
2340
- const descriptor = await getDescriptor(scopeName)
2341
- const db = context.db || context.transaction || api.knex.instance
2342
- const { resourceToTableName, tableNameToResource } = buildTableNameMaps()
2343
- const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor })
2344
- const adapter = new AnyapiQueryAdapter({
2345
- descriptor,
2346
- db,
2347
- registry,
2348
- descriptorsMap,
2349
- resourceToTableName,
2350
- tableNameToResource,
2351
- log,
2352
- })
2353
-
2354
- // Apply the same filter hooks as dataQuery for consistent counts
2355
- const scope = api.resources?.[scopeName]
2356
- const schemaInfo = scope?.vars?.schemaInfo
2357
- const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias
2358
-
2359
- const countContext = {
2360
- ...context,
2361
- knexQuery: {
2362
- query: adapter.query,
2363
- filters: context.queryParams?.filters,
2364
- schemaInfo,
2365
- scopeName,
2366
- tableName: tableNameForHooks,
2367
- db,
2368
- queryPurpose: 'count',
2369
- isAnyApi: true,
2370
- adapter,
2371
- storageAdapter,
2372
- }
2373
- }
2374
-
2375
- const hookParams = { context: countContext }
2376
- await polymorphicFiltersHook(hookParams, queryHookDependencies)
2377
- await crossTableFiltersHook(hookParams, queryHookDependencies)
2378
- await basicFiltersHook(hookParams, queryHookDependencies)
2379
- await api.runHooks('knexQueryFiltering', countContext)
2380
-
2381
- const countQuery = countContext.knexQuery?.query || adapter.query
2382
- const [{ count }] = await countQuery.count({ count: '*' })
2383
- return Number(count)
2384
- }
119
+ Object.assign(helpers, createCanonicalDataHelpers({
120
+ api,
121
+ knex,
122
+ getScopeStorageAdapter,
123
+ getDescriptor,
124
+ linkStore,
125
+ buildIncludes,
126
+ attachReverseRelationships,
127
+ attachManyToManyRelationships,
128
+ applyBuiltInAnyApiQueryFilters
129
+ }))
2385
130
 
2386
- addHook('scope:added', 'anyapi-register-resource', { sequence: 50 }, async ({ context }) => {
131
+ addHook('resource:added', 'anyapi-register-resource', {}, async ({ context }) => {
2387
132
  const { scopeName, scopeOptions = {} } = context
2388
- const scope = api.scopes?.[scopeName] || scopes?.[scopeName]
2389
- const stored = scopeOptionsRegistry.get(scopeName) || {}
2390
-
2391
- const baseSchema = scopeOptions.schema || scope?.scopeOptions?.schema || stored.schema || {}
2392
- const relationships = scopeOptions.relationships || scope?.scopeOptions?.relationships || stored.relationships || {}
2393
- const searchSchemaDef = scopeOptions.searchSchema || scope?.scopeOptions?.searchSchema || stored.searchSchema || null
2394
- const tableName = scopeOptions.tableName || scope?.scopeOptions?.tableName || stored.tableName || scopeName
2395
- const idProperty = scopeOptions.idProperty || scope?.scopeOptions?.idProperty || stored.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id'
2396
- const canonicalFieldsMap = scope?.scopeOptions?.canonicalFieldsMap || stored.canonicalFieldsMap || null
2397
-
2398
- const schema = { ...baseSchema }
2399
- if (idProperty && idProperty !== 'id' && !schema[idProperty]) {
2400
- schema[idProperty] = { type: 'number' }
2401
- }
2402
-
2403
- scopeOptionsRegistry.set(scopeName, {
2404
- schema,
2405
- relationships,
2406
- searchSchema: searchSchemaDef,
2407
- tableName,
2408
- idProperty,
2409
- canonicalFieldsMap,
2410
- })
2411
-
133
+ const scope = api.resources[scopeName]
134
+ const { schemaStructure, computed, schemaRelationships, idProperty } = scope.vars.schemaInfo
2412
135
  const descriptor = await registry.registerResource({
2413
136
  tenant: tenantId,
2414
137
  resource: scopeName,
2415
- schema,
2416
- relationships,
2417
- canonicalFieldMap: canonicalFieldsMap,
2418
- idProperty,
138
+ schema: { ...schemaStructure, ...computed },
139
+ relationships: schemaRelationships,
140
+ canonicalFieldMap: scopeOptions.canonicalFieldsMap || null,
141
+ idProperty
2419
142
  })
2420
-
2421
- const updatedStored = scopeOptionsRegistry.get(scopeName) || {}
2422
- updatedStored.canonicalFieldsMap = descriptor.canonicalFieldMap || updatedStored.canonicalFieldsMap || null
2423
- scopeOptionsRegistry.set(scopeName, updatedStored)
2424
-
2425
- if (scope) {
2426
- scope.scopeOptions = {
2427
- ...(scope.scopeOptions || {}),
2428
- schema,
2429
- relationships,
2430
- searchSchema: searchSchemaDef || undefined,
2431
- tableName,
2432
- idProperty,
2433
- canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
2434
- }
2435
- }
2436
-
2437
- await rehydrateSchemaInfo(scopeName)
143
+ scopeOptionsRegistry.set(scopeName, snapshotResourceConfiguration({
144
+ ...scopeOptions,
145
+ idProperty,
146
+ canonicalFieldsMap: descriptor.canonicalFieldMap
147
+ }))
148
+ await refreshStorageDescriptor(scopeName)
2438
149
  })
2439
150
 
2440
- addScopeMethod('createKnexTable', async ({ scopeName, scopeOptions }) => {
2441
- const scope = api.resources?.[scopeName] || scopes?.[scopeName]
2442
- const schemaInput = scopeOptions?.schema || scope?.scopeOptions?.schema || {}
2443
- const relationshipInput = scopeOptions?.relationships || scope?.scopeOptions?.relationships || {}
2444
- const storedBefore = scopeOptionsRegistry.get(scopeName) || {}
2445
- const idProperty = scopeOptions?.idProperty ||
2446
- scope?.scopeOptions?.idProperty ||
2447
- storedBefore.idProperty ||
2448
- scope?.vars?.schemaInfo?.idProperty ||
2449
- vars.idProperty ||
2450
- 'id'
2451
-
2452
- const effectiveSchema = { ...schemaInput }
2453
- if (idProperty && idProperty !== 'id' && !effectiveSchema[idProperty]) {
2454
- effectiveSchema[idProperty] = { type: 'number' }
2455
- }
2456
-
2457
- if (scope) {
2458
- const existingOptions = scope.scopeOptions || {}
2459
- scope.scopeOptions = {
2460
- ...existingOptions,
2461
- schema: {
2462
- ...(existingOptions.schema || {}),
2463
- ...schemaInput,
2464
- ...(idProperty && idProperty !== 'id' && !schemaInput[idProperty] ? { [idProperty]: { type: 'number' } } : {}),
2465
- },
2466
- relationships: {
2467
- ...(existingOptions.relationships || {}),
2468
- ...relationshipInput,
2469
- },
2470
- searchSchema: existingOptions.searchSchema || scopeOptions?.searchSchema,
2471
- }
2472
- }
2473
- scopeOptionsRegistry.set(scopeName, {
2474
- schema: scope?.scopeOptions?.schema || effectiveSchema || storedBefore.schema || {},
2475
- relationships: scope?.scopeOptions?.relationships || relationshipInput || storedBefore.relationships || {},
2476
- searchSchema: scope?.scopeOptions?.searchSchema || scopeOptions?.searchSchema || storedBefore.searchSchema || null,
2477
- tableName: scope?.scopeOptions?.tableName || scopeOptions?.tableName || storedBefore.tableName || scopeName,
2478
- idProperty: scope?.scopeOptions?.idProperty || scopeOptions?.idProperty || storedBefore.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
2479
- canonicalFieldsMap: storedBefore.canonicalFieldsMap || null,
2480
- })
2481
- const descriptor = await registry.registerResource({
2482
- tenant: tenantId,
2483
- resource: scopeName,
2484
- schema: effectiveSchema,
2485
- relationships: relationshipInput,
2486
- canonicalFieldMap: scopeOptions?.canonicalFieldsMap ||
2487
- scope?.scopeOptions?.canonicalFieldsMap ||
2488
- null,
2489
- idProperty,
2490
- })
2491
- if (scope) {
2492
- scope.scopeOptions = {
2493
- ...(scope.scopeOptions || {}),
2494
- canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
2495
- }
2496
- }
2497
- const storedAfter = scopeOptionsRegistry.get(scopeName) || {}
2498
- storedAfter.canonicalFieldsMap = descriptor.canonicalFieldMap || storedAfter.canonicalFieldsMap || null
2499
- scopeOptionsRegistry.set(scopeName, storedAfter)
2500
- await rehydrateSchemaInfo(scopeName)
151
+ addResourceMethod('createKnexTable', async ({ scopeName }) => {
152
+ await ensureAnyApiSchema(knex)
153
+ await refreshStorageDescriptor(scopeName)
2501
154
  })
2502
155
 
2503
- addScopeMethod('addKnexFields', async ({ scopeName, params }) => {
2504
- if (!params?.fields) return
2505
- const scope = api.resources?.[scopeName] || scopes?.[scopeName]
2506
- if (scope) {
2507
- const existingOptions = scope.scopeOptions || {}
2508
- scope.scopeOptions = {
2509
- ...existingOptions,
2510
- schema: {
2511
- ...(existingOptions.schema || {}),
2512
- },
2513
- relationships: existingOptions.relationships || {},
2514
- searchSchema: existingOptions.searchSchema,
2515
- }
156
+ addResourceMethod('addKnexFields', async ({ scopeName, params }) => {
157
+ if (!params?.fields || typeof params.fields !== 'object' || Array.isArray(params.fields)) {
158
+ throw new Error('fields parameter is required for addKnexFields')
2516
159
  }
160
+ assertFieldNameMap(params.fields, `added fields in '${scopeName}'`)
161
+ assertFieldNameMap(params.searchSchema, `added search schema in '${scopeName}'`)
162
+ assertFieldNameMap(params.canonicalFieldsMap, `added canonical fields in '${scopeName}'`)
163
+ const scope = api.resources[scopeName]
2517
164
  const storedBefore = scopeOptionsRegistry.get(scopeName) || {}
2518
- const canonicalEntries = params.canonicalFieldsMap ? { ...params.canonicalFieldsMap } : null
2519
- let latestDescriptor = null
2520
- for (const [fieldName, definition] of Object.entries(params.fields)) {
2521
- if (scope) {
2522
- scope.scopeOptions.schema[fieldName] = definition
2523
- }
2524
- const canonicalEntry = canonicalEntries ? canonicalEntries[fieldName] : null
2525
- if (canonicalEntries) {
2526
- delete canonicalEntries[fieldName]
2527
- }
2528
- latestDescriptor = await registry.allocateField({
2529
- tenant: tenantId,
2530
- resource: scopeName,
2531
- fieldName,
2532
- definition,
2533
- canonicalField: canonicalEntry,
2534
- })
2535
- }
2536
- if (canonicalEntries && Object.keys(canonicalEntries).length > 0) {
2537
- const unknown = Object.keys(canonicalEntries).join(', ')
165
+ const unknown = Object.keys(params.canonicalFieldsMap || {}).filter(field => !Object.hasOwn(params.fields, field))
166
+ if (unknown.length) {
2538
167
  throw new Error(`canonicalFieldsMap contains unknown fields for addKnexFields: ${unknown}`)
2539
168
  }
2540
- if (scope && latestDescriptor?.canonicalFieldMap) {
2541
- scope.scopeOptions.canonicalFieldsMap = latestDescriptor.canonicalFieldMap
2542
- }
2543
- if (scope && params.searchSchema) {
2544
- scope.scopeOptions.searchSchema = {
2545
- ...(scope.scopeOptions.searchSchema || {}),
2546
- ...params.searchSchema,
2547
- }
2548
- }
2549
- const updatedStored = {
2550
- schema: scope?.scopeOptions?.schema || storedBefore.schema || {},
2551
- relationships: scope?.scopeOptions?.relationships || storedBefore.relationships || {},
2552
- searchSchema: scope?.scopeOptions?.searchSchema || storedBefore.searchSchema || null,
2553
- tableName: scope?.scopeOptions?.tableName || storedBefore.tableName || scopeName,
2554
- idProperty: scope?.scopeOptions?.idProperty || storedBefore.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
2555
- canonicalFieldsMap: scope?.scopeOptions?.canonicalFieldsMap || storedBefore.canonicalFieldsMap || null,
2556
- }
169
+ const updatedStored = snapshotResourceConfiguration({
170
+ ...storedBefore,
171
+ schema: { ...storedBefore.schema, ...params.fields },
172
+ searchSchema: { ...storedBefore.searchSchema, ...params.searchSchema }
173
+ })
174
+ const compiled = { scopeOptions: updatedStored, vars: { idProperty: scope.vars.idProperty } }
175
+ await compileSchemas(compiled, { context: { scopeName }, runHooks: (name, context) => api.runHooks(name, context) })
176
+ await api.transaction(async transaction => {
177
+ for (const fieldName of Object.keys(params.fields)) {
178
+ await registry.allocateField({
179
+ tenant: tenantId,
180
+ resource: scopeName,
181
+ fieldName,
182
+ definition: compiled.vars.schemaInfo.schemaStructure[fieldName] || compiled.vars.schemaInfo.computed[fieldName],
183
+ canonicalField: params.canonicalFieldsMap?.[fieldName]
184
+ }, { transaction })
185
+ }
186
+ })
187
+ registry.invalidateDescriptor(tenantId, scopeName)
2557
188
  scopeOptionsRegistry.set(scopeName, updatedStored)
2558
- await rehydrateSchemaInfo(scopeName)
189
+ await refreshStorageDescriptor(scopeName, compiled.vars.schemaInfo)
2559
190
  })
2560
191
 
2561
- addScopeMethod('alterKnexFields', async () => {
192
+ addResourceMethod('alterKnexFields', async () => {
2562
193
  throw new Error('alterKnexFields is not supported by AnyAPI Knex plugin yet')
2563
194
  })
2564
195
  },