json-rest-api 1.0.22 → 1.0.24

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 (290) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/README.md +22 -1
  4. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  5. package/docs/API.md +196 -38
  6. package/docs/COMPARISON.md +2 -2
  7. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  9. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  10. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  11. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  12. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  13. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  14. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  15. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  16. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +370 -0
  17. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  18. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  19. package/docs/GUIDE/index.md +18 -9
  20. package/docs/QUICKSTART.md +5 -1
  21. package/docs/index.md +1 -1
  22. package/index.js +3 -2
  23. package/lib/enhanced-logger.js +1 -1
  24. package/package.json +6 -2
  25. package/plugins/core/bulk-operations-plugin.js +26 -34
  26. package/plugins/core/connectors/express-plugin.js +97 -190
  27. package/plugins/core/connectors/fastify-plugin.js +206 -0
  28. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  29. package/plugins/core/connectors/lib/connector-core.js +195 -0
  30. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  31. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  32. package/plugins/core/lib/anyapi/schema-utils.js +43 -4
  33. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
  34. package/plugins/core/lib/dbIntrospection.js +1078 -0
  35. package/plugins/core/lib/dbTablesOperations.js +1111 -160
  36. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  37. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
  38. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  39. package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
  40. package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
  41. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  42. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  43. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  44. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  45. package/plugins/core/lib/querying/url-helpers.js +8 -2
  46. package/plugins/core/lib/querying-writing/compile-schemas.js +20 -5
  47. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
  48. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  49. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  50. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  51. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  52. package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
  53. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  54. package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
  55. package/plugins/core/lib/storage/storage-adapter.js +82 -4
  56. package/plugins/core/lib/storage/storage-mapping.js +274 -0
  57. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
  58. package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
  59. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  60. package/plugins/core/rest-api-knex-plugin.js +271 -226
  61. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  62. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  63. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
  64. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  65. package/plugins/core/rest-api-plugin-methods/common.js +148 -92
  66. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
  67. package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
  68. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  69. package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
  70. package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
  71. package/plugins/core/rest-api-plugin-methods/get.js +29 -14
  72. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
  73. package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
  74. package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
  75. package/plugins/core/rest-api-plugin-methods/post.js +21 -8
  76. package/plugins/core/rest-api-plugin-methods/put.js +73 -21
  77. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  78. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  79. package/plugins/core/rest-api-plugin.js +6 -0
  80. package/plugins/core/rest-api-positioning-plugin.js +2 -5
  81. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  82. package/plugins/core/socketio-plugin.js +1 -4
  83. package/plugins/storage/local-storage.js +1 -1
  84. package/plugins/storage/s3-storage.js +5 -2
  85. package/quickTest.js +1 -0
  86. package/scripts/run-docs.js +80 -84
  87. package/test-debug.js +1 -1
  88. package/tests/README.md +7 -0
  89. package/tests/anyapi-basic.test.js +3 -0
  90. package/tests/autofilter.test.js +463 -0
  91. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  92. package/tests/custom-id-simplified-post.test.js +75 -0
  93. package/tests/custom-idproperty-relationships.test.js +33 -0
  94. package/tests/db-introspection.test.js +521 -0
  95. package/tests/db-table-operations.test.js +786 -0
  96. package/tests/fastify-plugin.test.js +156 -0
  97. package/tests/field-getters.test.js +1 -2
  98. package/tests/field-setters.test.js +0 -4
  99. package/tests/fixtures/api-configs.js +278 -176
  100. package/tests/helpers/fake-fastify.js +99 -0
  101. package/tests/http-connectors-parity.test.js +235 -0
  102. package/tests/id-normalization.test.js +444 -0
  103. package/tests/logical-id-write-validation.test.js +151 -0
  104. package/tests/pagination-cursor-multifield.test.js +11 -11
  105. package/tests/projected-fields.test.js +319 -0
  106. package/tests/relationships.test.js +3 -3
  107. package/tests/searchschema-knex-behavior.test.js +187 -0
  108. package/tests/searchschema-merge.test.js +21 -69
  109. package/tests/storage-mapping.test.js +259 -0
  110. package/audit/00-charter.md +0 -103
  111. package/audit/01-scope-map.md +0 -84
  112. package/audit/02-coverage-matrix.csv +0 -174
  113. package/audit/03-invariants.md +0 -123
  114. package/audit/04-standards-rubric.md +0 -56
  115. package/audit/05-competitor-rubric.md +0 -77
  116. package/audit/06-findings.md +0 -160
  117. package/audit/07-risk-register.csv +0 -10
  118. package/audit/08-remediation-roadmap.md +0 -177
  119. package/audit/09-signoff-check.md +0 -23
  120. package/audit/10-final-synthesis.md +0 -29
  121. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  122. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  123. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  157. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  158. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  159. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  160. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  161. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  162. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  163. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  164. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  165. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  166. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  167. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  168. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  169. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  170. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  171. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  172. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  173. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  174. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  175. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  176. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  177. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  178. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  179. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  180. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  181. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  182. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  183. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  184. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  185. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  186. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  187. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  188. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  189. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  190. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  191. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  192. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  193. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  194. package/audit/evidence/code/error-model-signals.txt +0 -98
  195. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  196. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  197. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  198. package/audit/evidence/code/idproperty-signals.txt +0 -157
  199. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  200. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  201. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  202. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  203. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  204. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  205. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  206. package/audit/evidence/code/permission-signals.txt +0 -193
  207. package/audit/evidence/code/query-include-signals.txt +0 -687
  208. package/audit/evidence/code/transaction-signals.txt +0 -28
  209. package/audit/evidence/code/validation-signals.txt +0 -650
  210. package/audit/evidence/code/write-path-signals.txt +0 -61
  211. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  212. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  213. package/audit/evidence/external/corpus-freeze.md +0 -21
  214. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  215. package/audit/evidence/external/npm-download-signals.txt +0 -23
  216. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  217. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  218. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  219. package/audit/evidence/external/readme-fortune.md +0 -165
  220. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  221. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  222. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  223. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  224. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  225. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  226. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  227. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  228. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  229. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  230. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  231. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  232. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  233. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  234. package/audit/evidence/external/standards-sources.txt +0 -6
  235. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  236. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  237. package/audit/evidence/runtime/environment.txt +0 -4
  238. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  239. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  240. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  241. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  242. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  243. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  244. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  245. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  246. package/audit/evidence/runtime/test-summary.md +0 -107
  247. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  248. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  249. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  250. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  251. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  252. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  253. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  254. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  255. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  256. package/audit/evidence/tests/skipped-tests.txt +0 -10
  257. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  258. package/audit/evidence/tests/test-suite-list.txt +0 -32
  259. package/audit/subsystems/docs-drift.md +0 -49
  260. package/audit/subsystems/pass2-cross-review.md +0 -131
  261. package/audit/subsystems/plugins-access.md +0 -51
  262. package/audit/subsystems/plugins-bulk.md +0 -50
  263. package/audit/subsystems/plugins-cors.md +0 -48
  264. package/audit/subsystems/plugins-file.md +0 -53
  265. package/audit/subsystems/plugins-multihome.md +0 -50
  266. package/audit/subsystems/plugins-positioning.md +0 -54
  267. package/audit/subsystems/plugins-socketio.md +0 -50
  268. package/audit/subsystems/query-engine.md +0 -61
  269. package/audit/subsystems/relationships.md +0 -55
  270. package/audit/subsystems/rest-orchestration.md +0 -66
  271. package/audit/subsystems/schema-core.md +0 -64
  272. package/audit/subsystems/storage-knex.md +0 -57
  273. package/audit/subsystems/tests.md +0 -48
  274. package/audit/subsystems/transport-express.md +0 -55
  275. package/audit/traces/trace-read-get.md +0 -22
  276. package/audit/traces/trace-read-query.md +0 -25
  277. package/audit/traces/trace-relationship-endpoints.md +0 -31
  278. package/audit/traces/trace-write-delete.md +0 -17
  279. package/audit/traces/trace-write-patch.md +0 -26
  280. package/audit/traces/trace-write-post.md +0 -22
  281. package/audit/traces/trace-write-put.md +0 -23
  282. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  283. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  284. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  285. package/plugins/core/rest-api-access.js +0 -459
  286. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  287. package/tests/multihome.test.js +0 -553
  288. package/tests/multihome.test.js.bak +0 -550
  289. package/tests/ownership-optout.test.js +0 -76
  290. package/tests/rest-api-access.test.js +0 -206
@@ -9,8 +9,6 @@
9
9
  * - Detecting circular dependencies in field definitions
10
10
  */
11
11
 
12
- import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
13
-
14
12
  /**
15
13
  * Marks all fields in a searchSchema as indexed for database optimization
16
14
  *
@@ -280,22 +280,15 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
280
280
  relationshipsData[relName] = {
281
281
  data: value.map(relId => ({ type: targetType, id: String(relId) }))
282
282
  }
283
- }
284
- // Handle hasOne relationships
285
- else if (relConfig.type === 'hasOne' && value !== undefined) {
283
+ } else if (relConfig.type === 'hasOne' && value !== undefined) {
286
284
  relationshipsData[relName] = {
287
285
  data: value ? { type: relConfig.target, id: String(value) } : null
288
286
  }
289
- }
290
- // Handle single belongsTo relationships if they were also defined in 'relationships'
291
- // (though typically they are defined in schema.belongsTo)
292
- else if (relConfig.belongsTo && value !== undefined) {
287
+ } else if (relConfig.belongsTo && value !== undefined) {
293
288
  relationshipsData[relName] = {
294
289
  data: value ? { type: relConfig.belongsTo, id: String(value) } : null
295
290
  }
296
- }
297
- // Handle polymorphic belongsTo relationships with minimal object format
298
- else if (relConfig.belongsToPolymorphic && value !== undefined) {
291
+ } else if (relConfig.belongsToPolymorphic && value !== undefined) {
299
292
  if (value === null) {
300
293
  relationshipsData[relName] = { data: null }
301
294
  } else if (typeof value === 'object' && value !== null && value.id && value._type) {
@@ -780,7 +773,7 @@ export const transformSingleJsonApiToSimplified = (scope, deps) => {
780
773
  const schemaEntry = Object.entries(schema).find(([_, def]) => def.as === relName)
781
774
 
782
775
  if (schemaEntry) {
783
- const [fieldName, fieldDef] = schemaEntry
776
+ const [, fieldDef] = schemaEntry
784
777
  if (fieldDef.belongsTo && relData.data) {
785
778
  // Create minimal relationship object (no more foreign key field)
786
779
  simplified[relName] = { id: relData.data.id }
@@ -0,0 +1,133 @@
1
+ export const translateCanonicalAttributesForStorage = (attributes = {}, descriptor = {}) => {
2
+ const row = {}
3
+
4
+ for (const [fieldName, value] of Object.entries(attributes)) {
5
+ const slot = descriptor.fields?.[fieldName]
6
+ if (!slot) continue
7
+
8
+ if (slot.slotType === 'belongsTo') {
9
+ row[slot.slot] = value == null ? null : String(value)
10
+ } else {
11
+ row[slot.slot] = value
12
+ }
13
+
14
+ if (slot.slotType === 'belongsTo') {
15
+ const alias = slot.alias || descriptor.fields?.[fieldName]?.alias
16
+ const belongsToInfo = alias ? descriptor.belongsTo?.[alias] : null
17
+ if (belongsToInfo) {
18
+ row[belongsToInfo.typeColumn] = value == null ? null : belongsToInfo.target
19
+ }
20
+ }
21
+ }
22
+
23
+ return row
24
+ }
25
+
26
+ export const getCanonicalResourceIdColumn = (descriptor = {}) => (
27
+ descriptor?.canonical?.logicalIdColumn || 'logical_id'
28
+ )
29
+
30
+ export const getCanonicalResourceId = (row = {}, descriptor = {}) => {
31
+ const logicalIdColumn = getCanonicalResourceIdColumn(descriptor)
32
+ const logicalId = row?.[logicalIdColumn]
33
+
34
+ if (logicalId !== undefined && logicalId !== null) {
35
+ return String(logicalId)
36
+ }
37
+
38
+ if (row?.id !== undefined && row?.id !== null) {
39
+ return String(row.id)
40
+ }
41
+
42
+ return null
43
+ }
44
+
45
+ export const translateCanonicalRecordFromStorage = (row = {}, descriptor = {}, options = {}) => {
46
+ const attributes = {}
47
+ const logicalIdColumn = getCanonicalResourceIdColumn(descriptor)
48
+ const consumedColumns = new Set(['id', logicalIdColumn])
49
+ const allowedExtraFields = new Set(options.allowedExtraFields || [])
50
+
51
+ for (const [slot, logical] of Object.entries(descriptor.reverseAttributes || {})) {
52
+ if (slot in row) {
53
+ attributes[logical] = row[slot]
54
+ consumedColumns.add(slot)
55
+ }
56
+ }
57
+
58
+ const relationships = {}
59
+
60
+ for (const [alias, info] of Object.entries(descriptor.belongsTo || {})) {
61
+ const idValue = row[info.idColumn]
62
+ consumedColumns.add(info.idColumn)
63
+ if (info.typeColumn) {
64
+ consumedColumns.add(info.typeColumn)
65
+ }
66
+ const relationshipData = idValue == null
67
+ ? null
68
+ : { type: info.target, id: String(idValue) }
69
+
70
+ relationships[alias] = { data: relationshipData }
71
+ }
72
+
73
+ for (const [alias, info] of Object.entries(descriptor.polymorphicBelongsTo || {})) {
74
+ const typeValue = info.typeColumn ? row[info.typeColumn] : null
75
+ const idValue = info.idColumn ? row[info.idColumn] : null
76
+ if (info.typeColumn) {
77
+ consumedColumns.add(info.typeColumn)
78
+ }
79
+ if (info.idColumn) {
80
+ consumedColumns.add(info.idColumn)
81
+ }
82
+
83
+ let relationshipData = null
84
+ if (typeValue != null && idValue != null) {
85
+ relationshipData = {
86
+ type: String(typeValue),
87
+ id: String(idValue),
88
+ }
89
+ }
90
+
91
+ relationships[alias] = { data: relationshipData }
92
+ }
93
+
94
+ const canonical = descriptor.canonical || {}
95
+ const internalColumns = new Set([
96
+ canonical.tenantColumn,
97
+ canonical.resourceColumn,
98
+ 'created_at',
99
+ 'updated_at',
100
+ 'deleted_at'
101
+ ].filter(Boolean))
102
+
103
+ for (const [columnName, value] of Object.entries(row)) {
104
+ if (consumedColumns.has(columnName) || internalColumns.has(columnName)) {
105
+ continue
106
+ }
107
+ if (!allowedExtraFields.has(columnName)) {
108
+ continue
109
+ }
110
+ attributes[columnName] = value
111
+ }
112
+
113
+ return { attributes, relationships }
114
+ }
115
+
116
+ export const getCanonicalFieldValue = (row, descriptor = {}, fieldName) => {
117
+ if (!row || !fieldName) return undefined
118
+
119
+ if (fieldName === 'id') {
120
+ return getCanonicalResourceId(row, descriptor)
121
+ }
122
+
123
+ const fieldInfo = descriptor.fields?.[fieldName]
124
+ if (fieldInfo?.slot && Object.hasOwn(row, fieldInfo.slot)) {
125
+ return row[fieldInfo.slot]
126
+ }
127
+
128
+ if (Object.hasOwn(row, fieldName)) {
129
+ return row[fieldName]
130
+ }
131
+
132
+ return undefined
133
+ }
@@ -1,3 +1,15 @@
1
+ import {
2
+ getFieldValue as getLegacyFieldValue,
3
+ getIdColumn,
4
+ getStorageColumn,
5
+ translateAttributesForStorage,
6
+ } from './storage-mapping.js'
7
+ import {
8
+ getCanonicalFieldValue,
9
+ getCanonicalResourceIdColumn,
10
+ translateCanonicalAttributesForStorage,
11
+ } from './canonical-storage-mapping.js'
12
+
1
13
  const passthrough = (value) => value
2
14
  const identityTranslate = (_field, value) => value
3
15
  const identityScope = (query) => query
@@ -22,6 +34,59 @@ const normalizeBelongsToValue = (value) => {
22
34
  return String(value)
23
35
  }
24
36
 
37
+ const translateSourceColumn = (source, adapter) => {
38
+ if (!adapter || source === '*') return source
39
+ return adapter.translateColumn(source) || source
40
+ }
41
+
42
+ export const createSelectTranslator = (adapter) => {
43
+ if (!adapter) return null
44
+
45
+ return (field, alias) => {
46
+ if (field === '*') {
47
+ return alias ? `${alias}.*` : '*'
48
+ }
49
+
50
+ const translated = adapter.translateColumn(field)
51
+ const result = translated || field
52
+
53
+ if (field === 'id' && result !== 'id') {
54
+ const qualified = alias ? `${alias}.${result}` : result
55
+ return `${qualified} as id`
56
+ }
57
+
58
+ if (!alias) {
59
+ return result
60
+ }
61
+
62
+ if (result.includes('.')) {
63
+ return result
64
+ }
65
+
66
+ return `${alias}.${result}`
67
+ }
68
+ }
69
+
70
+ export const translateSelectFieldsForAdapter = (fields, adapter) => {
71
+ if (!adapter || !fields) return fields
72
+
73
+ const translateField = createSelectTranslator(adapter)
74
+
75
+ return fields.map((field) => {
76
+ if (typeof field !== 'string') return field
77
+ if (field === '*') return '*'
78
+
79
+ const aliasMatch = field.match(/\s+as\s+/i)
80
+ if (aliasMatch) {
81
+ const [source, alias] = field.split(/\s+as\s+/i)
82
+ const translatedSource = translateSourceColumn(source.trim(), adapter)
83
+ return `${translatedSource} as ${alias.trim()}`
84
+ }
85
+
86
+ return translateField(field)
87
+ })
88
+ }
89
+
25
90
  const baseAdapter = ({
26
91
  knex,
27
92
  tableName,
@@ -29,6 +94,8 @@ const baseAdapter = ({
29
94
  translateColumn,
30
95
  translateFilterValue = identityTranslate,
31
96
  applyResourceScope = identityScope,
97
+ toStorageRow = passthrough,
98
+ getFieldValue = (record, fieldName) => record?.[fieldName],
32
99
  isCanonical = false,
33
100
  }) => {
34
101
  const buildBaseQuery = ({ transaction } = {}) => {
@@ -43,6 +110,8 @@ const baseAdapter = ({
43
110
  translateColumn,
44
111
  translateFilterValue,
45
112
  applyResourceScope,
113
+ toStorageRow,
114
+ getFieldValue,
46
115
  buildBaseQuery,
47
116
  selectColumns: (builder, columns) => selectColumnsOnBuilder(builder, columns),
48
117
  }
@@ -50,9 +119,11 @@ const baseAdapter = ({
50
119
 
51
120
  const createLegacyAdapter = ({ knex, schemaInfo }) => {
52
121
  const tableName = schemaInfo.tableName
53
- const idColumn = schemaInfo.idProperty || 'id'
122
+ const idColumn = getIdColumn(schemaInfo)
54
123
 
55
- const translateColumn = (column) => column
124
+ const translateColumn = (field) => getStorageColumn(schemaInfo, field)
125
+ const toStorageRow = (attributes, options = {}) => translateAttributesForStorage(attributes, schemaInfo, options)
126
+ const getFieldValue = (record, fieldName) => getLegacyFieldValue(record, schemaInfo, fieldName)
56
127
 
57
128
  return baseAdapter({
58
129
  knex,
@@ -61,6 +132,8 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
61
132
  translateColumn,
62
133
  translateFilterValue: identityTranslate,
63
134
  applyResourceScope: identityScope,
135
+ toStorageRow,
136
+ getFieldValue,
64
137
  isCanonical: false,
65
138
  })
66
139
  }
@@ -74,7 +147,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
74
147
 
75
148
  const translateColumn = (field) => {
76
149
  if (!field) return field
77
- if (field === 'id') return 'id'
150
+ if (field === 'id') return getCanonicalResourceIdColumn(descriptor)
78
151
 
79
152
  const canonicalEntry = canonicalFieldMap[field]
80
153
  if (typeof canonicalEntry === 'string') {
@@ -136,13 +209,18 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
136
209
  .where(canonical.resourceColumn, descriptor.resource)
137
210
  }
138
211
 
212
+ const toStorageRow = (attributes) => translateCanonicalAttributesForStorage(attributes, descriptor)
213
+ const getFieldValue = (record, fieldName) => getCanonicalFieldValue(record, descriptor, fieldName)
214
+
139
215
  return baseAdapter({
140
216
  knex,
141
217
  tableName: canonical.tableName,
142
- idColumn: 'id',
218
+ idColumn: getCanonicalResourceIdColumn(descriptor),
143
219
  translateColumn,
144
220
  translateFilterValue,
145
221
  applyResourceScope,
222
+ toStorageRow,
223
+ getFieldValue,
146
224
  isCanonical: true,
147
225
  })
148
226
  }
@@ -0,0 +1,274 @@
1
+ const EMPTY_STORAGE_INFO = Object.freeze({
2
+ idColumn: 'id',
3
+ storage: Object.freeze({
4
+ naming: 'snake_case'
5
+ }),
6
+ fields: Object.freeze({}),
7
+ columns: Object.freeze({})
8
+ })
9
+
10
+ const isPlainObject = (value) => (
11
+ value !== null &&
12
+ typeof value === 'object' &&
13
+ !Array.isArray(value)
14
+ )
15
+
16
+ const SNAKE_CASE_STORAGE_NAMING = 'snake_case'
17
+ const EXACT_STORAGE_NAMING = 'exact'
18
+
19
+ const STORAGE_NAMING_ALIASES = Object.freeze({
20
+ snake: SNAKE_CASE_STORAGE_NAMING,
21
+ snake_case: SNAKE_CASE_STORAGE_NAMING,
22
+ snakeCase: SNAKE_CASE_STORAGE_NAMING,
23
+ exact: EXACT_STORAGE_NAMING,
24
+ field: EXACT_STORAGE_NAMING,
25
+ verbatim: EXACT_STORAGE_NAMING
26
+ })
27
+
28
+ const toSnakeCase = (value) => String(value)
29
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
30
+ .replace(/([A-Z]+)([A-Z][a-z0-9])/g, '$1_$2')
31
+ .toLowerCase()
32
+
33
+ export const normalizeStorageConfig = (storage = undefined) => {
34
+ if (storage === undefined || storage === null) {
35
+ return {
36
+ naming: SNAKE_CASE_STORAGE_NAMING
37
+ }
38
+ }
39
+
40
+ if (!isPlainObject(storage)) {
41
+ throw new Error('Resource storage metadata must be an object.')
42
+ }
43
+
44
+ const rawNaming = storage.naming
45
+ const naming = rawNaming === undefined
46
+ ? SNAKE_CASE_STORAGE_NAMING
47
+ : STORAGE_NAMING_ALIASES[rawNaming]
48
+
49
+ if (!naming) {
50
+ throw new Error(
51
+ `Invalid resource storage.naming value '${String(rawNaming)}'. Expected 'snake_case' or 'exact'.`
52
+ )
53
+ }
54
+
55
+ return {
56
+ ...storage,
57
+ naming
58
+ }
59
+ }
60
+
61
+ const resolveDefaultStorageColumn = (fieldName, { idColumn = 'id', storageConfig = undefined } = {}) => {
62
+ if (fieldName === 'id') {
63
+ return idColumn
64
+ }
65
+
66
+ const normalizedStorage = normalizeStorageConfig(storageConfig)
67
+ if (normalizedStorage.naming === EXACT_STORAGE_NAMING) {
68
+ return fieldName
69
+ }
70
+
71
+ return toSnakeCase(fieldName)
72
+ }
73
+
74
+ export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = 'id', storageConfig = undefined } = {}) => {
75
+ const fieldStorage = definition.storage
76
+
77
+ if (fieldStorage !== undefined && !isPlainObject(fieldStorage)) {
78
+ throw new Error(`Field '${fieldName}' has invalid storage metadata. Expected an object.`)
79
+ }
80
+
81
+ const column = fieldStorage?.column ?? resolveDefaultStorageColumn(fieldName, {
82
+ idColumn,
83
+ storageConfig
84
+ })
85
+ if (typeof column !== 'string' || column.trim() === '') {
86
+ throw new Error(`Field '${fieldName}' has an invalid storage.column value.`)
87
+ }
88
+
89
+ if (fieldName === 'id' && column !== idColumn) {
90
+ throw new Error(
91
+ `Field 'id' must use storage.column '${idColumn}' to match the resource id column.`
92
+ )
93
+ }
94
+
95
+ if (fieldStorage?.serialize !== undefined && typeof fieldStorage.serialize !== 'function') {
96
+ throw new Error(`Field '${fieldName}' has an invalid storage.serialize value. Expected a function.`)
97
+ }
98
+
99
+ return {
100
+ column,
101
+ serialize: fieldStorage?.serialize || null,
102
+ persisted: definition.virtual !== true && definition.computed !== true
103
+ }
104
+ }
105
+
106
+ export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id', storage = undefined } = {}) => {
107
+ const idColumn = idProperty || 'id'
108
+ const storageConfig = normalizeStorageConfig(storage)
109
+ const fields = {}
110
+ const columns = {}
111
+ const usedColumns = new Map([[idColumn, 'id']])
112
+
113
+ for (const [fieldName, definition] of Object.entries(schemaStructure)) {
114
+ const normalized = normalizeFieldStorage(fieldName, definition, {
115
+ idColumn,
116
+ storageConfig
117
+ })
118
+ const isIdSurrogateField = fieldName === idColumn && definition.type === 'id'
119
+
120
+ if (fieldName !== 'id' && normalized.column === idColumn && !isIdSurrogateField) {
121
+ throw new Error(
122
+ `Field '${fieldName}' cannot use storage.column '${idColumn}' because it is reserved for the resource id.`
123
+ )
124
+ }
125
+
126
+ const existingField = usedColumns.get(normalized.column)
127
+ if (existingField && existingField !== fieldName && !(isIdSurrogateField && existingField === 'id')) {
128
+ throw new Error(
129
+ `Fields '${existingField}' and '${fieldName}' both map to storage column '${normalized.column}'.`
130
+ )
131
+ }
132
+
133
+ usedColumns.set(normalized.column, fieldName)
134
+ fields[fieldName] = {
135
+ ...normalized,
136
+ definition
137
+ }
138
+
139
+ if (normalized.persisted && !isIdSurrogateField) {
140
+ columns[normalized.column] = fieldName
141
+ }
142
+ }
143
+
144
+ return {
145
+ idColumn,
146
+ storage: storageConfig,
147
+ fields,
148
+ columns
149
+ }
150
+ }
151
+
152
+ export const getStorageInfo = (schemaInfo = {}) => {
153
+ if (schemaInfo.storageInfo) {
154
+ return schemaInfo.storageInfo
155
+ }
156
+
157
+ if (!schemaInfo.schemaStructure) {
158
+ return EMPTY_STORAGE_INFO
159
+ }
160
+
161
+ return buildStorageInfo({
162
+ schemaStructure: schemaInfo.schemaStructure,
163
+ idProperty: schemaInfo.idProperty || 'id',
164
+ storage: schemaInfo.storage
165
+ })
166
+ }
167
+
168
+ export const getIdColumn = (schemaInfo = {}) => {
169
+ return getStorageInfo(schemaInfo).idColumn || schemaInfo.idProperty || 'id'
170
+ }
171
+
172
+ export const getStorageColumn = (schemaInfo = {}, fieldName) => {
173
+ if (!fieldName) return fieldName
174
+ if (fieldName === 'id') return getIdColumn(schemaInfo)
175
+
176
+ const fieldStorage = getStorageInfo(schemaInfo).fields[fieldName]
177
+ return fieldStorage?.column || fieldName
178
+ }
179
+
180
+ export const getLogicalFieldName = (schemaInfo = {}, columnName) => {
181
+ if (!columnName) return columnName
182
+ if (columnName === getIdColumn(schemaInfo)) return 'id'
183
+
184
+ const logicalField = getStorageInfo(schemaInfo).columns[columnName]
185
+ return logicalField || columnName
186
+ }
187
+
188
+ export const getFieldValue = (record, schemaInfo = {}, fieldName) => {
189
+ if (!record || !fieldName) return undefined
190
+
191
+ if (fieldName === 'id') {
192
+ if (Object.hasOwn(record, 'id')) {
193
+ return record.id
194
+ }
195
+
196
+ const idColumn = getIdColumn(schemaInfo)
197
+ if (Object.hasOwn(record, idColumn)) {
198
+ return record[idColumn]
199
+ }
200
+
201
+ return translateRecordFromStorage(record, schemaInfo).id
202
+ }
203
+
204
+ if (Object.hasOwn(record, fieldName)) {
205
+ return record[fieldName]
206
+ }
207
+
208
+ const columnName = getStorageColumn(schemaInfo, fieldName)
209
+ if (Object.hasOwn(record, columnName)) {
210
+ return record[columnName]
211
+ }
212
+
213
+ return translateRecordFromStorage(record, schemaInfo)[fieldName]
214
+ }
215
+
216
+ export const translateAttributesForStorage = (attributes, schemaInfo = {}, options = {}) => {
217
+ if (!attributes) return attributes || {}
218
+
219
+ const { context = null, operation = null } = options
220
+ const storageInfo = getStorageInfo(schemaInfo)
221
+ const computed = schemaInfo.computed || {}
222
+
223
+ return Object.entries(attributes).reduce((translated, [fieldName, value]) => {
224
+ if (fieldName in computed) {
225
+ return translated
226
+ }
227
+
228
+ const fieldStorage = storageInfo.fields[fieldName]
229
+ if (fieldStorage && !fieldStorage.persisted) {
230
+ return translated
231
+ }
232
+
233
+ const columnName = fieldName === 'id'
234
+ ? storageInfo.idColumn
235
+ : fieldStorage?.column || fieldName
236
+ const serializedValue = fieldStorage?.serialize
237
+ ? fieldStorage.serialize(value, {
238
+ fieldName,
239
+ columnName,
240
+ definition: fieldStorage.definition,
241
+ schemaInfo,
242
+ context,
243
+ operation
244
+ })
245
+ : value
246
+
247
+ translated[columnName] = serializedValue
248
+ return translated
249
+ }, {})
250
+ }
251
+
252
+ export const translateRecordFromStorage = (record, schemaInfo = {}) => {
253
+ if (!record || !schemaInfo) return record
254
+
255
+ const storageInfo = getStorageInfo(schemaInfo)
256
+ const translated = {}
257
+
258
+ for (const [columnName, value] of Object.entries(record)) {
259
+ if (columnName === storageInfo.idColumn) {
260
+ translated.id = value
261
+ continue
262
+ }
263
+
264
+ const logicalField = storageInfo.columns[columnName]
265
+ if (logicalField) {
266
+ translated[logicalField] = value
267
+ continue
268
+ }
269
+
270
+ translated[columnName] = value
271
+ }
272
+
273
+ return translated
274
+ }
@@ -1,5 +1,5 @@
1
1
  import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
2
- import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-helpers.js'
2
+ import { normalizeRelationshipIdentifiers } from '../querying-writing/resource-id-normalization.js'
3
3
 
4
4
  /**
5
5
  * Updates many-to-many relationships intelligently by synchronizing pivot table records
@@ -103,7 +103,8 @@ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-hel
103
103
  export const updateManyToManyRelationship = async (scope, deps) => {
104
104
  // Extract values from deps
105
105
  const { api, context } = deps
106
- const { resourceId, relDef, relData, transaction: trx } = context
106
+ const { resourceId, relDef, transaction: trx } = context
107
+ const relData = normalizeRelationshipIdentifiers(context.relData || [], { api })
107
108
 
108
109
  // Get the knex instance from the pivot scope
109
110
  const pivotScope = api.resources[relDef.through]
@@ -257,6 +258,7 @@ export const updateManyToManyRelationship = async (scope, deps) => {
257
258
  * 6. Returns (no data returned, throws on error)
258
259
  */
259
260
  export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
261
+ relData = normalizeRelationshipIdentifiers(relData, { api })
260
262
  if (relData.length === 0) return // Early exit if nothing to create
261
263
 
262
264
  // Get pivot table info