json-rest-api 1.0.29 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,184 +0,0 @@
1
- import { describe, it, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { Api } from 'hooked-api'
4
- import { RestApiPlugin, FastifyPlugin } from '../index.js'
5
- import { getRequestContracts } from '../plugins/core/lib/querying-writing/request-contracts.js'
6
- import {
7
- FakeFastifyApp,
8
- FakeReply,
9
- findFastifyRoute
10
- } from './helpers/fake-fastify.js'
11
-
12
- function resolveSchemaNode (rootSchema, schemaNode) {
13
- if (!schemaNode || !Array.isArray(schemaNode.allOf) || schemaNode.allOf.length !== 1) {
14
- return schemaNode
15
- }
16
-
17
- const ref = schemaNode.allOf[0]?.$ref || ''
18
- if (!ref.startsWith('#/definitions/')) {
19
- return schemaNode
20
- }
21
-
22
- const definitionName = ref.slice('#/definitions/'.length)
23
- return rootSchema?.definitions?.[definitionName] || schemaNode
24
- }
25
-
26
- async function createFastifyConnectorApi () {
27
- const app = new FakeFastifyApp()
28
- const api = new Api({
29
- name: 'fastify-connector-test-api',
30
- log: { level: process.env.LOG_LEVEL || 'silent' }
31
- })
32
-
33
- await api.use(RestApiPlugin, {
34
- simplifiedApi: false,
35
- simplifiedTransport: false
36
- })
37
-
38
- await api.use(FastifyPlugin, {
39
- app,
40
- mountPath: '/api'
41
- })
42
-
43
- await api.addResource('users', {
44
- schema: {
45
- id: { type: 'id' },
46
- name: { type: 'string', required: true }
47
- }
48
- })
49
-
50
- await api.addResource('articles', {
51
- schema: {
52
- id: { type: 'id' },
53
- title: { type: 'string', required: true },
54
- body: { type: 'string' },
55
- author_id: { type: 'id', belongsTo: 'users', as: 'author' },
56
- internal_summary: { type: 'string', computed: true }
57
- }
58
- })
59
-
60
- return { api, app }
61
- }
62
-
63
- describe('Fastify Plugin', () => {
64
- let api
65
- let app
66
-
67
- beforeEach(async () => {
68
- ({ api, app } = await createFastifyConnectorApi())
69
- })
70
-
71
- it('registers Fastify write routes with schema-backed JSON:API body validation', () => {
72
- const postRoute = findFastifyRoute(app, 'POST', '/api/articles')
73
- const putRoute = findFastifyRoute(app, 'PUT', '/api/articles/:id')
74
- const patchRoute = findFastifyRoute(app, 'PATCH', '/api/articles/:id')
75
- const getRoute = findFastifyRoute(app, 'GET', '/api/articles/:id')
76
- const postRelationshipRoute = findFastifyRoute(app, 'POST', '/api/articles/:id/relationships/:relationshipName')
77
- const patchRelationshipRoute = findFastifyRoute(app, 'PATCH', '/api/articles/:id/relationships/:relationshipName')
78
- const deleteRelationshipRoute = findFastifyRoute(app, 'DELETE', '/api/articles/:id/relationships/:relationshipName')
79
- const relationshipContracts = getRequestContracts({
80
- scopeName: 'articles',
81
- schemaInfo: api.resources.articles.vars.schemaInfo,
82
- includeDepthLimit: api.resources.articles.vars.includeDepthLimit,
83
- sortableFields: api.resources.articles.vars.sortableFields
84
- })
85
-
86
- assert.ok(postRoute?.schema?.body, 'POST route should have a body schema')
87
- assert.ok(putRoute?.schema?.body, 'PUT route should have a body schema')
88
- assert.ok(patchRoute?.schema?.body, 'PATCH route should have a body schema')
89
- assert.equal(getRoute?.schema, undefined, 'GET route should not have a body schema')
90
- assert.ok(postRelationshipRoute?.schema?.body, 'POST relationship route should have a body schema')
91
- assert.ok(patchRelationshipRoute?.schema?.body, 'PATCH relationship route should have a body schema')
92
- assert.ok(deleteRelationshipRoute?.schema?.body, 'DELETE relationship route should have a body schema')
93
-
94
- const postDataSchema = resolveSchemaNode(postRoute.schema.body, postRoute.schema.body.properties.data)
95
- const postAttributes = postDataSchema.properties.attributes
96
-
97
- assert.deepEqual(postDataSchema.required, ['type'])
98
- assert.equal(postAttributes.properties.title.type, 'string')
99
- assert.equal(postAttributes.properties.author_id, undefined, 'belongsTo foreign key should not be accepted in attributes')
100
- assert.equal(postAttributes.properties.internal_summary, undefined, 'computed fields should not be accepted in attributes')
101
- assert.equal(postAttributes.properties.id, undefined, 'resource ids should not appear in attributes')
102
- const postRelationships = resolveSchemaNode(postRoute.schema.body, postDataSchema.properties.relationships)
103
- assert.ok(postRelationships.properties.author, 'declared belongsTo relationships should be represented in the transport schema')
104
-
105
- const putDataSchema = resolveSchemaNode(putRoute.schema.body, putRoute.schema.body.properties.data)
106
- assert.ok(putDataSchema.required.includes('id'), 'PUT body schema should require data.id')
107
-
108
- const patchDataSchemaNode = patchRoute.schema.body.properties.data
109
- assert.ok(Array.isArray(patchDataSchemaNode.anyOf), 'PATCH body schema should require at least one writable section')
110
- assert.deepEqual(patchDataSchemaNode.anyOf, [
111
- { required: ['attributes'] },
112
- { required: ['relationships'] }
113
- ])
114
-
115
- assert.equal(postRelationshipRoute.schema.body.properties.data.type, 'array')
116
- assert.ok(Array.isArray(patchRelationshipRoute.schema.body.properties.data.anyOf))
117
- assert.equal(deleteRelationshipRoute.schema.body.properties.data.type, 'array')
118
- assert.deepEqual(
119
- postRelationshipRoute.schema.body,
120
- relationshipContracts.postRelationship.schema.toJsonSchema({
121
- mode: relationshipContracts.postRelationship.mode,
122
- additionalProperties: false
123
- })
124
- )
125
- assert.deepEqual(
126
- patchRelationshipRoute.schema.body,
127
- relationshipContracts.patchRelationship.schema.toJsonSchema({
128
- mode: relationshipContracts.patchRelationship.mode,
129
- additionalProperties: false
130
- })
131
- )
132
- assert.deepEqual(
133
- deleteRelationshipRoute.schema.body,
134
- relationshipContracts.deleteRelationship.schema.toJsonSchema({
135
- mode: relationshipContracts.deleteRelationship.mode,
136
- additionalProperties: false
137
- })
138
- )
139
- })
140
-
141
- it('registers the JSON:API content-type parser and rejects unsupported write content types before the route handler', async () => {
142
- const parser = app.parsers.find((entry) => entry.contentType === 'application/vnd.api+json')
143
- assert.ok(parser, 'Fastify connector should register the JSON:API parser')
144
- assert.deepEqual(parser.options, { parseAs: 'string' })
145
-
146
- const postRoute = findFastifyRoute(app, 'POST', '/api/articles')
147
- const reply = new FakeReply()
148
-
149
- await postRoute.preValidation(
150
- { headers: { 'content-type': 'text/plain' } },
151
- reply
152
- )
153
-
154
- assert.equal(reply.statusCode, 415)
155
- assert.equal(reply.contentType, 'application/vnd.api+json')
156
- assert.deepEqual(reply.payload, {
157
- errors: [{
158
- status: '415',
159
- title: 'Unsupported Media Type',
160
- detail: 'Content-Type must be application/vnd.api+json or application/json'
161
- }]
162
- })
163
-
164
- const multipartReply = new FakeReply()
165
- await postRoute.preValidation(
166
- { headers: { 'content-type': 'multipart/form-data; boundary=test' } },
167
- multipartReply
168
- )
169
-
170
- assert.equal(multipartReply.statusCode, 415)
171
- })
172
-
173
- it('leaves custom routes unschematized when they do not provide route metadata', async () => {
174
- await api.addRoute({
175
- method: 'POST',
176
- path: '/api/custom-endpoint',
177
- handler: async () => ({ ok: true })
178
- })
179
-
180
- const customRoute = findFastifyRoute(app, 'POST', '/api/custom-endpoint')
181
- assert.ok(customRoute)
182
- assert.equal(customRoute.schema, undefined)
183
- })
184
- })
@@ -1,370 +0,0 @@
1
- import { describe, it, before, beforeEach, after } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import knexLib from 'knex'
4
- import { createFieldGettersApi } from './fixtures/api-configs.js'
5
- import { cleanTables } from './helpers/test-utils.js'
6
- import { storageMode } from './helpers/storage-mode.js'
7
-
8
- // Create Knex instance for tests
9
- const knex = knexLib({
10
- client: 'better-sqlite3',
11
- connection: {
12
- filename: ':memory:'
13
- },
14
- useNullAsDefault: true
15
- })
16
-
17
- // API instance that persists across tests
18
- let api
19
-
20
- describe('Field Getters', () => {
21
- before(async () => {
22
- // Initialize API once
23
- api = await createFieldGettersApi(knex)
24
- })
25
-
26
- after(async () => {
27
- // Close database connection to allow tests to exit
28
- await knex.destroy()
29
- })
30
-
31
- describe('Basic Field Transformations', () => {
32
- beforeEach(async () => {
33
- await cleanTables(knex, ['getter_users'])
34
- })
35
-
36
- it('should apply simple getter transformations', async () => {
37
- // Create user with data that needs transformation
38
- const user = await api.resources.users.post({
39
- email: ' USER@EXAMPLE.COM ',
40
- name: ' John Doe ',
41
- phone: '1234567890',
42
- metadata_json: '{"key": "value", "num": 123}',
43
- tags_csv: 'tag1, tag2, tag3'
44
- })
45
-
46
- // Check transformations applied on create
47
- assert.equal(user.email, 'user@example.com')
48
- assert.equal(user.name, 'John Doe')
49
- assert.equal(user.phone, '(123) 456-7890')
50
- assert.deepEqual(user.metadata_json, { key: 'value', num: 123 })
51
- assert.deepEqual(user.tags_csv, ['tag1', 'tag2', 'tag3'])
52
-
53
- // Verify getters apply on fetch too
54
- const fetched = await api.resources.users.get({ id: user.id })
55
- assert.equal(fetched.email, 'user@example.com')
56
- assert.equal(fetched.name, 'John Doe')
57
- assert.equal(fetched.phone, '(123) 456-7890')
58
- assert.deepEqual(fetched.metadata_json, { key: 'value', num: 123 })
59
- assert.deepEqual(fetched.tags_csv, ['tag1', 'tag2', 'tag3'])
60
- })
61
-
62
- it('should handle null and undefined values', async () => {
63
- const user = await api.resources.users.post({
64
- email: null,
65
- phone: null,
66
- metadata_json: null,
67
- tags_csv: null
68
- })
69
-
70
- assert.equal(user.email, undefined) // null?.toLowerCase().trim() returns undefined
71
- assert.equal(user.name, undefined) // omitted nullable field remains unset
72
- assert.equal(user.phone, null) // getter explicitly returns null for falsy values
73
- assert.deepEqual(user.metadata_json, {})
74
- assert.deepEqual(user.tags_csv, [])
75
- })
76
-
77
- it('should handle invalid data gracefully', async () => {
78
- const user = await api.resources.users.post({
79
- email: ' mixed@CASE.com ',
80
- name: ' ',
81
- phone: '123', // Too short
82
- metadata_json: 'invalid json',
83
- tags_csv: ', , empty, , values, '
84
- })
85
-
86
- assert.equal(user.email, 'mixed@case.com')
87
- assert.equal(user.name, '')
88
- assert.equal(user.phone, '123') // Unchanged when can't format
89
- assert.deepEqual(user.metadata_json, {}) // Empty object on parse error
90
- assert.deepEqual(user.tags_csv, ['empty', 'values']) // Filters empty values
91
- })
92
- })
93
-
94
- describe('Getters with Computed Fields', () => {
95
- beforeEach(async () => {
96
- await cleanTables(knex, ['getter_products'])
97
- })
98
-
99
- it('should apply getters before computed fields', async () => {
100
- const product = await api.resources.products.post({
101
- name: 'widget',
102
- description: 'This is a very long product description that should be truncated',
103
- price_str: '100.00',
104
- tax_rate_str: '0.20'
105
- })
106
-
107
- // Getters should transform values
108
- assert.equal(product.name, 'WIDGET')
109
- assert.equal(product.description, 'This is a very long product description that sh...')
110
- assert.equal(typeof product.price_str, 'number')
111
- assert.equal(product.price_str, 100)
112
- assert.equal(product.tax_rate_str, 0.20)
113
-
114
- // Computed field should see getter-transformed values
115
- assert.equal(product.total_price, 120)
116
- })
117
-
118
- it('should work with sparse fieldsets', async () => {
119
- const product = await api.resources.products.post({
120
- name: 'another widget',
121
- description: 'Short desc',
122
- price_str: '50.00',
123
- tax_rate_str: '0.10'
124
- })
125
-
126
- // Fetch with sparse fieldsets
127
- const sparse = await api.resources.products.get({
128
- id: product.id,
129
- queryParams: {
130
- fields: { products: 'name,total_price' }
131
- }
132
- })
133
-
134
- assert.equal(sparse.name, 'ANOTHER WIDGET')
135
- assert.ok(Math.abs(sparse.total_price - 55) < 0.0001, `Expected total_price to be ~55, got ${sparse.total_price}`)
136
- assert.equal(sparse.description, undefined)
137
- assert.equal(sparse.price_str, undefined)
138
- })
139
- })
140
-
141
- describe('Getter Dependencies', () => {
142
- beforeEach(async () => {
143
- await cleanTables(knex, ['getter_formatted'])
144
- })
145
-
146
- it('should apply getters in dependency order', async () => {
147
- const data = await api.resources.formatted_data.post({
148
- step1: ' hello ',
149
- step2: 'world',
150
- step3: 'end'
151
- })
152
-
153
- // Check that getters ran in order
154
- assert.equal(data.step1, 'hello') // Trimmed
155
- assert.equal(data.step2, 'world [step1: hello]') // Sees trimmed step1
156
- assert.equal(data.step3, 'end [step2: world [step1: hello]]') // Sees processed step2
157
- })
158
-
159
- it('should handle missing dependencies gracefully', async () => {
160
- const data = await api.resources.formatted_data.post({
161
- step3: 'only step3'
162
- // step1 and step2 are missing
163
- })
164
-
165
- assert.equal(data.step1, undefined)
166
- assert.equal(data.step2, null)
167
- assert.equal(data.step3, 'only step3 [step2: null]')
168
- })
169
- })
170
-
171
- describe('Async Getters', () => {
172
- beforeEach(async () => {
173
- await cleanTables(knex, ['getter_encrypted'])
174
- })
175
-
176
- it('should support async getter functions', async () => {
177
- // Helper to simulate encryption
178
- const encrypt = (value) => Buffer.from(value).toString('base64')
179
-
180
- const payload = {
181
- secret: encrypt('my-secret-value'),
182
- data: encrypt('sensitive-info')
183
- }
184
-
185
- let id
186
- if (storageMode.isAnyApi()) {
187
- // AnyAPI mode stores records in canonical slots, so use the API to ensure persistence
188
- const created = await api.resources.encrypted_data.post(payload)
189
- id = created.id
190
- } else {
191
- // Legacy mode can insert directly into the backing table for the same effect
192
- const inserted = await knex('getter_encrypted')
193
- .insert(payload)
194
- .returning('id')
195
- .then((rows) => rows)
196
- const first = Array.isArray(inserted) ? inserted[0] : inserted
197
- id = typeof first === 'object' && first !== null ? first.id : first
198
- }
199
-
200
- // Fetch should decrypt via async getters
201
- const record = await api.resources.encrypted_data.get({ id })
202
- assert.equal(record.secret, 'my-secret-value')
203
- assert.equal(record.data, '[encrypted_data] sensitive-info')
204
- })
205
-
206
- it('should handle null values in async getters', async () => {
207
- const record = await api.resources.encrypted_data.post({
208
- secret: null,
209
- data: null
210
- })
211
-
212
- assert.equal(record.secret, null)
213
- assert.equal(record.data, null)
214
- })
215
- })
216
-
217
- describe('Getters in Included Resources', () => {
218
- beforeEach(async () => {
219
- await cleanTables(knex, ['getter_products', 'getter_reviews'])
220
- })
221
-
222
- it('should apply getters to included resources', async () => {
223
- const product = await api.resources.products.post({
224
- name: 'test product',
225
- description: 'A product',
226
- price_str: '99.99',
227
- tax_rate_str: '0'
228
- })
229
-
230
- // Add reviews
231
- await api.resources.reviews.post({
232
- product: product.id,
233
- content: 'Great product!',
234
- rating: 5
235
- })
236
-
237
- await api.resources.reviews.post({
238
- product: product.id,
239
- content: 'Not bad',
240
- rating: 3
241
- })
242
-
243
- // Fetch with includes
244
- const result = await api.resources.products.get({
245
- id: product.id,
246
- queryParams: {
247
- include: ['reviews']
248
- }
249
- })
250
-
251
- // Product getters should apply
252
- assert.equal(result.name, 'TEST PRODUCT')
253
-
254
- // Review getters should apply to included resources
255
- assert.equal(result.reviews.length, 2)
256
- assert.equal(result.reviews[0].content, '[REVIEW] Great product!')
257
- assert.equal(result.reviews[1].content, '[REVIEW] Not bad')
258
- })
259
-
260
- it('should work with sparse fieldsets on included resources', async () => {
261
- const product = await api.resources.products.post({
262
- name: 'another product',
263
- description: 'Description',
264
- price_str: '10',
265
- tax_rate_str: '0'
266
- })
267
-
268
- await api.resources.reviews.post({
269
- product: product.id,
270
- content: 'Review text',
271
- rating: 4
272
- })
273
-
274
- const result = await api.resources.products.get({
275
- id: product.id,
276
- queryParams: {
277
- include: ['reviews'],
278
- fields: {
279
- products: 'name',
280
- reviews: 'content' // Only content, not rating
281
- }
282
- }
283
- })
284
-
285
- assert.equal(result.name, 'ANOTHER PRODUCT')
286
- assert.equal(result.description, undefined)
287
-
288
- assert.equal(result.reviews.length, 1)
289
- assert.equal(result.reviews[0].content, '[REVIEW] Review text')
290
- assert.equal(result.reviews[0].rating, undefined)
291
- })
292
- })
293
-
294
- describe('Getter Error Handling', () => {
295
- it('should handle getter errors gracefully', async () => {
296
- // Add a resource with a failing getter
297
- await api.addResource('error_test', {
298
- schema: {
299
- id: { type: 'id' },
300
- good_field: {
301
- type: 'string',
302
- getter: (value) => value?.toUpperCase()
303
- },
304
- bad_field: {
305
- type: 'string',
306
- getter: (value) => {
307
- throw new Error('Getter failed!')
308
- }
309
- }
310
- },
311
- tableName: 'getter_errors'
312
- })
313
- await api.resources.error_test.createKnexTable()
314
- if (storageMode.isAnyApi()) {
315
- storageMode.registerTable('getter_errors', 'error_test')
316
- }
317
-
318
- const record = await api.resources.error_test.post({
319
- good_field: 'hello',
320
- bad_field: 'world'
321
- })
322
-
323
- // Good field should be transformed
324
- assert.equal(record.good_field, 'HELLO')
325
- // Bad field should keep original value (error logged but not thrown)
326
- assert.equal(record.bad_field, 'world')
327
-
328
- await cleanTables(knex, ['getter_errors'])
329
- })
330
- })
331
-
332
- describe('Circular Dependencies', () => {
333
- it('should detect circular getter dependencies', async () => {
334
- await assert.rejects(
335
- api.addResource('circular_test', {
336
- schema: {
337
- fieldA: {
338
- type: 'string',
339
- getter: (v) => v,
340
- runGetterAfter: ['fieldB']
341
- },
342
- fieldB: {
343
- type: 'string',
344
- getter: (v) => v,
345
- runGetterAfter: ['fieldA']
346
- }
347
- },
348
- tableName: 'circular_test'
349
- }),
350
- /Circular dependency detected/
351
- )
352
- })
353
-
354
- it('should detect unknown getter dependencies', async () => {
355
- await assert.rejects(
356
- api.addResource('unknown_test', {
357
- schema: {
358
- field1: {
359
- type: 'string',
360
- getter: (v) => v,
361
- runGetterAfter: ['nonexistent']
362
- }
363
- },
364
- tableName: 'unknown_test'
365
- }),
366
- /getter dependency 'nonexistent' that does not exist/
367
- )
368
- })
369
- })
370
- })