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,620 +0,0 @@
1
- import { describe, it, before, after, beforeEach } 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 {
7
- validateJsonApiStructure,
8
- cleanTables,
9
- createJsonApiDocument,
10
- createRelationship,
11
- createToManyRelationship,
12
- resourceIdentifier,
13
- countRecords
14
- } from './helpers/test-utils.js'
15
- import { createBasicApi } from './fixtures/api-configs.js'
16
-
17
- // Create Knex instance for tests
18
- const knex = knexLib({
19
- client: 'better-sqlite3',
20
- connection: {
21
- filename: ':memory:'
22
- },
23
- useNullAsDefault: true
24
- })
25
-
26
- // API instance that persists across tests
27
- let api
28
- let app
29
- const relationshipCommitEvents = []
30
- const relationshipRollbackEvents = []
31
-
32
- describe('Relationship Endpoints Plugin', () => {
33
- before(async () => {
34
- // Initialize API with the relationships plugin
35
- app = express()
36
- api = await createBasicApi(knex, {
37
- express: {
38
- mountPath: '' // No mount path for this test
39
- },
40
- includeExpress: true
41
- })
42
- api.http.express.mount(app)
43
-
44
- await api.customize({
45
- hooks: {
46
- afterCommit: {
47
- functionName: 'relationship-after-commit-test-tracker',
48
- handler: async ({ context }) => {
49
- if (context.method?.endsWith('Relationship')) {
50
- relationshipCommitEvents.push({
51
- method: context.method,
52
- shouldCommit: context.shouldCommit,
53
- hasTransaction: !!context.transaction
54
- })
55
- }
56
- }
57
- },
58
- afterRollback: {
59
- functionName: 'relationship-after-rollback-test-tracker',
60
- handler: async ({ context }) => {
61
- if (context.method?.endsWith('Relationship')) {
62
- relationshipRollbackEvents.push({
63
- method: context.method,
64
- shouldCommit: context.shouldCommit,
65
- hasTransaction: !!context.transaction
66
- })
67
- }
68
- }
69
- }
70
- }
71
- })
72
- })
73
-
74
- after(async () => {
75
- // Close database connection
76
- await knex.destroy()
77
- })
78
-
79
- describe('Many-to-Many Relationship Endpoints', () => {
80
- let countryId
81
- let book1Id
82
- let author1Id
83
- let author2Id
84
- let author3Id
85
-
86
- beforeEach(async () => {
87
- await cleanTables(knex, [
88
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
89
- ])
90
-
91
- // Create test data
92
- const countryDoc = createJsonApiDocument('countries', { name: 'USA', code: 'US' })
93
- const countryResult = await api.resources.countries.post({
94
- inputRecord: countryDoc,
95
- simplified: false
96
- })
97
- countryId = countryResult.data.id
98
-
99
- // Create a book
100
- const bookDoc = createJsonApiDocument('books',
101
- { title: 'Test Book' },
102
- { country: createRelationship(resourceIdentifier('countries', countryId)) }
103
- )
104
- const bookResult = await api.resources.books.post({
105
- inputRecord: bookDoc,
106
- simplified: false
107
- })
108
- book1Id = bookResult.data.id
109
-
110
- // Create authors
111
- const author1Doc = createJsonApiDocument('authors', { name: 'Author One' })
112
- const author1Result = await api.resources.authors.post({
113
- inputRecord: author1Doc,
114
- simplified: false
115
- })
116
- author1Id = author1Result.data.id
117
-
118
- const author2Doc = createJsonApiDocument('authors', { name: 'Author Two' })
119
- const author2Result = await api.resources.authors.post({
120
- inputRecord: author2Doc,
121
- simplified: false
122
- })
123
- author2Id = author2Result.data.id
124
-
125
- const author3Doc = createJsonApiDocument('authors', { name: 'Author Three' })
126
- const author3Result = await api.resources.authors.post({
127
- inputRecord: author3Doc,
128
- simplified: false
129
- })
130
- author3Id = author3Result.data.id
131
- })
132
-
133
- it('should get empty relationship data for new book', async () => {
134
- const result = await api.resources.books.getRelationship({
135
- id: book1Id,
136
- relationshipName: 'authors'
137
- })
138
-
139
- assert(result.links, 'Should have links')
140
- assert.equal(result.links.self, `/books/${book1Id}/relationships/authors`)
141
- assert.equal(result.links.related, `/books/${book1Id}/authors`)
142
- assert(Array.isArray(result.data), 'Data should be an array')
143
- assert.equal(result.data.length, 0, 'Should have no authors')
144
- })
145
-
146
- it('should add authors to book via POST relationship endpoint', async () => {
147
- // Add authors via relationship endpoint
148
- await api.resources.books.postRelationship({
149
- id: book1Id,
150
- relationshipName: 'authors',
151
- relationshipData: [
152
- resourceIdentifier('authors', author1Id),
153
- resourceIdentifier('authors', author2Id)
154
- ]
155
- })
156
-
157
- // Verify authors were added via getRelationship
158
- const relResult = await api.resources.books.getRelationship({
159
- id: book1Id,
160
- relationshipName: 'authors'
161
- })
162
-
163
- assert.equal(relResult.data.length, 2, 'Should have 2 authors')
164
- assert.deepEqual(relResult.data[0], resourceIdentifier('authors', author1Id))
165
- assert.deepEqual(relResult.data[1], resourceIdentifier('authors', author2Id))
166
-
167
- // Verify pivot table records
168
- const pivotCount = await countRecords(knex, 'basic_book_authors')
169
- assert.equal(pivotCount, 2, 'Should have 2 pivot records')
170
-
171
- // Verify through GET with include
172
- const getResult = await api.resources.books.get({
173
- id: book1Id,
174
- queryParams: {
175
- include: ['authors']
176
- },
177
- simplified: false
178
- })
179
-
180
- assert(getResult.included, 'Should have included data')
181
- assert.equal(getResult.included.length, 2, 'Should include 2 authors')
182
- assert(getResult.data.relationships.authors, 'Should have authors relationship')
183
- assert.equal(getResult.data.relationships.authors.data.length, 2)
184
- })
185
-
186
- it('should get related resources via GET related endpoint', async () => {
187
- // Add authors first
188
- await api.resources.books.postRelationship({
189
- id: book1Id,
190
- relationshipName: 'authors',
191
- relationshipData: [
192
- resourceIdentifier('authors', author1Id),
193
- resourceIdentifier('authors', author2Id)
194
- ]
195
- })
196
-
197
- // Get related resources
198
- const result = await api.resources.books.getRelated({
199
- id: book1Id,
200
- relationshipName: 'authors'
201
- })
202
-
203
- // Verify response
204
- validateJsonApiStructure(result, true) // true for collection
205
- assert.equal(result.data.length, 2, 'Should have 2 authors')
206
- assert.equal(result.data[0].type, 'authors')
207
- assert.equal(result.data[1].type, 'authors')
208
-
209
- const authorNames = result.data.map(a => a.attributes.name).sort()
210
- assert.deepEqual(authorNames, ['Author One', 'Author Two'])
211
- })
212
-
213
- it('should remove specific authors via DELETE relationship endpoint', async () => {
214
- // Add all authors first
215
- await api.resources.books.postRelationship({
216
- id: book1Id,
217
- relationshipName: 'authors',
218
- relationshipData: [
219
- resourceIdentifier('authors', author1Id),
220
- resourceIdentifier('authors', author2Id),
221
- resourceIdentifier('authors', author3Id)
222
- ]
223
- })
224
-
225
- // Verify all were added
226
- let pivotCount = await countRecords(knex, 'basic_book_authors')
227
- assert.equal(pivotCount, 3, 'Should have 3 pivot records')
228
-
229
- // Remove one author
230
- await api.resources.books.deleteRelationship({
231
- id: book1Id,
232
- relationshipName: 'authors',
233
- relationshipData: [
234
- resourceIdentifier('authors', author2Id)
235
- ]
236
- })
237
-
238
- // Verify only two authors remain
239
- const relResult = await api.resources.books.getRelationship({
240
- id: book1Id,
241
- relationshipName: 'authors'
242
- })
243
-
244
- assert.equal(relResult.data.length, 2, 'Should have 2 authors remaining')
245
- assert.deepEqual(relResult.data[0], resourceIdentifier('authors', author1Id))
246
- assert.deepEqual(relResult.data[1], resourceIdentifier('authors', author3Id))
247
-
248
- // Verify pivot table
249
- pivotCount = await countRecords(knex, 'basic_book_authors')
250
- assert.equal(pivotCount, 2, 'Should have 2 pivot records')
251
- })
252
-
253
- it('should handle errors when relationship does not exist', async () => {
254
- await assert.rejects(
255
- api.resources.books.getRelationship({
256
- id: book1Id,
257
- relationshipName: 'nonexistent'
258
- }),
259
- {
260
- message: /Relationship 'nonexistent' not found/
261
- }
262
- )
263
- })
264
-
265
- it('should handle errors when posting to belongsTo relationship', async () => {
266
- await assert.rejects(
267
- api.resources.books.postRelationship({
268
- id: book1Id,
269
- relationshipName: 'country',
270
- relationshipData: [
271
- resourceIdentifier('countries', countryId)
272
- ]
273
- }),
274
- {
275
- message: /Cannot POST to to-one relationship/
276
- }
277
- )
278
- })
279
-
280
- it('should work with inverse relationship (authors to books)', async () => {
281
- // First add the relationship from book side
282
- await api.resources.books.postRelationship({
283
- id: book1Id,
284
- relationshipName: 'authors',
285
- relationshipData: [
286
- resourceIdentifier('authors', author1Id)
287
- ]
288
- })
289
-
290
- // Now check from author side
291
- const relResult = await api.resources.authors.getRelationship({
292
- id: author1Id,
293
- relationshipName: 'books'
294
- })
295
-
296
- assert.equal(relResult.data.length, 1, 'Author should have 1 book')
297
- assert.deepEqual(relResult.data[0], resourceIdentifier('books', book1Id))
298
-
299
- // Get related books
300
- const booksResult = await api.resources.authors.getRelated({
301
- id: author1Id,
302
- relationshipName: 'books'
303
- })
304
-
305
- validateJsonApiStructure(booksResult, true)
306
- assert.equal(booksResult.data.length, 1)
307
- assert.equal(booksResult.data[0].attributes.title, 'Test Book')
308
- })
309
- })
310
-
311
- describe('One-to-Many Relationship Endpoints', () => {
312
- let countryId
313
- let publisherId
314
- let book1Id
315
- let book2Id
316
-
317
- beforeEach(async () => {
318
- await cleanTables(knex, [
319
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
320
- ])
321
-
322
- // Create test data
323
- const countryDoc = createJsonApiDocument('countries', { name: 'USA', code: 'US' })
324
- const countryResult = await api.resources.countries.post({
325
- inputRecord: countryDoc,
326
- simplified: false
327
- })
328
- countryId = countryResult.data.id
329
-
330
- // Create a publisher
331
- const publisherDoc = createJsonApiDocument('publishers',
332
- { name: 'Test Publisher' },
333
- { country: createRelationship(resourceIdentifier('countries', countryId)) }
334
- )
335
- const publisherResult = await api.resources.publishers.post({
336
- inputRecord: publisherDoc,
337
- simplified: false
338
- })
339
- publisherId = publisherResult.data.id
340
-
341
- // Create books
342
- const book1Doc = createJsonApiDocument('books',
343
- { title: 'Book One' },
344
- {
345
- country: createRelationship(resourceIdentifier('countries', countryId)),
346
- publisher: createRelationship(resourceIdentifier('publishers', publisherId))
347
- }
348
- )
349
- const book1Result = await api.resources.books.post({
350
- inputRecord: book1Doc,
351
- simplified: false
352
- })
353
- book1Id = book1Result.data.id
354
-
355
- const book2Doc = createJsonApiDocument('books',
356
- { title: 'Book Two' },
357
- {
358
- country: createRelationship(resourceIdentifier('countries', countryId)),
359
- publisher: createRelationship(resourceIdentifier('publishers', publisherId))
360
- }
361
- )
362
- const book2Result = await api.resources.books.post({
363
- inputRecord: book2Doc,
364
- simplified: false
365
- })
366
- book2Id = book2Result.data.id
367
- })
368
-
369
- it('should get one-to-many relationship data', async () => {
370
- const result = await api.resources.publishers.getRelationship({
371
- id: publisherId,
372
- relationshipName: 'books'
373
- })
374
-
375
- assert(result.links, 'Should have links')
376
- assert.equal(result.links.self, `/publishers/${publisherId}/relationships/books`)
377
- assert.equal(result.links.related, `/publishers/${publisherId}/books`)
378
- assert(Array.isArray(result.data), 'Data should be an array')
379
- assert.equal(result.data.length, 2, 'Should have 2 books')
380
- })
381
-
382
- it('should get related resources for one-to-many', async () => {
383
- const result = await api.resources.publishers.getRelated({
384
- id: publisherId,
385
- relationshipName: 'books'
386
- })
387
-
388
- validateJsonApiStructure(result, true)
389
- assert.equal(result.data.length, 2)
390
- const titles = result.data.map(b => b.attributes.title).sort()
391
- assert.deepEqual(titles, ['Book One', 'Book Two'])
392
- })
393
-
394
- // Note: belongsTo relationships (like 'publisher') are not accessible via relationship endpoints
395
- // They are foreign key fields, not true JSON:API relationships
396
- })
397
-
398
- describe('Relationship Cardinality Validation', () => {
399
- let countryId
400
- let bookId
401
- let authorId
402
-
403
- const assertRelationshipIdValidationError = (error) => {
404
- assert.equal(error.code, 'REST_API_VALIDATION')
405
- assert.match(error.message, /id/i)
406
- return true
407
- }
408
-
409
- beforeEach(async () => {
410
- await cleanTables(knex, [
411
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
412
- ])
413
-
414
- const country = await api.resources.countries.post({
415
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' }),
416
- simplified: false
417
- })
418
- countryId = country.data.id
419
-
420
- const book = await api.resources.books.post({
421
- inputRecord: createJsonApiDocument(
422
- 'books',
423
- { title: 'Cardinality Book' },
424
- { country: createRelationship(resourceIdentifier('countries', countryId)) }
425
- ),
426
- simplified: false
427
- })
428
- bookId = book.data.id
429
-
430
- const author = await api.resources.authors.post({
431
- inputRecord: createJsonApiDocument('authors', { name: 'Author One' }),
432
- simplified: false
433
- })
434
- authorId = author.data.id
435
- })
436
-
437
- it('rejects arrays for to-one relationship PATCH calls', async () => {
438
- await assert.rejects(
439
- api.resources.books.patchRelationship({
440
- id: bookId,
441
- relationshipName: 'country',
442
- relationshipData: [resourceIdentifier('countries', countryId)]
443
- }),
444
- (error) => {
445
- assert.equal(error.code, 'REST_API_VALIDATION')
446
- assert.match(error.message, /single resource identifier or null/)
447
- assert(error.details.fields.includes('data.relationships.country.data'))
448
- return true
449
- }
450
- )
451
- })
452
-
453
- it('rejects scalars for to-many relationship PATCH calls', async () => {
454
- await assert.rejects(
455
- api.resources.books.patchRelationship({
456
- id: bookId,
457
- relationshipName: 'authors',
458
- relationshipData: resourceIdentifier('authors', authorId)
459
- }),
460
- (error) => {
461
- assert.equal(error.code, 'REST_API_VALIDATION')
462
- assert.match(error.message, /array of resource identifiers/)
463
- assert(error.details.fields.includes('data.relationships.authors.data'))
464
- return true
465
- }
466
- )
467
- })
468
-
469
- it('rejects invalid relationship cardinality through Express transport', async () => {
470
- const toOneResponse = await request(app)
471
- .patch(`/books/${bookId}/relationships/country`)
472
- .set('Content-Type', 'application/vnd.api+json')
473
- .set('Accept', 'application/vnd.api+json')
474
- .send({
475
- data: [resourceIdentifier('countries', countryId)]
476
- })
477
-
478
- assert.equal(toOneResponse.status, 422)
479
- assert.match(toOneResponse.body.errors?.[0]?.detail || '', /single resource identifier or null/)
480
-
481
- const toManyResponse = await request(app)
482
- .patch(`/books/${bookId}/relationships/authors`)
483
- .set('Content-Type', 'application/vnd.api+json')
484
- .set('Accept', 'application/vnd.api+json')
485
- .send({
486
- data: resourceIdentifier('authors', authorId)
487
- })
488
-
489
- assert.equal(toManyResponse.status, 422)
490
- assert.match(toManyResponse.body.errors?.[0]?.detail || '', /array of resource identifiers/)
491
- })
492
-
493
- it('rejects malformed relationship identifiers through direct relationship methods', async () => {
494
- await assert.rejects(
495
- api.resources.books.postRelationship({
496
- id: bookId,
497
- relationshipName: 'authors',
498
- relationshipData: [{ type: 'authors' }]
499
- }),
500
- assertRelationshipIdValidationError
501
- )
502
-
503
- await assert.rejects(
504
- api.resources.books.patchRelationship({
505
- id: bookId,
506
- relationshipName: 'country',
507
- relationshipData: { type: 'countries' }
508
- }),
509
- assertRelationshipIdValidationError
510
- )
511
- })
512
-
513
- it('rejects malformed relationship identifiers through Express transport', async () => {
514
- const response = await request(app)
515
- .delete(`/books/${bookId}/relationships/authors`)
516
- .set('Content-Type', 'application/vnd.api+json')
517
- .set('Accept', 'application/vnd.api+json')
518
- .send({
519
- data: [{ type: 'authors' }]
520
- })
521
-
522
- assert.equal(response.status, 422)
523
- assert.match(response.body.errors?.[0]?.detail || '', /id/i)
524
- })
525
- })
526
-
527
- describe('Relationship Transaction Hooks', () => {
528
- let countryId
529
- let secondCountryId
530
- let bookId
531
- let authorId
532
-
533
- beforeEach(async () => {
534
- relationshipCommitEvents.length = 0
535
- relationshipRollbackEvents.length = 0
536
-
537
- await cleanTables(knex, [
538
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
539
- ])
540
-
541
- const country = await api.resources.countries.post({
542
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' }),
543
- simplified: false
544
- })
545
- countryId = country.data.id
546
-
547
- const secondCountry = await api.resources.countries.post({
548
- inputRecord: createJsonApiDocument('countries', { name: 'Canada', code: 'CA' }),
549
- simplified: false
550
- })
551
- secondCountryId = secondCountry.data.id
552
-
553
- const book = await api.resources.books.post({
554
- inputRecord: createJsonApiDocument(
555
- 'books',
556
- { title: 'Hook Book' },
557
- { country: createRelationship(resourceIdentifier('countries', countryId)) }
558
- ),
559
- simplified: false
560
- })
561
- bookId = book.data.id
562
-
563
- const author = await api.resources.authors.post({
564
- inputRecord: createJsonApiDocument('authors', { name: 'Hook Author' }),
565
- simplified: false
566
- })
567
- authorId = author.data.id
568
-
569
- relationshipCommitEvents.length = 0
570
- relationshipRollbackEvents.length = 0
571
- })
572
-
573
- it('runs afterCommit for relationship methods that own their transaction', async () => {
574
- await api.resources.books.postRelationship({
575
- id: bookId,
576
- relationshipName: 'authors',
577
- relationshipData: [resourceIdentifier('authors', authorId)]
578
- })
579
-
580
- await api.resources.books.patchRelationship({
581
- id: bookId,
582
- relationshipName: 'country',
583
- relationshipData: resourceIdentifier('countries', secondCountryId)
584
- })
585
-
586
- await api.resources.books.deleteRelationship({
587
- id: bookId,
588
- relationshipName: 'authors',
589
- relationshipData: [resourceIdentifier('authors', authorId)]
590
- })
591
-
592
- assert.deepEqual(
593
- relationshipCommitEvents.map((event) => event.method),
594
- ['postRelationship', 'patchRelationship', 'deleteRelationship']
595
- )
596
- assert(relationshipCommitEvents.every((event) => event.shouldCommit))
597
- assert(relationshipCommitEvents.every((event) => event.hasTransaction))
598
- assert.equal(relationshipRollbackEvents.length, 0)
599
- })
600
-
601
- it('preserves afterRollback for failed relationship methods that own their transaction', async () => {
602
- await assert.rejects(
603
- api.resources.books.postRelationship({
604
- id: bookId,
605
- relationshipName: 'missing',
606
- relationshipData: []
607
- }),
608
- /Relationship 'missing' not found/
609
- )
610
-
611
- assert.deepEqual(
612
- relationshipRollbackEvents.map((event) => event.method),
613
- ['postRelationship']
614
- )
615
- assert(relationshipRollbackEvents[0].shouldCommit)
616
- assert(relationshipRollbackEvents[0].hasTransaction)
617
- assert.equal(relationshipCommitEvents.length, 0)
618
- })
619
- })
620
- })
@@ -1,56 +0,0 @@
1
- import { after, before, beforeEach, describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import knexLib from 'knex'
4
- import {
5
- createRelationshipIncludeStorageAdapterApi,
6
- seedRelationshipIncludeStorageAdapterApi
7
- } from './fixtures/api-configs.js'
8
- import { cleanTables } from './helpers/test-utils.js'
9
-
10
- const knex = knexLib({
11
- client: 'better-sqlite3',
12
- connection: {
13
- filename: ':memory:'
14
- },
15
- useNullAsDefault: true
16
- })
17
-
18
- describe('Relationship include storage adapters', () => {
19
- let api
20
-
21
- before(async () => {
22
- api = await createRelationshipIncludeStorageAdapterApi(knex)
23
- })
24
-
25
- beforeEach(async () => {
26
- await cleanTables(knex, [
27
- 'uninitialized_bookings',
28
- 'uninitialized_pets'
29
- ])
30
- await seedRelationshipIncludeStorageAdapterApi(knex)
31
- })
32
-
33
- after(async () => {
34
- await knex.destroy()
35
- })
36
-
37
- it('maps sparse fields for an included scope whose adapter is not initialized', async () => {
38
- assert.equal(api.resources.pets.vars.storageAdapter, undefined)
39
-
40
- const result = await api.resources.bookings.get({
41
- id: 41,
42
- queryParams: {
43
- include: ['pet'],
44
- fields: {
45
- bookings: 'reference,petId',
46
- pets: 'firstName'
47
- }
48
- },
49
- simplified: false
50
- })
51
-
52
- const includedPet = result.included.find(resource => resource.type === 'pets')
53
- assert(includedPet)
54
- assert.deepEqual(includedPet.attributes, { firstName: 'Coco' })
55
- })
56
- })