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,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
- })