json-rest-api 1.0.28 → 2.0.0

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