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
@@ -0,0 +1,463 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import {
5
+ validateJsonApiStructure,
6
+ cleanTables,
7
+ createJsonApiDocument,
8
+ assertResourceAttributes,
9
+ createRelationship,
10
+ resourceIdentifier
11
+ } from './helpers/test-utils.js'
12
+ import { storageMode } from './helpers/storage-mode.js'
13
+ import { createAutoFilterApi } from './fixtures/api-configs.js'
14
+
15
+ const knex = knexLib({
16
+ client: 'better-sqlite3',
17
+ connection: {
18
+ filename: ':memory:'
19
+ },
20
+ useNullAsDefault: true
21
+ })
22
+
23
+ let api
24
+
25
+ const scopedContext = (workspaceId, userId, projectId) => ({
26
+ scopeValues: {
27
+ workspaceId,
28
+ userId,
29
+ ...(projectId !== undefined ? { projectId } : {})
30
+ }
31
+ })
32
+
33
+ const moveTaskOutOfScope = async ({ taskId, workspaceId, userId }) => {
34
+ if (storageMode.isAnyApi()) {
35
+ const descriptor = await api.anyapi.registry.getDescriptor(api.anyapi.tenantId, 'tasks')
36
+ await knex('any_records')
37
+ .where({
38
+ id: Number(taskId),
39
+ tenant_id: api.anyapi.tenantId,
40
+ resource: 'tasks'
41
+ })
42
+ .update({
43
+ [descriptor.fields.workspace_id.slot]: workspaceId,
44
+ [descriptor.fields.user_id.slot]: userId
45
+ })
46
+ return
47
+ }
48
+
49
+ await knex('autofilter_tasks')
50
+ .where({ id: Number(taskId) })
51
+ .update({
52
+ workspace_id: workspaceId,
53
+ user_id: userId
54
+ })
55
+ }
56
+
57
+ describe('AutoFilter Plugin', () => {
58
+ before(async () => {
59
+ api = await createAutoFilterApi(knex)
60
+ })
61
+
62
+ after(async () => {
63
+ await knex.destroy()
64
+ })
65
+
66
+ beforeEach(async () => {
67
+ await cleanTables(knex, [
68
+ 'autofilter_workspace_reports',
69
+ 'autofilter_user_notes',
70
+ 'autofilter_projects',
71
+ 'autofilter_tasks',
72
+ 'autofilter_optional_tasks',
73
+ 'autofilter_system_settings'
74
+ ])
75
+ })
76
+
77
+ it('stamps workspace-scoped records and filters collections by workspace', async () => {
78
+ const reportA = await api.resources.workspace_reports.post({
79
+ inputRecord: createJsonApiDocument('workspace_reports', {
80
+ title: 'Workspace A report'
81
+ }),
82
+ simplified: false
83
+ }, scopedContext('workspace-a', 101))
84
+
85
+ await api.resources.workspace_reports.post({
86
+ inputRecord: createJsonApiDocument('workspace_reports', {
87
+ title: 'Workspace B report'
88
+ }),
89
+ simplified: false
90
+ }, scopedContext('workspace-b', 202))
91
+
92
+ validateJsonApiStructure(reportA)
93
+ assertResourceAttributes(reportA.data, {
94
+ title: 'Workspace A report',
95
+ workspace_id: 'workspace-a'
96
+ })
97
+
98
+ const queryA = await api.resources.workspace_reports.query({
99
+ simplified: false
100
+ }, scopedContext('workspace-a', 999))
101
+
102
+ validateJsonApiStructure(queryA, true)
103
+ assert.equal(queryA.data.length, 1)
104
+ assert.equal(queryA.data[0].attributes.title, 'Workspace A report')
105
+ assert.equal(queryA.data[0].attributes.workspace_id, 'workspace-a')
106
+ })
107
+
108
+ it('stamps user-scoped records and filters collections by user', async () => {
109
+ const noteA = await api.resources.user_notes.post({
110
+ inputRecord: createJsonApiDocument('user_notes', {
111
+ body: 'User 101 note'
112
+ }),
113
+ simplified: false
114
+ }, scopedContext('workspace-a', 101))
115
+
116
+ await api.resources.user_notes.post({
117
+ inputRecord: createJsonApiDocument('user_notes', {
118
+ body: 'User 202 note'
119
+ }),
120
+ simplified: false
121
+ }, scopedContext('workspace-a', 202))
122
+
123
+ validateJsonApiStructure(noteA)
124
+ assertResourceAttributes(noteA.data, {
125
+ body: 'User 101 note',
126
+ user_id: 101
127
+ })
128
+
129
+ const queryA = await api.resources.user_notes.query({
130
+ simplified: false
131
+ }, scopedContext('workspace-b', 101))
132
+
133
+ validateJsonApiStructure(queryA, true)
134
+ assert.equal(queryA.data.length, 1)
135
+ assert.equal(queryA.data[0].attributes.body, 'User 101 note')
136
+ assert.equal(queryA.data[0].attributes.user_id, 101)
137
+ })
138
+
139
+ it('applies composite workspace+user scoping', async () => {
140
+ const ownProject = await api.resources.projects.post({
141
+ inputRecord: createJsonApiDocument('projects', {
142
+ name: 'Workspace A / User 101'
143
+ }),
144
+ simplified: false
145
+ }, scopedContext('workspace-a', 101))
146
+
147
+ await api.resources.projects.post({
148
+ inputRecord: createJsonApiDocument('projects', {
149
+ name: 'Workspace A / User 202'
150
+ }),
151
+ simplified: false
152
+ }, scopedContext('workspace-a', 202))
153
+
154
+ await api.resources.projects.post({
155
+ inputRecord: createJsonApiDocument('projects', {
156
+ name: 'Workspace B / User 101'
157
+ }),
158
+ simplified: false
159
+ }, scopedContext('workspace-b', 101))
160
+
161
+ validateJsonApiStructure(ownProject)
162
+ assertResourceAttributes(ownProject.data, {
163
+ name: 'Workspace A / User 101',
164
+ workspace_id: 'workspace-a',
165
+ user_id: 101
166
+ })
167
+
168
+ const scopedQuery = await api.resources.projects.query({
169
+ simplified: false
170
+ }, scopedContext('workspace-a', 101))
171
+
172
+ validateJsonApiStructure(scopedQuery, true)
173
+ assert.equal(scopedQuery.data.length, 1)
174
+ assert.equal(scopedQuery.data[0].attributes.name, 'Workspace A / User 101')
175
+ })
176
+
177
+ it('treats public resources as unscoped', async () => {
178
+ const created = await api.resources.system_settings.post({
179
+ inputRecord: createJsonApiDocument('system_settings', {
180
+ key: 'app.version',
181
+ value: '1.0.0'
182
+ }),
183
+ simplified: false
184
+ })
185
+
186
+ validateJsonApiStructure(created)
187
+
188
+ const anonymousQuery = await api.resources.system_settings.query({
189
+ simplified: false
190
+ })
191
+ validateJsonApiStructure(anonymousQuery, true)
192
+ assert.equal(anonymousQuery.data.length, 1)
193
+
194
+ const scopedQuery = await api.resources.system_settings.query({
195
+ simplified: false
196
+ }, scopedContext('workspace-a', 101))
197
+ validateJsonApiStructure(scopedQuery, true)
198
+ assert.equal(scopedQuery.data.length, 1)
199
+ })
200
+
201
+ it('scopes single-record lookups so out-of-scope records are not found', async () => {
202
+ const project = await api.resources.projects.post({
203
+ inputRecord: createJsonApiDocument('projects', {
204
+ name: 'Scoped Project'
205
+ }),
206
+ simplified: false
207
+ }, scopedContext('workspace-a', 101))
208
+
209
+ await assert.rejects(
210
+ async () => {
211
+ await api.resources.projects.get({
212
+ id: project.data.id,
213
+ simplified: false
214
+ }, scopedContext('workspace-a', 202))
215
+ },
216
+ (error) => error.code === 'REST_API_RESOURCE',
217
+ 'Out-of-scope records should behave as not found'
218
+ )
219
+ })
220
+
221
+ it('stamps scoped fields on PUT and rejects inconsistent scoped updates', async () => {
222
+ const project = await api.resources.projects.post({
223
+ inputRecord: createJsonApiDocument('projects', {
224
+ name: 'Original Project',
225
+ description: 'Original Description'
226
+ }),
227
+ simplified: false
228
+ }, scopedContext('workspace-a', 101))
229
+
230
+ await api.resources.projects.put({
231
+ inputRecord: {
232
+ data: {
233
+ type: 'projects',
234
+ id: project.data.id,
235
+ attributes: {
236
+ name: 'Updated Project',
237
+ description: 'Updated Description',
238
+ status: 'active'
239
+ }
240
+ }
241
+ },
242
+ simplified: false
243
+ }, scopedContext('workspace-a', 101))
244
+
245
+ const afterPut = await api.resources.projects.get({
246
+ id: project.data.id,
247
+ simplified: false
248
+ }, scopedContext('workspace-a', 101))
249
+
250
+ validateJsonApiStructure(afterPut)
251
+ assertResourceAttributes(afterPut.data, {
252
+ name: 'Updated Project',
253
+ description: 'Updated Description',
254
+ workspace_id: 'workspace-a',
255
+ user_id: 101
256
+ })
257
+
258
+ await assert.rejects(
259
+ async () => {
260
+ await api.resources.projects.patch({
261
+ id: project.data.id,
262
+ inputRecord: {
263
+ data: {
264
+ type: 'projects',
265
+ id: project.data.id,
266
+ attributes: {
267
+ workspace_id: 'workspace-b'
268
+ }
269
+ }
270
+ },
271
+ simplified: false
272
+ }, scopedContext('workspace-a', 101))
273
+ },
274
+ (error) => error.code === 'REST_API_VALIDATION',
275
+ 'PATCH should reject inconsistent scope-field updates'
276
+ )
277
+ })
278
+
279
+ it('uses scoped lookups when validating relationships', async () => {
280
+ const projectA = await api.resources.projects.post({
281
+ inputRecord: createJsonApiDocument('projects', {
282
+ name: 'Project A'
283
+ }),
284
+ simplified: false
285
+ }, scopedContext('workspace-a', 101))
286
+
287
+ const projectB = await api.resources.projects.post({
288
+ inputRecord: createJsonApiDocument('projects', {
289
+ name: 'Project B'
290
+ }),
291
+ simplified: false
292
+ }, scopedContext('workspace-b', 101))
293
+
294
+ const task = await api.resources.tasks.post({
295
+ inputRecord: createJsonApiDocument('tasks',
296
+ {
297
+ title: 'Task A'
298
+ },
299
+ {
300
+ project: createRelationship(resourceIdentifier('projects', projectA.data.id))
301
+ }
302
+ ),
303
+ simplified: false
304
+ }, scopedContext('workspace-a', 101))
305
+
306
+ validateJsonApiStructure(task)
307
+ assertResourceAttributes(task.data, {
308
+ title: 'Task A',
309
+ workspace_id: 'workspace-a',
310
+ user_id: 101
311
+ })
312
+
313
+ await assert.rejects(
314
+ async () => {
315
+ await api.resources.tasks.post({
316
+ inputRecord: createJsonApiDocument('tasks',
317
+ {
318
+ title: 'Cross-scope Task'
319
+ },
320
+ {
321
+ project: createRelationship(resourceIdentifier('projects', projectB.data.id))
322
+ }
323
+ ),
324
+ simplified: false
325
+ }, scopedContext('workspace-a', 101))
326
+ },
327
+ (error) => error.code === 'REST_API_RESOURCE' || error.code === 'REST_API_VALIDATION',
328
+ 'Relationship validation should respect scoped datasets'
329
+ )
330
+ })
331
+
332
+ it('scopes included resources through the same autofilter path as top-level queries', async () => {
333
+ const project = await api.resources.projects.post({
334
+ inputRecord: createJsonApiDocument('projects', {
335
+ name: 'Scoped include project'
336
+ }),
337
+ simplified: false
338
+ }, scopedContext('workspace-a', 101))
339
+
340
+ const visibleTask = await api.resources.tasks.post({
341
+ inputRecord: createJsonApiDocument('tasks',
342
+ {
343
+ title: 'Visible task'
344
+ },
345
+ {
346
+ project: createRelationship(resourceIdentifier('projects', project.data.id))
347
+ }
348
+ ),
349
+ simplified: false
350
+ }, scopedContext('workspace-a', 101))
351
+
352
+ const hiddenTask = await api.resources.tasks.post({
353
+ inputRecord: createJsonApiDocument('tasks',
354
+ {
355
+ title: 'Out-of-scope task'
356
+ },
357
+ {
358
+ project: createRelationship(resourceIdentifier('projects', project.data.id))
359
+ }
360
+ ),
361
+ simplified: false
362
+ }, scopedContext('workspace-a', 101))
363
+
364
+ await moveTaskOutOfScope({
365
+ taskId: hiddenTask.data.id,
366
+ workspaceId: 'workspace-b',
367
+ userId: 202
368
+ })
369
+
370
+ const result = await api.resources.projects.get({
371
+ id: project.data.id,
372
+ queryParams: {
373
+ include: ['tasks']
374
+ },
375
+ simplified: false
376
+ }, scopedContext('workspace-a', 101))
377
+
378
+ validateJsonApiStructure(result)
379
+ assert.deepEqual(result.data.relationships.tasks.data, [
380
+ resourceIdentifier('tasks', visibleTask.data.id)
381
+ ])
382
+ assert.equal(result.included?.length, 1)
383
+ assert.equal(result.included?.[0]?.id, visibleTask.data.id)
384
+ assert.equal(result.included?.[0]?.attributes.title, 'Visible task')
385
+ })
386
+
387
+ it('treats null belongsTo autofilter values as valid null linkage on writes', async () => {
388
+ const implicitNull = await api.resources.optional_tasks.post({
389
+ inputRecord: createJsonApiDocument('optional_tasks', {
390
+ title: 'Implicit null project'
391
+ }),
392
+ simplified: false
393
+ }, scopedContext('workspace-a', 101))
394
+
395
+ const explicitNull = await api.resources.optional_tasks.post({
396
+ inputRecord: createJsonApiDocument(
397
+ 'optional_tasks',
398
+ {
399
+ title: 'Explicit null project'
400
+ },
401
+ {
402
+ project: { data: null }
403
+ }
404
+ ),
405
+ simplified: false
406
+ }, scopedContext('workspace-a', 101))
407
+
408
+ const implicitFetched = await api.resources.optional_tasks.get({
409
+ id: implicitNull.data.id,
410
+ simplified: false
411
+ }, scopedContext('workspace-a', 101))
412
+
413
+ const explicitFetched = await api.resources.optional_tasks.get({
414
+ id: explicitNull.data.id,
415
+ simplified: false
416
+ }, scopedContext('workspace-a', 101))
417
+
418
+ validateJsonApiStructure(implicitFetched)
419
+ validateJsonApiStructure(explicitFetched)
420
+ assert.equal(implicitFetched.data.relationships.project.data, null)
421
+ assert.equal(explicitFetched.data.relationships.project.data, null)
422
+ })
423
+
424
+ it('fails cleanly when a required autofilter value is missing', async () => {
425
+ await assert.rejects(
426
+ async () => {
427
+ await api.resources.projects.query({
428
+ simplified: false
429
+ })
430
+ },
431
+ (error) => error.code === 'REST_API_AUTOFILTER_CONTEXT',
432
+ 'Scoped resources should require configured scope values'
433
+ )
434
+
435
+ await assert.rejects(
436
+ async () => {
437
+ await api.resources.projects.post({
438
+ inputRecord: createJsonApiDocument('projects', {
439
+ name: 'No Scope'
440
+ }),
441
+ simplified: false
442
+ })
443
+ },
444
+ (error) => error.code === 'REST_API_AUTOFILTER_CONTEXT',
445
+ 'Scoped writes should require configured scope values'
446
+ )
447
+ })
448
+
449
+ it('exposes compiled preset configuration without auth semantics', () => {
450
+ assert.deepEqual(api.autofilter.getConfig(), {
451
+ presets: ['public', 'workspace', 'user', 'workspace_user'],
452
+ resolvers: ['workspace', 'user', 'currentProject']
453
+ })
454
+
455
+ assert.deepEqual(api.autofilter.getScopeConfig('projects'), {
456
+ preset: 'workspace_user',
457
+ filters: [
458
+ { field: 'workspace_id', resolver: 'workspace', required: true },
459
+ { field: 'user_id', resolver: 'user', required: true }
460
+ ]
461
+ })
462
+ })
463
+ })
@@ -0,0 +1,128 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import { Api } from 'hooked-api'
5
+ import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
6
+ import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
7
+ import {
8
+ cleanTables,
9
+ validateJsonApiStructure
10
+ } from './helpers/test-utils.js'
11
+
12
+ const knex = knexLib({
13
+ client: 'better-sqlite3',
14
+ connection: {
15
+ filename: ':memory:'
16
+ },
17
+ useNullAsDefault: true
18
+ })
19
+
20
+ let api
21
+
22
+ function createMysqlStyleInsertDb (baseKnex) {
23
+ return (tableName) => {
24
+ const query = baseKnex(tableName)
25
+ const originalInsert = query.insert.bind(query)
26
+
27
+ query.insert = (...args) => {
28
+ const insertBuilder = originalInsert(...args)
29
+ const originalReturning = insertBuilder.returning.bind(insertBuilder)
30
+
31
+ insertBuilder.returning = async (...returnArgs) => {
32
+ await originalReturning(...returnArgs)
33
+ return [0]
34
+ }
35
+
36
+ return insertBuilder
37
+ }
38
+
39
+ return query
40
+ }
41
+ }
42
+
43
+ describe('Custom id POST fallback when insert returning is unusable', () => {
44
+ before(async () => {
45
+ api = new Api({
46
+ name: 'custom-id-mysql-returning-fallback-test'
47
+ })
48
+
49
+ await api.use(RestApiPlugin, {
50
+ simplifiedApi: true,
51
+ simplifiedTransport: false,
52
+ returnRecordApi: {
53
+ post: 'full',
54
+ put: 'full',
55
+ patch: 'full'
56
+ }
57
+ })
58
+ await api.use(RestApiKnexPlugin, { knex })
59
+
60
+ await api.addResource('user_settings', {
61
+ tableName: 'mysqlish_user_settings',
62
+ idProperty: 'user_id',
63
+ searchSchema: {
64
+ id: { type: 'id', actualField: 'id' }
65
+ },
66
+ schema: {
67
+ theme: { type: 'string', required: true, max: 32 }
68
+ }
69
+ })
70
+ await api.resources.user_settings.createKnexTable()
71
+ })
72
+
73
+ after(async () => {
74
+ await knex.destroy()
75
+ })
76
+
77
+ beforeEach(async () => {
78
+ await cleanTables(knex, ['mysqlish_user_settings'])
79
+ })
80
+
81
+ it('returns the explicit id for simplified POST when the insert result is 0', async () => {
82
+ const mysqlStyleDb = createMysqlStyleInsertDb(knex)
83
+
84
+ const created = await api.resources.user_settings.post({
85
+ inputRecord: {
86
+ id: '6',
87
+ theme: 'dark'
88
+ },
89
+ simplified: true,
90
+ transaction: mysqlStyleDb
91
+ })
92
+
93
+ assert.equal(created.id, '6')
94
+ assert.equal(created.theme, 'dark')
95
+
96
+ const dbRow = await knex('mysqlish_user_settings').where('user_id', 6).first()
97
+ assert.ok(dbRow)
98
+ assert.equal(String(dbRow.user_id), '6')
99
+ assert.equal(dbRow.theme, 'dark')
100
+ })
101
+
102
+ it('returns the explicit id for non-simplified POST when the insert result is 0', async () => {
103
+ const mysqlStyleDb = createMysqlStyleInsertDb(knex)
104
+
105
+ const created = await api.resources.user_settings.post({
106
+ inputRecord: {
107
+ data: {
108
+ type: 'user_settings',
109
+ id: '8',
110
+ attributes: {
111
+ theme: 'light'
112
+ }
113
+ }
114
+ },
115
+ simplified: false,
116
+ transaction: mysqlStyleDb
117
+ })
118
+
119
+ validateJsonApiStructure(created)
120
+ assert.equal(created.data.id, '8')
121
+ assert.equal(created.data.attributes.theme, 'light')
122
+
123
+ const dbRow = await knex('mysqlish_user_settings').where('user_id', 8).first()
124
+ assert.ok(dbRow)
125
+ assert.equal(String(dbRow.user_id), '8')
126
+ assert.equal(dbRow.theme, 'light')
127
+ })
128
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import { Api } from 'hooked-api'
5
+ import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
6
+ import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
7
+ import { cleanTables } from './helpers/test-utils.js'
8
+
9
+ const knex = knexLib({
10
+ client: 'better-sqlite3',
11
+ connection: {
12
+ filename: ':memory:'
13
+ },
14
+ useNullAsDefault: true
15
+ })
16
+
17
+ let api
18
+
19
+ describe('Simplified POST with explicit resource ids', () => {
20
+ before(async () => {
21
+ api = new Api({
22
+ name: 'custom-id-simplified-post-test'
23
+ })
24
+
25
+ await api.use(RestApiPlugin, {
26
+ simplifiedApi: true,
27
+ simplifiedTransport: false,
28
+ returnRecordApi: {
29
+ post: 'full',
30
+ put: 'full',
31
+ patch: 'full'
32
+ }
33
+ })
34
+ await api.use(RestApiKnexPlugin, { knex })
35
+
36
+ await api.addResource('user_settings', {
37
+ tableName: 'test_user_settings',
38
+ idProperty: 'user_id',
39
+ searchSchema: {
40
+ id: { type: 'id', actualField: 'id' }
41
+ },
42
+ schema: {
43
+ theme: { type: 'string', required: true, max: 32 }
44
+ }
45
+ })
46
+ await api.resources.user_settings.createKnexTable()
47
+ })
48
+
49
+ after(async () => {
50
+ await knex.destroy()
51
+ })
52
+
53
+ beforeEach(async () => {
54
+ await cleanTables(knex, ['test_user_settings'])
55
+ })
56
+
57
+ it('persists the provided resource id when posting simplified records to a custom-id resource', async () => {
58
+ const created = await api.resources.user_settings.post({
59
+ id: '7',
60
+ theme: 'dark'
61
+ })
62
+
63
+ assert.equal(created.id, '7')
64
+ assert.equal(created.theme, 'dark')
65
+
66
+ const dbRow = await knex('test_user_settings').where('user_id', 7).first()
67
+ assert.ok(dbRow)
68
+ assert.equal(String(dbRow.user_id), '7')
69
+ assert.equal(dbRow.theme, 'dark')
70
+
71
+ const fetched = await api.resources.user_settings.get({ id: '7' })
72
+ assert.equal(fetched.id, '7')
73
+ assert.equal(fetched.theme, 'dark')
74
+ })
75
+ })
@@ -161,6 +161,14 @@ describe('Custom idProperty Relationship Operations', () => {
161
161
  assert.equal(getResult.included.length, 1)
162
162
  assert.equal(getResult.included[0].type, 'countries')
163
163
  assert.equal(getResult.included[0].attributes.name, 'United States')
164
+ assert.equal(
165
+ getResult.data.relationships.country.links.self,
166
+ `/publishers/${publisherResult.data.id}/relationships/country`
167
+ )
168
+ assert.equal(
169
+ getResult.data.relationships.country.links.related,
170
+ `/publishers/${publisherResult.data.id}/country`
171
+ )
164
172
  })
165
173
 
166
174
  it('should update belongsTo relationship via PATCH', async () => {
@@ -895,5 +903,30 @@ describe('Custom idProperty Relationship Operations', () => {
895
903
  assertResourceRelationship(putResult.data, 'country',
896
904
  resourceIdentifier('countries', country.data.id))
897
905
  })
906
+
907
+ it('should reject PUT when an existing persisted attribute is omitted', async () => {
908
+ const country = await api.resources.countries.post({
909
+ inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
910
+ })
911
+
912
+ await assert.rejects(
913
+ async () => api.resources.countries.put({
914
+ inputRecord: {
915
+ data: {
916
+ type: 'countries',
917
+ id: country.data.id,
918
+ attributes: {
919
+ name: 'Renamed Country'
920
+ }
921
+ }
922
+ }
923
+ }),
924
+ (error) => {
925
+ assert.equal(error.message, 'PUT request omits persisted fields that already have stored values')
926
+ assert(error.details.fields.includes('data.attributes.code'))
927
+ return true
928
+ }
929
+ )
930
+ })
898
931
  })
899
932
  })