json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,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
- })