json-rest-api 1.0.22 → 1.0.23

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 (260) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +2 -2
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/audit/00-charter.md +0 -103
  81. package/audit/01-scope-map.md +0 -84
  82. package/audit/02-coverage-matrix.csv +0 -174
  83. package/audit/03-invariants.md +0 -123
  84. package/audit/04-standards-rubric.md +0 -56
  85. package/audit/05-competitor-rubric.md +0 -77
  86. package/audit/06-findings.md +0 -160
  87. package/audit/07-risk-register.csv +0 -10
  88. package/audit/08-remediation-roadmap.md +0 -177
  89. package/audit/09-signoff-check.md +0 -23
  90. package/audit/10-final-synthesis.md +0 -29
  91. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  92. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  93. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  94. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  95. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  96. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  97. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  98. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  99. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  100. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  101. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  102. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  103. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  104. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  105. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  106. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  107. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  108. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  109. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  110. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  111. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  112. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  113. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  114. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  115. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  116. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  117. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  118. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  119. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  120. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  121. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  122. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  123. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  157. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  158. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  159. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  160. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  161. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  162. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  163. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  164. package/audit/evidence/code/error-model-signals.txt +0 -98
  165. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  166. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  167. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  168. package/audit/evidence/code/idproperty-signals.txt +0 -157
  169. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  170. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  171. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  172. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  173. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  174. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  175. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  176. package/audit/evidence/code/permission-signals.txt +0 -193
  177. package/audit/evidence/code/query-include-signals.txt +0 -687
  178. package/audit/evidence/code/transaction-signals.txt +0 -28
  179. package/audit/evidence/code/validation-signals.txt +0 -650
  180. package/audit/evidence/code/write-path-signals.txt +0 -61
  181. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  182. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  183. package/audit/evidence/external/corpus-freeze.md +0 -21
  184. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  185. package/audit/evidence/external/npm-download-signals.txt +0 -23
  186. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  187. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  188. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  189. package/audit/evidence/external/readme-fortune.md +0 -165
  190. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  191. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  192. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  193. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  194. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  195. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  196. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  197. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  198. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  199. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  200. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  201. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  202. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  203. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  204. package/audit/evidence/external/standards-sources.txt +0 -6
  205. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  206. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  207. package/audit/evidence/runtime/environment.txt +0 -4
  208. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  209. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  210. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  211. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  212. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  213. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  214. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  215. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  216. package/audit/evidence/runtime/test-summary.md +0 -107
  217. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  218. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  219. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  220. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  221. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  222. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  223. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  224. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  225. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  226. package/audit/evidence/tests/skipped-tests.txt +0 -10
  227. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  228. package/audit/evidence/tests/test-suite-list.txt +0 -32
  229. package/audit/subsystems/docs-drift.md +0 -49
  230. package/audit/subsystems/pass2-cross-review.md +0 -131
  231. package/audit/subsystems/plugins-access.md +0 -51
  232. package/audit/subsystems/plugins-bulk.md +0 -50
  233. package/audit/subsystems/plugins-cors.md +0 -48
  234. package/audit/subsystems/plugins-file.md +0 -53
  235. package/audit/subsystems/plugins-multihome.md +0 -50
  236. package/audit/subsystems/plugins-positioning.md +0 -54
  237. package/audit/subsystems/plugins-socketio.md +0 -50
  238. package/audit/subsystems/query-engine.md +0 -61
  239. package/audit/subsystems/relationships.md +0 -55
  240. package/audit/subsystems/rest-orchestration.md +0 -66
  241. package/audit/subsystems/schema-core.md +0 -64
  242. package/audit/subsystems/storage-knex.md +0 -57
  243. package/audit/subsystems/tests.md +0 -48
  244. package/audit/subsystems/transport-express.md +0 -55
  245. package/audit/traces/trace-read-get.md +0 -22
  246. package/audit/traces/trace-read-query.md +0 -25
  247. package/audit/traces/trace-relationship-endpoints.md +0 -31
  248. package/audit/traces/trace-write-delete.md +0 -17
  249. package/audit/traces/trace-write-patch.md +0 -26
  250. package/audit/traces/trace-write-post.md +0 -22
  251. package/audit/traces/trace-write-put.md +0 -23
  252. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  253. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  254. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  255. package/plugins/core/rest-api-access.js +0 -459
  256. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  257. package/tests/multihome.test.js +0 -553
  258. package/tests/multihome.test.js.bak +0 -550
  259. package/tests/ownership-optout.test.js +0 -76
  260. package/tests/rest-api-access.test.js +0 -206
@@ -0,0 +1,235 @@
1
+ import { describe, it, before, beforeEach, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import express from 'express'
4
+ import request from 'supertest'
5
+ import knexLib from 'knex'
6
+ import { Api } from 'hooked-api'
7
+ import {
8
+ RestApiPlugin,
9
+ RestApiKnexPlugin,
10
+ ExpressPlugin,
11
+ FastifyPlugin
12
+ } from '../index.js'
13
+ import { getUnsupportedMediaTypeErrorBody } from '../plugins/core/connectors/lib/transport-http-helpers.js'
14
+ import { cleanTables, createJsonApiDocument } from './helpers/test-utils.js'
15
+ import {
16
+ FakeFastifyApp,
17
+ invokeFastifyRoute
18
+ } from './helpers/fake-fastify.js'
19
+
20
+ const knex = knexLib({
21
+ client: 'better-sqlite3',
22
+ connection: {
23
+ filename: ':memory:'
24
+ },
25
+ useNullAsDefault: true
26
+ })
27
+
28
+ async function createConnectorParityApi () {
29
+ const expressApp = express()
30
+ const fastifyApp = new FakeFastifyApp()
31
+ const api = new Api({
32
+ name: 'connector-parity-test-api',
33
+ log: { level: process.env.LOG_LEVEL || 'silent' }
34
+ })
35
+
36
+ await api.use(RestApiPlugin, {
37
+ simplifiedApi: false,
38
+ simplifiedTransport: false,
39
+ returnRecordApi: {
40
+ post: true,
41
+ put: false,
42
+ patch: false
43
+ },
44
+ returnRecordTransport: {
45
+ post: 'full',
46
+ put: 'no',
47
+ patch: 'minimal'
48
+ },
49
+ sortableFields: ['id', 'name', 'code']
50
+ })
51
+
52
+ await api.use(RestApiKnexPlugin, { knex })
53
+ await api.use(ExpressPlugin, { mountPath: '/api' })
54
+ await api.use(FastifyPlugin, {
55
+ app: fastifyApp,
56
+ mountPath: '/api'
57
+ })
58
+
59
+ await api.customize({
60
+ hooks: {
61
+ 'transport:request': {
62
+ functionName: 'connector-parity-rejector',
63
+ handler: async ({ context }) => {
64
+ if (context.transport?.request?.headers?.['x-block-request'] === 'yes') {
65
+ context.reject(401, 'Blocked by connector parity hook', {
66
+ title: 'Unauthorized'
67
+ })
68
+ }
69
+ }
70
+ },
71
+ 'transport:response': {
72
+ functionName: 'connector-parity-header',
73
+ handler: async ({ context }) => {
74
+ context.transport.response.headers['x-connector-test'] = 'enabled'
75
+ }
76
+ }
77
+ }
78
+ })
79
+
80
+ await api.addResource('countries', {
81
+ schema: {
82
+ id: { type: 'id' },
83
+ name: { type: 'string', required: true, max: 100 },
84
+ code: { type: 'string', max: 2 }
85
+ },
86
+ tableName: 'connector_parity_countries'
87
+ })
88
+
89
+ await api.resources.countries.createKnexTable()
90
+ api.http.express.mount(expressApp)
91
+
92
+ return { api, expressApp, fastifyApp }
93
+ }
94
+
95
+ function assertLocationSuffix (location, resourceId) {
96
+ assert.ok(location, 'Expected Location header to be present')
97
+ assert.ok(
98
+ location.endsWith(`/api/countries/${resourceId}`) ||
99
+ location.endsWith(`/countries/${resourceId}`),
100
+ `Unexpected Location header: ${location}`
101
+ )
102
+ }
103
+
104
+ describe('HTTP Connector Parity', () => {
105
+ let api
106
+ let expressApp
107
+ let fastifyApp
108
+
109
+ before(async () => {
110
+ ({ api, expressApp, fastifyApp } = await createConnectorParityApi())
111
+ })
112
+
113
+ after(async () => {
114
+ await knex.destroy()
115
+ })
116
+
117
+ beforeEach(async () => {
118
+ await cleanTables(knex, ['connector_parity_countries'])
119
+ })
120
+
121
+ it('returns the same create semantics through Express and Fastify', async () => {
122
+ const expressDoc = createJsonApiDocument('countries', {
123
+ name: 'Express Country',
124
+ code: 'EC'
125
+ })
126
+ const fastifyDoc = createJsonApiDocument('countries', {
127
+ name: 'Fastify Country',
128
+ code: 'FC'
129
+ })
130
+
131
+ const expressResponse = await request(expressApp)
132
+ .post('/api/countries')
133
+ .set('Content-Type', 'application/vnd.api+json')
134
+ .set('Accept', 'application/vnd.api+json')
135
+ .send(expressDoc)
136
+
137
+ const { reply: fastifyReply } = await invokeFastifyRoute(fastifyApp, {
138
+ method: 'POST',
139
+ routeUrl: '/api/countries',
140
+ requestUrl: '/api/countries',
141
+ headers: {
142
+ 'content-type': 'application/vnd.api+json',
143
+ accept: 'application/vnd.api+json'
144
+ },
145
+ body: fastifyDoc
146
+ })
147
+
148
+ assert.equal(expressResponse.status, 201)
149
+ assert.equal(fastifyReply.statusCode, 201)
150
+
151
+ assert.equal(expressResponse.body.data.type, 'countries')
152
+ assert.equal(fastifyReply.payload.data.type, 'countries')
153
+ assert.equal(expressResponse.body.data.attributes.name, 'Express Country')
154
+ assert.equal(fastifyReply.payload.data.attributes.name, 'Fastify Country')
155
+ assert.equal(expressResponse.headers['x-connector-test'], 'enabled')
156
+ assert.equal(fastifyReply.headers['x-connector-test'], 'enabled')
157
+
158
+ assertLocationSuffix(expressResponse.headers.location, expressResponse.body.data.id)
159
+ assertLocationSuffix(fastifyReply.headers.Location, fastifyReply.payload.data.id)
160
+ })
161
+
162
+ it('rejects unsupported write content types using each connector transport policy', async () => {
163
+ const expressResponse = await request(expressApp)
164
+ .post('/api/countries')
165
+ .set('Content-Type', 'text/plain')
166
+ .send('invalid payload')
167
+
168
+ const { reply: fastifyReply } = await invokeFastifyRoute(fastifyApp, {
169
+ method: 'POST',
170
+ routeUrl: '/api/countries',
171
+ requestUrl: '/api/countries',
172
+ headers: { 'content-type': 'text/plain' },
173
+ body: 'invalid payload'
174
+ })
175
+
176
+ assert.equal(expressResponse.status, 415)
177
+ assert.equal(fastifyReply.statusCode, 415)
178
+ assert.deepEqual(
179
+ expressResponse.body,
180
+ getUnsupportedMediaTypeErrorBody({ allowMultipart: true })
181
+ )
182
+ assert.deepEqual(
183
+ fastifyReply.payload,
184
+ getUnsupportedMediaTypeErrorBody()
185
+ )
186
+ })
187
+
188
+ it('maps validation errors the same way in both connectors', async () => {
189
+ const invalidDoc = createJsonApiDocument('countries', {
190
+ code: 'IV'
191
+ })
192
+
193
+ const expressResponse = await request(expressApp)
194
+ .post('/api/countries')
195
+ .set('Content-Type', 'application/vnd.api+json')
196
+ .set('Accept', 'application/vnd.api+json')
197
+ .send(invalidDoc)
198
+
199
+ const { reply: fastifyReply } = await invokeFastifyRoute(fastifyApp, {
200
+ method: 'POST',
201
+ routeUrl: '/api/countries',
202
+ requestUrl: '/api/countries',
203
+ headers: {
204
+ 'content-type': 'application/vnd.api+json',
205
+ accept: 'application/vnd.api+json'
206
+ },
207
+ body: invalidDoc
208
+ })
209
+
210
+ assert.equal(expressResponse.status, 422)
211
+ assert.equal(fastifyReply.statusCode, 422)
212
+ assert.deepEqual(fastifyReply.payload, expressResponse.body)
213
+ assert.equal(expressResponse.headers['x-connector-test'], 'enabled')
214
+ assert.equal(fastifyReply.headers['x-connector-test'], 'enabled')
215
+ })
216
+
217
+ it('honors transport request rejection hooks the same way in both connectors', async () => {
218
+ const expressResponse = await request(expressApp)
219
+ .get('/api/countries')
220
+ .set('x-block-request', 'yes')
221
+
222
+ const { reply: fastifyReply } = await invokeFastifyRoute(fastifyApp, {
223
+ method: 'GET',
224
+ routeUrl: '/api/countries',
225
+ requestUrl: '/api/countries',
226
+ headers: { 'x-block-request': 'yes' }
227
+ })
228
+
229
+ assert.equal(expressResponse.status, 401)
230
+ assert.equal(fastifyReply.statusCode, 401)
231
+ assert.deepEqual(fastifyReply.payload, expressResponse.body)
232
+ assert.equal(expressResponse.headers['x-connector-test'], undefined)
233
+ assert.equal(fastifyReply.headers['x-connector-test'], undefined)
234
+ })
235
+ })
@@ -0,0 +1,319 @@
1
+ import { describe, it, before, beforeEach, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { Api } from 'hooked-api'
4
+ import knexLib from 'knex'
5
+ import { LabelPlugin, QueryProjectionsPlugin, RestApiKnexPlugin, RestApiPlugin } from '../index.js'
6
+ import {
7
+ cleanTables,
8
+ createJsonApiDocument,
9
+ createRelationship,
10
+ resourceIdentifier,
11
+ validateJsonApiStructure,
12
+ } from './helpers/test-utils.js'
13
+ import { createProjectedFieldsApi } 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
+ describe('Query Projections', () => {
24
+ let api
25
+ const testData = {}
26
+
27
+ before(async () => {
28
+ api = await createProjectedFieldsApi(knex)
29
+ })
30
+
31
+ after(async () => {
32
+ await knex.destroy()
33
+ })
34
+
35
+ beforeEach(async () => {
36
+ await cleanTables(knex, ['projected_books', 'projected_authors'])
37
+
38
+ const authorPayloads = [
39
+ { first_name: 'Alex', last_name: 'Carter' },
40
+ { first_name: 'Alex', last_name: 'Carter' },
41
+ { first_name: 'Bella', last_name: 'Stone' },
42
+ { first_name: 'Aaron', last_name: 'Zephyr' }
43
+ ]
44
+
45
+ testData.authors = []
46
+ for (const payload of authorPayloads) {
47
+ const result = await api.resources.authors.post({
48
+ inputRecord: createJsonApiDocument('authors', payload),
49
+ simplified: false
50
+ })
51
+ testData.authors.push(result.data)
52
+ }
53
+
54
+ testData.expectedAuthorOrder = [
55
+ testData.authors[3].id,
56
+ testData.authors[0].id,
57
+ testData.authors[1].id,
58
+ testData.authors[2].id
59
+ ]
60
+
61
+ const bookOne = await api.resources.books.post({
62
+ inputRecord: createJsonApiDocument(
63
+ 'books',
64
+ { title: 'Derived Fields Handbook' },
65
+ { author: createRelationship(resourceIdentifier('authors', testData.authors[0].id)) }
66
+ ),
67
+ simplified: false
68
+ })
69
+
70
+ const bookTwo = await api.resources.books.post({
71
+ inputRecord: createJsonApiDocument(
72
+ 'books',
73
+ { title: 'Cursor Patterns' },
74
+ { author: createRelationship(resourceIdentifier('authors', testData.authors[2].id)) }
75
+ ),
76
+ simplified: false
77
+ })
78
+
79
+ testData.books = [bookOne.data, bookTwo.data]
80
+ })
81
+
82
+ async function createEphemeralProjectionApi (resourceName, resourceOptions) {
83
+ const api = new Api({
84
+ name: `${resourceName}-projection-test-api`,
85
+ log: { level: process.env.LOG_LEVEL || 'info' }
86
+ })
87
+
88
+ await api.use(RestApiPlugin, {
89
+ simplifiedApi: true,
90
+ simplifiedTransport: true,
91
+ returnRecordApi: {
92
+ post: true,
93
+ put: true,
94
+ patch: true
95
+ }
96
+ })
97
+ await api.use(QueryProjectionsPlugin)
98
+ await api.use(RestApiKnexPlugin, { knex })
99
+ await api.addResource(resourceName, resourceOptions)
100
+
101
+ return api
102
+ }
103
+
104
+ it('should return query projection fields by default and ignore query projection input on writes', async () => {
105
+ const created = await api.resources.authors.post({
106
+ inputRecord: createJsonApiDocument('authors', {
107
+ first_name: 'Nina',
108
+ last_name: 'Simone',
109
+ full_name: 'Injected Value'
110
+ }),
111
+ simplified: false
112
+ })
113
+
114
+ validateJsonApiStructure(created)
115
+ assert.equal(created.data.attributes.first_name, 'Nina')
116
+ assert.equal(created.data.attributes.last_name, 'Simone')
117
+ assert.equal(created.data.attributes.full_name, 'Nina Simone')
118
+
119
+ const fetched = await api.resources.authors.get({
120
+ id: created.data.id,
121
+ simplified: false
122
+ })
123
+
124
+ validateJsonApiStructure(fetched)
125
+ assert.equal(fetched.data.attributes.full_name, 'Nina Simone')
126
+ })
127
+
128
+ it('should allow query projection fields in sparse fieldsets', async () => {
129
+ const result = await api.resources.authors.get({
130
+ id: testData.authors[0].id,
131
+ queryParams: {
132
+ fields: {
133
+ authors: 'first_name,full_name'
134
+ }
135
+ },
136
+ simplified: false
137
+ })
138
+
139
+ validateJsonApiStructure(result)
140
+ assert.equal(result.data.attributes.first_name, 'Alex')
141
+ assert.equal(result.data.attributes.full_name, 'Alex Carter')
142
+ assert.equal(result.data.attributes.last_name, undefined)
143
+ })
144
+
145
+ it('should expose query projection fields on included resources', async () => {
146
+ const result = await api.resources.books.get({
147
+ id: testData.books[0].id,
148
+ queryParams: {
149
+ include: ['author'],
150
+ fields: {
151
+ books: 'title',
152
+ authors: 'full_name'
153
+ }
154
+ },
155
+ simplified: false
156
+ })
157
+
158
+ validateJsonApiStructure(result)
159
+ assert.ok(Array.isArray(result.included))
160
+
161
+ const includedAuthor = result.included.find((resource) => resource.type === 'authors')
162
+ assert.ok(includedAuthor, 'expected included author resource')
163
+ assert.equal(includedAuthor.attributes.full_name, 'Alex Carter')
164
+ assert.equal(includedAuthor.attributes.first_name, undefined)
165
+ assert.equal(includedAuthor.attributes.last_name, undefined)
166
+ })
167
+
168
+ it('should support stable cursor pagination when sorting by a query projection field', async () => {
169
+ const seenIds = []
170
+ const seenFirstNames = []
171
+ let nextCursor = null
172
+ let iterations = 0
173
+ let firstCursor = null
174
+
175
+ while (iterations < 10) {
176
+ const result = await api.resources.authors.query({
177
+ queryParams: {
178
+ sort: ['full_name'],
179
+ fields: {
180
+ authors: 'first_name'
181
+ },
182
+ page: {
183
+ size: 1,
184
+ ...(nextCursor ? { after: nextCursor } : {})
185
+ }
186
+ },
187
+ simplified: false
188
+ })
189
+
190
+ validateJsonApiStructure(result, true)
191
+ assert.equal(result.data.length, 1)
192
+
193
+ const resource = result.data[0]
194
+ seenIds.push(resource.id)
195
+ seenFirstNames.push(resource.attributes.first_name)
196
+ assert.equal(resource.attributes.full_name, undefined)
197
+
198
+ if (!firstCursor && result.meta?.pagination?.cursor?.next) {
199
+ firstCursor = result.meta.pagination.cursor.next
200
+ }
201
+
202
+ if (!result.meta?.pagination?.hasMore) {
203
+ break
204
+ }
205
+
206
+ nextCursor = result.meta.pagination.cursor.next
207
+ iterations += 1
208
+ }
209
+
210
+ assert.deepEqual(seenIds, testData.expectedAuthorOrder)
211
+ assert.equal(new Set(seenIds).size, seenIds.length, 'cursor pagination should not duplicate records')
212
+ assert.ok(firstCursor?.includes('full_name:'), 'cursor should encode query projection sort field')
213
+ assert.ok(firstCursor?.includes('id:'), 'cursor should append id as deterministic tie-breaker')
214
+ assert.deepEqual(seenFirstNames, ['Aaron', 'Alex', 'Alex', 'Bella'])
215
+ })
216
+
217
+ it('should allow sorting by a normallyHidden query projection field without returning it by default', async () => {
218
+ const result = await api.resources.authors.query({
219
+ queryParams: {
220
+ sort: ['sort_name']
221
+ },
222
+ simplified: false
223
+ })
224
+
225
+ validateJsonApiStructure(result, true)
226
+ assert.deepEqual(
227
+ result.data.map((resource) => resource.id),
228
+ testData.expectedAuthorOrder
229
+ )
230
+ result.data.forEach((resource) => {
231
+ assert.equal(resource.attributes.sort_name, undefined)
232
+ })
233
+ })
234
+
235
+ it('should reject query projection expressions that return plain SQL strings', async () => {
236
+ const stringApi = await createEphemeralProjectionApi('string_projection_authors', {
237
+ schema: {
238
+ id: { type: 'id' },
239
+ first_name: { type: 'string', required: true },
240
+ last_name: { type: 'string', required: true }
241
+ },
242
+ queryFields: {
243
+ full_name: {
244
+ type: 'string',
245
+ select: () => "trim(coalesce(first_name, '') || ' ' || coalesce(last_name, ''))"
246
+ }
247
+ },
248
+ tableName: 'projected_authors_string_expr'
249
+ })
250
+
251
+ await stringApi.resources.string_projection_authors.createKnexTable()
252
+ await knex('projected_authors_string_expr').insert({
253
+ first_name: 'Nina',
254
+ last_name: 'Simone'
255
+ })
256
+
257
+ await assert.rejects(
258
+ () => stringApi.resources.string_projection_authors.query({
259
+ queryParams: {},
260
+ simplified: false
261
+ }),
262
+ /Query field expressions must return knex raw, knex ref, or a knex query builder/
263
+ )
264
+ })
265
+
266
+ it('should reject query projection names that conflict with relationship names', async () => {
267
+ await assert.rejects(
268
+ () => createEphemeralProjectionApi('conflicting_projection_authors', {
269
+ schema: {
270
+ id: { type: 'id' },
271
+ name: { type: 'string', required: true }
272
+ },
273
+ queryFields: {
274
+ books: {
275
+ type: 'string',
276
+ select: ({ knex }) => knex.raw("'X'")
277
+ }
278
+ },
279
+ relationships: {
280
+ books: { type: 'hasMany', target: 'books', foreignKey: 'author_id' }
281
+ },
282
+ tableName: 'projected_authors_conflict'
283
+ }),
284
+ /conflicts with an existing schema/
285
+ )
286
+ })
287
+
288
+ it('should reject query projection names that conflict with computed fields injected by plugins', async () => {
289
+ const labeledApi = new Api({
290
+ name: 'label-projection-conflict-api',
291
+ log: { level: process.env.LOG_LEVEL || 'info' }
292
+ })
293
+
294
+ await labeledApi.use(RestApiPlugin, {
295
+ simplifiedApi: true,
296
+ simplifiedTransport: true
297
+ })
298
+ await labeledApi.use(LabelPlugin)
299
+ await labeledApi.use(QueryProjectionsPlugin)
300
+ await labeledApi.use(RestApiKnexPlugin, { knex })
301
+
302
+ await assert.rejects(
303
+ () => labeledApi.addResource('labeled_projection_authors', {
304
+ schema: {
305
+ id: { type: 'id' },
306
+ name: { type: 'string', required: true }
307
+ },
308
+ queryFields: {
309
+ label: {
310
+ type: 'string',
311
+ select: ({ knex }) => knex.raw("'X'")
312
+ }
313
+ },
314
+ tableName: 'projected_authors_label_conflict'
315
+ }),
316
+ /conflicts with an existing schema, computed, or relationship name/
317
+ )
318
+ })
319
+ })
@@ -330,7 +330,6 @@ describe('Relationship Operations', () => {
330
330
  }
331
331
  }
332
332
  }
333
- console.log('THE NEXT LINE WILL BLOCK THE TEST')
334
333
  await basicApi.resources.books.put({
335
334
  id: bookResult.data.id,
336
335
  inputRecord: putDoc,
@@ -590,7 +589,7 @@ describe('Relationship Operations', () => {
590
589
  assert(publisher.relationships.country)
591
590
  })
592
591
 
593
- it('should handle PUT with partial relationships object', async () => {
592
+ it('should clear omitted relationships when PUT provides a relationships object', async () => {
594
593
  // Create resources
595
594
  const countryDoc = createJsonApiDocument('countries', { name: 'USA', code: 'US' })
596
595
  const countryResult = await basicApi.resources.countries.post({
@@ -640,7 +639,8 @@ describe('Relationship Operations', () => {
640
639
  simplified: false
641
640
  })
642
641
 
643
- // PUT with only publisher relationship (should preserve authors since relationships object is partial)
642
+ // PUT with only publisher relationship. Omitted relationships are cleared
643
+ // once a relationships object is provided for a complete replacement.
644
644
  const putDoc = {
645
645
  data: {
646
646
  type: 'books',