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,932 +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 { createCustomIdPropertyApi } from './fixtures/api-configs.js'
5
- import {
6
- validateJsonApiStructure,
7
- resourceIdentifier,
8
- cleanTables,
9
- countRecords,
10
- createJsonApiDocument,
11
- createRelationship,
12
- createToManyRelationship,
13
- assertResourceAttributes,
14
- assertResourceRelationship
15
- } from './helpers/test-utils.js'
16
- import { storageMode } from './helpers/storage-mode.js'
17
-
18
- // Create Knex instance for tests
19
- const knex = knexLib({
20
- client: 'better-sqlite3',
21
- connection: {
22
- filename: ':memory:'
23
- },
24
- useNullAsDefault: true
25
- })
26
-
27
- // API instance that persists across tests
28
- let api
29
-
30
- describe('Custom idProperty Relationship Operations', () => {
31
- before(async () => {
32
- // Initialize API with custom idProperty for all resources
33
- api = await createCustomIdPropertyApi(knex)
34
- })
35
-
36
- after(async () => {
37
- await knex.destroy()
38
- })
39
-
40
- describe('Basic CRUD with custom idProperty', () => {
41
- beforeEach(async () => {
42
- await cleanTables(knex, [
43
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
44
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
45
- ])
46
- })
47
-
48
- it('should create and retrieve resources with custom ID fields', async () => {
49
- // Create country
50
- const countryDoc = createJsonApiDocument('countries', {
51
- name: 'United States',
52
- code: 'US'
53
- })
54
-
55
- const countryResult = await api.resources.countries.post({
56
- inputRecord: countryDoc
57
- })
58
-
59
- // Verify response has 'id' not 'country_id'
60
- assert(countryResult.data.id, 'Should have id field')
61
- assert(!countryResult.data.attributes.country_id, 'Should not expose country_id in attributes')
62
-
63
- // Verify database has correct column when using legacy storage
64
- if (!storageMode.isAnyApi()) {
65
- const dbCountry = await knex('custom_id_countries').first()
66
- assert(dbCountry.country_id, 'Database should have country_id column')
67
- assert.equal(String(dbCountry.country_id), countryResult.data.id)
68
- }
69
-
70
- // Test GET
71
- const getResult = await api.resources.countries.get({
72
- id: countryResult.data.id
73
- })
74
-
75
- assert.equal(getResult.data.id, countryResult.data.id)
76
- assert.equal(getResult.data.attributes.name, 'United States')
77
- })
78
-
79
- it('should handle PATCH updates returning full record', async () => {
80
- const country = await api.resources.countries.post({
81
- inputRecord: createJsonApiDocument('countries', {
82
- name: 'United States',
83
- code: 'US'
84
- })
85
- })
86
-
87
- const patchResult = await api.resources.countries.patch({
88
- inputRecord: {
89
- data: {
90
- type: 'countries',
91
- id: country.data.id,
92
- attributes: {
93
- name: 'USA'
94
- }
95
- }
96
- }
97
- })
98
-
99
- // With returnRecordApi.patch = 'full', we should get the complete record
100
- validateJsonApiStructure(patchResult, false)
101
- assert.equal(patchResult.data.attributes.name, 'USA')
102
- assert.equal(patchResult.data.attributes.code, 'US') // Original value preserved
103
- })
104
- })
105
-
106
- describe('One-to-One (belongsTo) Relationships with custom idProperty', () => {
107
- beforeEach(async () => {
108
- await cleanTables(knex, [
109
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
110
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
111
- ])
112
- })
113
-
114
- it('should create resource with belongsTo relationship using custom IDs', async () => {
115
- // Create a country first
116
- const countryDoc = createJsonApiDocument('countries', {
117
- name: 'United States',
118
- code: 'US'
119
- })
120
-
121
- const countryResult = await api.resources.countries.post({
122
- inputRecord: countryDoc
123
- })
124
-
125
- // Create publisher with country relationship
126
- const publisherDoc = createJsonApiDocument('publishers',
127
- {
128
- name: 'Random House'
129
- },
130
- {
131
- country: createRelationship(resourceIdentifier('countries', countryResult.data.id))
132
- }
133
- )
134
-
135
- const publisherResult = await api.resources.publishers.post({
136
- inputRecord: publisherDoc
137
- })
138
-
139
- // Verify response
140
- validateJsonApiStructure(publisherResult, false)
141
- assert.equal(publisherResult.data.type, 'publishers')
142
- assertResourceRelationship(publisherResult.data, 'country',
143
- resourceIdentifier('countries', countryResult.data.id))
144
-
145
- // Verify database foreign key uses country_id in legacy mode
146
- if (!storageMode.isAnyApi()) {
147
- const dbPublisher = await knex('custom_id_publishers').first()
148
- assert.equal(String(dbPublisher.country_id), countryResult.data.id)
149
- }
150
-
151
- // Verify through GET with include
152
- const getResult = await api.resources.publishers.get({
153
- id: publisherResult.data.id,
154
- queryParams: {
155
- include: ['country']
156
- }
157
- })
158
-
159
- validateJsonApiStructure(getResult, false)
160
- assert(getResult.included, 'Should have included data')
161
- assert.equal(getResult.included.length, 1)
162
- assert.equal(getResult.included[0].type, 'countries')
163
- assert.equal(getResult.included[0].attributes.name, 'United States')
164
- assert.equal(
165
- getResult.data.relationships.country.links.self,
166
- `/publishers/${publisherResult.data.id}/relationships/country`
167
- )
168
- assert.equal(
169
- getResult.data.relationships.country.links.related,
170
- `/publishers/${publisherResult.data.id}/country`
171
- )
172
- })
173
-
174
- it('should update belongsTo relationship via PATCH', async () => {
175
- // Create two countries
176
- const country1 = await api.resources.countries.post({
177
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
178
- })
179
-
180
- const country2 = await api.resources.countries.post({
181
- inputRecord: createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
182
- })
183
-
184
- // Create publisher with first country
185
- const publisher = await api.resources.publishers.post({
186
- inputRecord: createJsonApiDocument('publishers',
187
- { name: 'Test Publisher' },
188
- { country: createRelationship(resourceIdentifier('countries', country1.data.id)) }
189
- )
190
- })
191
-
192
- // Update to second country
193
- const patchDoc = {
194
- data: {
195
- type: 'publishers',
196
- id: publisher.data.id,
197
- relationships: {
198
- country: createRelationship(resourceIdentifier('countries', country2.data.id))
199
- }
200
- }
201
- }
202
-
203
- const patchResult = await api.resources.publishers.patch({
204
- inputRecord: patchDoc
205
- })
206
-
207
- // Verify we get full record back
208
- assert.equal(patchResult.data.attributes.name, 'Test Publisher')
209
- assertResourceRelationship(patchResult.data, 'country',
210
- resourceIdentifier('countries', country2.data.id))
211
-
212
- // Verify update via GET with include
213
- const updated = await api.resources.publishers.get({
214
- id: publisher.data.id,
215
- queryParams: { include: ['country'] }
216
- })
217
-
218
- assert.equal(updated.included[0].id, country2.data.id)
219
- assert.equal(updated.included[0].attributes.name, 'UK')
220
- })
221
- })
222
-
223
- describe('Many-to-Many Relationships with custom idProperty', () => {
224
- let countryId
225
- let publisherId
226
- let author1Id
227
- let author2Id
228
- let bookId
229
-
230
- beforeEach(async () => {
231
- await cleanTables(knex, [
232
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
233
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
234
- ])
235
-
236
- // Setup test data
237
- const country = await api.resources.countries.post({
238
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
239
- })
240
- countryId = country.data.id
241
-
242
- const publisher = await api.resources.publishers.post({
243
- inputRecord: createJsonApiDocument('publishers',
244
- { name: 'Test Publisher' },
245
- { country: createRelationship(resourceIdentifier('countries', countryId)) }
246
- )
247
- })
248
- publisherId = publisher.data.id
249
-
250
- const author1 = await api.resources.authors.post({
251
- inputRecord: createJsonApiDocument('authors', { name: 'Author One' })
252
- })
253
- author1Id = author1.data.id
254
-
255
- const author2 = await api.resources.authors.post({
256
- inputRecord: createJsonApiDocument('authors', { name: 'Author Two' })
257
- })
258
- author2Id = author2.data.id
259
- })
260
-
261
- it('should create many-to-many relationships with custom IDs', async () => {
262
- // Create book with multiple authors
263
- const bookDoc = createJsonApiDocument('books',
264
- { title: 'Test Book' },
265
- {
266
- country: createRelationship(resourceIdentifier('countries', countryId)),
267
- publisher: createRelationship(resourceIdentifier('publishers', publisherId)),
268
- authors: createToManyRelationship([
269
- resourceIdentifier('authors', author1Id),
270
- resourceIdentifier('authors', author2Id)
271
- ])
272
- }
273
- )
274
-
275
- const bookResult = await api.resources.books.post({
276
- inputRecord: bookDoc
277
- })
278
-
279
- bookId = bookResult.data.id
280
-
281
- // Verify pivot table has correct foreign keys
282
- if (!storageMode.isAnyApi()) {
283
- const pivotRecords = await knex('custom_id_book_authors').select('*')
284
- assert.equal(pivotRecords.length, 2)
285
-
286
- const bookIds = pivotRecords.map((r) => r.book_id)
287
- const authorIds = pivotRecords.map((r) => r.author_id).sort()
288
-
289
- assert(bookIds.every((id) => String(id) === bookId))
290
- assert.deepEqual(authorIds.map(String).sort(), [author1Id, author2Id].sort())
291
- } else {
292
- const pivotCount = await countRecords(knex, 'custom_id_book_authors')
293
- assert.equal(pivotCount, 2)
294
- }
295
-
296
- // Verify through GET with include
297
- const getResult = await api.resources.books.get({
298
- id: bookId,
299
- queryParams: { include: ['authors'] }
300
- })
301
-
302
- assert(getResult.included)
303
- assert.equal(getResult.included.length, 2)
304
- const includedNames = getResult.included.map(a => a.attributes.name).sort()
305
- assert.deepEqual(includedNames, ['Author One', 'Author Two'])
306
- })
307
-
308
- it('should handle PATCH updates to many-to-many relationships', async () => {
309
- // Create book with one author
310
- const book = await api.resources.books.post({
311
- inputRecord: createJsonApiDocument('books',
312
- { title: 'Test Book' },
313
- {
314
- country: createRelationship(resourceIdentifier('countries', countryId)),
315
- authors: createToManyRelationship([resourceIdentifier('authors', author1Id)])
316
- }
317
- )
318
- })
319
-
320
- // Update to have both authors
321
- const patchDoc = {
322
- data: {
323
- type: 'books',
324
- id: book.data.id,
325
- relationships: {
326
- authors: createToManyRelationship([
327
- resourceIdentifier('authors', author1Id),
328
- resourceIdentifier('authors', author2Id)
329
- ])
330
- }
331
- }
332
- }
333
-
334
- const patchResult = await api.resources.books.patch({
335
- inputRecord: patchDoc,
336
- queryParams: {
337
- include: ['authors']
338
- }
339
- })
340
-
341
- // Verify full record returned
342
- assert.equal(patchResult.data.attributes.title, 'Test Book')
343
- // With include, we should get the authors relationship data
344
- assertResourceRelationship(patchResult.data, 'authors',
345
- [resourceIdentifier('authors', author1Id), resourceIdentifier('authors', author2Id)])
346
-
347
- // Verify update
348
- const updated = await api.resources.books.get({
349
- id: book.data.id,
350
- queryParams: { include: ['authors'] }
351
- })
352
-
353
- assert.equal(updated.included.length, 2)
354
- })
355
- })
356
-
357
- describe('Polymorphic Relationships with custom idProperty', () => {
358
- let bookId
359
- let authorId
360
- let publisherId
361
-
362
- beforeEach(async () => {
363
- await cleanTables(knex, [
364
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
365
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
366
- ])
367
-
368
- // Create test resources
369
- const country = await api.resources.countries.post({
370
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
371
- })
372
-
373
- const book = await api.resources.books.post({
374
- inputRecord: createJsonApiDocument('books',
375
- { title: 'Test Book' },
376
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
377
- )
378
- })
379
- bookId = book.data.id
380
-
381
- const author = await api.resources.authors.post({
382
- inputRecord: createJsonApiDocument('authors', { name: 'Test Author' })
383
- })
384
- authorId = author.data.id
385
-
386
- const publisher = await api.resources.publishers.post({
387
- inputRecord: createJsonApiDocument('publishers', { name: 'Test Publisher' })
388
- })
389
- publisherId = publisher.data.id
390
- })
391
-
392
- it('should create polymorphic review for book with custom IDs', async () => {
393
- const review = await api.resources.reviews.post({
394
- inputRecord: createJsonApiDocument('reviews', {
395
- rating: 5,
396
- title: 'Great book!',
397
- content: 'Excellent read.',
398
- reviewer_name: 'John Doe',
399
- reviewable_type: 'books',
400
- reviewable_id: bookId
401
- })
402
- })
403
-
404
- validateJsonApiStructure(review)
405
- assert.equal(review.data.attributes.reviewable_type, 'books')
406
- assert.equal(String(review.data.attributes.reviewable_id), bookId)
407
-
408
- if (!storageMode.isAnyApi()) {
409
- const dbReview = await knex('custom_id_reviews').first()
410
- assert.equal(dbReview.reviewable_type, 'books')
411
- assert.equal(String(dbReview.reviewable_id), bookId)
412
- assert(dbReview.review_id, 'Should have custom review_id column')
413
- }
414
- })
415
-
416
- it('should create polymorphic review for author with custom IDs', async () => {
417
- const review = await api.resources.reviews.post({
418
- inputRecord: createJsonApiDocument('reviews', {
419
- rating: 4,
420
- title: 'Talented author',
421
- content: 'Great writing style.',
422
- reviewer_name: 'Jane Doe',
423
- reviewable_type: 'authors',
424
- reviewable_id: authorId
425
- })
426
- })
427
-
428
- assert.equal(review.data.attributes.reviewable_type, 'authors')
429
- assert.equal(String(review.data.attributes.reviewable_id), authorId)
430
- })
431
-
432
- it('should query polymorphic reviews by type', async () => {
433
- // Create reviews for different types
434
- await api.resources.reviews.post({
435
- inputRecord: createJsonApiDocument('reviews', {
436
- rating: 5,
437
- title: 'Book review',
438
- content: 'Great book',
439
- reviewer_name: 'Reader',
440
- reviewable_type: 'books',
441
- reviewable_id: bookId
442
- })
443
- })
444
-
445
- await api.resources.reviews.post({
446
- inputRecord: createJsonApiDocument('reviews', {
447
- rating: 4,
448
- title: 'Author review',
449
- content: 'Great author',
450
- reviewer_name: 'Fan',
451
- reviewable_type: 'authors',
452
- reviewable_id: authorId
453
- })
454
- })
455
-
456
- // Query only book reviews
457
- const bookReviews = await api.resources.reviews.query({
458
- queryParams: {
459
- filters: { reviewable_type: 'books' }
460
- }
461
- })
462
- assert.equal(bookReviews.data.length, 1, `Expected 1 book review, got ${bookReviews.data.length}`)
463
- assert.equal(bookReviews.data[0].attributes.reviewable_type, 'books')
464
- })
465
- })
466
-
467
- describe('Nested Includes with custom idProperty', () => {
468
- beforeEach(async () => {
469
- await cleanTables(knex, [
470
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
471
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
472
- ])
473
- })
474
-
475
- it('should handle nested includes through custom ID relationships', async () => {
476
- // Create hierarchy: Country -> Publisher -> Book -> Authors
477
- const country = await api.resources.countries.post({
478
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
479
- })
480
-
481
- const publisher = await api.resources.publishers.post({
482
- inputRecord: createJsonApiDocument('publishers',
483
- { name: 'Test Publisher' },
484
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
485
- )
486
- })
487
-
488
- const author = await api.resources.authors.post({
489
- inputRecord: createJsonApiDocument('authors', { name: 'Test Author' })
490
- })
491
-
492
- const book = await api.resources.books.post({
493
- inputRecord: createJsonApiDocument('books',
494
- { title: 'Test Book' },
495
- {
496
- country: createRelationship(resourceIdentifier('countries', country.data.id)),
497
- publisher: createRelationship(resourceIdentifier('publishers', publisher.data.id)),
498
- authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
499
- }
500
- )
501
- })
502
-
503
- // Query with nested includes
504
- const result = await api.resources.books.query({
505
- queryParams: {
506
- include: ['publisher.country', 'authors']
507
- }
508
- })
509
-
510
- validateJsonApiStructure(result, true)
511
- assert(result.included)
512
-
513
- // Find each type in included
514
- const includedPublisher = result.included.find(r => r.type === 'publishers')
515
- const includedCountry = result.included.find(r => r.type === 'countries')
516
- const includedAuthor = result.included.find(r => r.type === 'authors')
517
-
518
- assert(includedPublisher, 'Should include publisher')
519
- assert(includedCountry, 'Should include country')
520
- assert(includedAuthor, 'Should include author')
521
-
522
- // Verify nested relationship
523
- assert.equal(includedPublisher.relationships.country.data.id, country.data.id)
524
- })
525
-
526
- it('should handle 3+ levels of nested includes', async () => {
527
- // Create deeper hierarchy
528
- const country = await api.resources.countries.post({
529
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
530
- })
531
-
532
- const author = await api.resources.authors.post({
533
- inputRecord: createJsonApiDocument('authors',
534
- { name: 'Test Author' },
535
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
536
- )
537
- })
538
-
539
- const publisher = await api.resources.publishers.post({
540
- inputRecord: createJsonApiDocument('publishers',
541
- { name: 'Test Publisher' },
542
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
543
- )
544
- })
545
-
546
- const book = await api.resources.books.post({
547
- inputRecord: createJsonApiDocument('books',
548
- { title: 'Test Book' },
549
- {
550
- country: createRelationship(resourceIdentifier('countries', country.data.id)),
551
- publisher: createRelationship(resourceIdentifier('publishers', publisher.data.id)),
552
- authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
553
- }
554
- )
555
- })
556
-
557
- // Query with 3 levels: books -> authors -> country
558
- const result = await api.resources.books.query({
559
- queryParams: {
560
- include: ['authors.country', 'publisher.country']
561
- }
562
- })
563
-
564
- validateJsonApiStructure(result, true)
565
- assert(result.included)
566
-
567
- const types = result.included.map(r => r.type)
568
- assert(types.includes('authors'), 'Should include authors')
569
- assert(types.includes('publishers'), 'Should include publishers')
570
- assert(types.includes('countries'), 'Should include countries')
571
- })
572
- })
573
-
574
- describe('Include with Sparse Fieldsets and custom idProperty', () => {
575
- const testData = {}
576
-
577
- beforeEach(async () => {
578
- await cleanTables(knex, [
579
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
580
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
581
- ])
582
-
583
- // Create test data
584
- const country = await api.resources.countries.post({
585
- inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
586
- })
587
- testData.country = country.data
588
-
589
- const publisher = await api.resources.publishers.post({
590
- inputRecord: createJsonApiDocument('publishers',
591
- { name: 'Test Publisher' },
592
- { country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
593
- )
594
- })
595
- testData.publisher = publisher.data
596
-
597
- const author = await api.resources.authors.post({
598
- inputRecord: createJsonApiDocument('authors', { name: 'Test Author', biography: 'A great author' })
599
- })
600
- testData.author = author.data
601
-
602
- const book = await api.resources.books.post({
603
- inputRecord: createJsonApiDocument('books',
604
- { title: 'Test Book', isbn: '1234567890', pages: 300 },
605
- {
606
- country: createRelationship(resourceIdentifier('countries', testData.country.id)),
607
- publisher: createRelationship(resourceIdentifier('publishers', testData.publisher.id)),
608
- authors: createToManyRelationship([resourceIdentifier('authors', testData.author.id)])
609
- }
610
- )
611
- })
612
- testData.book = book.data
613
- })
614
-
615
- it('should combine sparse fieldsets with includes using custom IDs', async () => {
616
- const result = await api.resources.books.get({
617
- id: testData.book.id,
618
- queryParams: {
619
- include: ['publisher', 'authors'],
620
- fields: {
621
- books: 'title',
622
- publishers: 'name',
623
- authors: 'name' // Exclude biography
624
- }
625
- }
626
- })
627
-
628
- // Check main resource has only requested fields
629
- assert(result.data.attributes.title)
630
- assert(!result.data.attributes.isbn)
631
- assert(!result.data.attributes.pages)
632
-
633
- // Check included resources have only requested fields
634
- const publisher = result.included.find(r => r.type === 'publishers')
635
- assert(publisher.attributes.name)
636
- assert(publisher.id) // ID is always included
637
-
638
- const author = result.included.find(r => r.type === 'authors')
639
- assert(author.attributes.name)
640
- assert(!author.attributes.biography)
641
- assert(author.id)
642
- })
643
- })
644
-
645
- describe('Include Depth Validation with custom idProperty', () => {
646
- beforeEach(async () => {
647
- await cleanTables(knex, [
648
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
649
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
650
- ])
651
- })
652
-
653
- it('should enforce include depth limits', async () => {
654
- // Create deep hierarchy
655
- const country = await api.resources.countries.post({
656
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
657
- })
658
-
659
- const publisher = await api.resources.publishers.post({
660
- inputRecord: createJsonApiDocument('publishers',
661
- { name: 'Test Publisher' },
662
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
663
- )
664
- })
665
-
666
- const author = await api.resources.authors.post({
667
- inputRecord: createJsonApiDocument('authors',
668
- { name: 'Test Author' },
669
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
670
- )
671
- })
672
-
673
- const book = await api.resources.books.post({
674
- inputRecord: createJsonApiDocument('books',
675
- { title: 'Test Book' },
676
- {
677
- country: createRelationship(resourceIdentifier('countries', country.data.id)),
678
- publisher: createRelationship(resourceIdentifier('publishers', publisher.data.id)),
679
- authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
680
- }
681
- )
682
- })
683
-
684
- // Test depth limit (default is 3)
685
- // This should work: books -> authors -> country (depth 2)
686
- const validDepth = await api.resources.books.query({
687
- queryParams: {
688
- include: ['authors.country']
689
- }
690
- })
691
- assert(validDepth.data)
692
-
693
- // This should fail: trying to go too deep (if we had deeper relationships)
694
- // Since our test data doesn't have 4+ levels, we test that 3 levels work
695
- const maxDepth = await api.resources.books.query({
696
- queryParams: {
697
- include: ['publisher.country', 'authors.country']
698
- }
699
- })
700
- assert(maxDepth.data)
701
- })
702
- })
703
-
704
- describe('Complex Scenarios with custom idProperty', () => {
705
- beforeEach(async () => {
706
- await cleanTables(knex, [
707
- 'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
708
- 'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
709
- ])
710
- })
711
-
712
- it('should handle complex queries with filters, includes, and custom IDs', async () => {
713
- // Create multiple countries
714
- const usa = await api.resources.countries.post({
715
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
716
- })
717
-
718
- const uk = await api.resources.countries.post({
719
- inputRecord: createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
720
- })
721
-
722
- // Create publishers in different countries
723
- const usPublisher = await api.resources.publishers.post({
724
- inputRecord: createJsonApiDocument('publishers',
725
- { name: 'US Publisher' },
726
- { country: createRelationship(resourceIdentifier('countries', usa.data.id)) }
727
- )
728
- })
729
-
730
- const ukPublisher = await api.resources.publishers.post({
731
- inputRecord: createJsonApiDocument('publishers',
732
- { name: 'UK Publisher' },
733
- { country: createRelationship(resourceIdentifier('countries', uk.data.id)) }
734
- )
735
- })
736
-
737
- // Create authors
738
- const author1 = await api.resources.authors.post({
739
- inputRecord: createJsonApiDocument('authors', { name: 'Author One' })
740
- })
741
-
742
- const author2 = await api.resources.authors.post({
743
- inputRecord: createJsonApiDocument('authors', { name: 'Author Two' })
744
- })
745
-
746
- // Create books
747
- await api.resources.books.post({
748
- inputRecord: createJsonApiDocument('books',
749
- { title: 'US Book 1' },
750
- {
751
- country: createRelationship(resourceIdentifier('countries', usa.data.id)),
752
- publisher: createRelationship(resourceIdentifier('publishers', usPublisher.data.id)),
753
- authors: createToManyRelationship([resourceIdentifier('authors', author1.data.id)])
754
- }
755
- )
756
- })
757
-
758
- await api.resources.books.post({
759
- inputRecord: createJsonApiDocument('books',
760
- { title: 'UK Book 1' },
761
- {
762
- country: createRelationship(resourceIdentifier('countries', uk.data.id)),
763
- publisher: createRelationship(resourceIdentifier('publishers', ukPublisher.data.id)),
764
- authors: createToManyRelationship([
765
- resourceIdentifier('authors', author1.data.id),
766
- resourceIdentifier('authors', author2.data.id)
767
- ])
768
- }
769
- )
770
- })
771
-
772
- // Complex query: US books with their publishers and authors
773
- const result = await api.resources.books.query({
774
- queryParams: {
775
- filters: { country: usa.data.id },
776
- include: ['publisher.country', 'authors'],
777
- sort: ['title']
778
- }
779
- })
780
-
781
- assert.equal(result.data.length, 1)
782
- assert.equal(result.data[0].attributes.title, 'US Book 1')
783
-
784
- // Verify includes
785
- const includedTypes = result.included.map(r => r.type)
786
- assert(includedTypes.includes('publishers'))
787
- assert(includedTypes.includes('countries'))
788
- assert(includedTypes.includes('authors'))
789
- })
790
-
791
- it('should handle bulk operations with custom IDs', async () => {
792
- const country = await api.resources.countries.post({
793
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
794
- })
795
-
796
- // Create multiple authors
797
- const authors = []
798
- for (let i = 1; i <= 3; i++) {
799
- const author = await api.resources.authors.post({
800
- inputRecord: createJsonApiDocument('authors', { name: `Author ${i}` })
801
- })
802
- authors.push(author.data)
803
- }
804
-
805
- // Create book with all authors
806
- const book = await api.resources.books.post({
807
- inputRecord: createJsonApiDocument('books',
808
- { title: 'Multi-author Book' },
809
- {
810
- country: createRelationship(resourceIdentifier('countries', country.data.id)),
811
- authors: createToManyRelationship(
812
- authors.map(a => resourceIdentifier('authors', a.id))
813
- )
814
- }
815
- )
816
- })
817
-
818
- // Verify all relationships were created
819
- const bookWithAuthors = await api.resources.books.get({
820
- id: book.data.id,
821
- queryParams: { include: ['authors'] }
822
- })
823
-
824
- assert.equal(bookWithAuthors.included.length, 3)
825
-
826
- // Verify pivot table
827
- const pivotCount = await countRecords(knex, 'custom_id_book_authors')
828
- assert.equal(pivotCount, 3)
829
- })
830
-
831
- it('should handle DELETE operations with custom IDs', async () => {
832
- const country = await api.resources.countries.post({
833
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
834
- })
835
-
836
- const author = await api.resources.authors.post({
837
- inputRecord: createJsonApiDocument('authors', { name: 'Test Author' })
838
- })
839
-
840
- const book = await api.resources.books.post({
841
- inputRecord: createJsonApiDocument('books',
842
- { title: 'Test Book' },
843
- {
844
- country: createRelationship(resourceIdentifier('countries', country.data.id)),
845
- authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
846
- }
847
- )
848
- })
849
-
850
- // Delete the book
851
- await api.resources.books.delete({ id: book.data.id })
852
-
853
- // Verify book is deleted
854
- try {
855
- await api.resources.books.get({ id: book.data.id })
856
- assert.fail('Should have thrown error')
857
- } catch (error) {
858
- assert(error.message.includes('not found') || error.message.includes('404'))
859
- }
860
-
861
- // The REST API correctly does NOT cascade delete pivot records
862
- // This is intentional - automatic cascade deletion would be dangerous
863
- // Pivot records remain and should be cleaned up by application logic if needed
864
- const pivotCount = await countRecords(knex, 'custom_id_book_authors')
865
- assert.equal(pivotCount, 1, 'Pivot record should still exist')
866
-
867
- // Verify author still exists
868
- const authorStillExists = await api.resources.authors.get({ id: author.data.id })
869
- assert(authorStillExists.data)
870
- })
871
-
872
- it('should handle PUT operations with custom IDs', async () => {
873
- const country = await api.resources.countries.post({
874
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
875
- })
876
-
877
- const publisher = await api.resources.publishers.post({
878
- inputRecord: createJsonApiDocument('publishers',
879
- { name: 'Original Publisher' },
880
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
881
- )
882
- })
883
-
884
- // PUT to completely replace the publisher
885
- const putResult = await api.resources.publishers.put({
886
- inputRecord: {
887
- data: {
888
- type: 'publishers',
889
- id: publisher.data.id,
890
- attributes: {
891
- name: 'Replaced Publisher'
892
- },
893
- relationships: {
894
- country: createRelationship(resourceIdentifier('countries', country.data.id))
895
- }
896
- }
897
- }
898
- })
899
-
900
- // Verify full record returned
901
- validateJsonApiStructure(putResult, false)
902
- assert.equal(putResult.data.attributes.name, 'Replaced Publisher')
903
- assertResourceRelationship(putResult.data, 'country',
904
- resourceIdentifier('countries', country.data.id))
905
- })
906
-
907
- it('should reject PUT when an existing persisted attribute is omitted', async () => {
908
- const country = await api.resources.countries.post({
909
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
910
- })
911
-
912
- await assert.rejects(
913
- async () => api.resources.countries.put({
914
- inputRecord: {
915
- data: {
916
- type: 'countries',
917
- id: country.data.id,
918
- attributes: {
919
- name: 'Renamed Country'
920
- }
921
- }
922
- }
923
- }),
924
- (error) => {
925
- assert.equal(error.message, 'PUT request omits persisted fields that already have stored values')
926
- assert(error.details.fields.includes('data.attributes.code'))
927
- return true
928
- }
929
- )
930
- })
931
- })
932
- })