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