json-rest-api 1.0.29 → 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 (292) 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 +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} +74 -30
  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 -177
  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 +48 -317
  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 +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -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 +179 -194
  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 +100 -290
  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 +93 -38
  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 +128 -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 -2485
  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 -1077
  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 -395
  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 -148
  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 +36 -68
  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 +37 -81
  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 -1247
  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 -148
  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 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  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-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,748 +0,0 @@
1
- # Effects of PUT and PATCH on Related Data
2
-
3
- ## Understanding Update Operations
4
-
5
- When updating resources through the REST API, it's crucial to understand how PUT and PATCH operations affect related data. This chapter explores the differences between these operations and their impact on all types of relationships.
6
-
7
- First, let's define and create some resources:
8
-
9
- ```javascript
10
- // Define publishers resource
11
- await api.addResource('publishers', {
12
- schema: {
13
- name: { type: 'string', required: true, max: 255 },
14
- country: { type: 'string', required: true, max: 100 }
15
- },
16
- relationships: {
17
- books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
18
- }
19
- });
20
- await api.resources.publishers.createKnexTable();
21
-
22
- // Define authors resource
23
- await api.addResource('authors', {
24
- schema: {
25
- name: { type: 'string', required: true, max: 100 },
26
- surname: { type: 'string', required: true, max: 100 },
27
- birth_year: { type: 'number', required: true }
28
- },
29
- relationships: {
30
- books: { type: 'hasMany', target: 'books', foreignKey: 'author_id' }
31
- }
32
- });
33
- await api.resources.authors.createKnexTable();
34
-
35
- // Define genres resource
36
- await api.addResource('genres', {
37
- schema: {
38
- name: { type: 'string', required: true, max: 100, unique: true }
39
- }
40
- });
41
- await api.resources.genres.createKnexTable();
42
-
43
- // Define books resource with belongsTo and many-to-many relationships
44
- await api.addResource('books', {
45
- schema: {
46
- title: { type: 'string', required: true, max: 255 },
47
- isbn: { type: 'string', required: true, max: 13, unique: true },
48
- published_year: { type: 'number', required: true },
49
- page_count: { type: 'number', required: true },
50
- author_id: { type: 'id', belongsTo: 'authors', as: 'author', required: true },
51
- publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
52
- },
53
- relationships: {
54
- // Many-to-many relationship with genres
55
- genres: {
56
- type: 'manyToMany',
57
- through: 'book_genres',
58
- foreignKey: 'book_id',
59
- otherKey: 'genre_id'
60
- }
61
- }
62
- });
63
- await api.resources.books.createKnexTable();
64
-
65
- // Define the pivot table for book-genre relationships
66
- await api.addResource('book_genres', {
67
- schema: {
68
- book_id: { type: 'id', required: true },
69
- genre_id: { type: 'id', required: true },
70
- created_at: { type: 'datetime', default: 'now' },
71
- primary_genre: { type: 'boolean', default: false }
72
- }
73
- });
74
- await api.resources.book_genres.createKnexTable();
75
- ```
76
-
77
- Let's create a comprehensive dataset that includes all relationship types. We'll use this same dataset throughout the chapter to demonstrate how each operation affects the data.
78
-
79
- Please note that each one of the following sections in this guide will expect this whole dataset to be freshly added:
80
-
81
- ```javascript
82
- // Create publishers
83
- const penguinPublisher = await api.resources.publishers.post({
84
- name: 'Penguin Random House',
85
- country: 'USA'
86
- });
87
- // Returns: { id: 1, name: 'Penguin Random House', country: 'USA' }
88
-
89
- const harperPublisher = await api.resources.publishers.post({
90
- name: 'HarperCollins',
91
- country: 'USA'
92
- });
93
- // Returns: { id: 2, name: 'HarperCollins', country: 'USA' }
94
-
95
- // Create authors
96
- const tolkien = await api.resources.authors.post({
97
- name: 'J.R.R.',
98
- surname: 'Tolkien',
99
- birth_year: 1892
100
- });
101
- // Returns: { id: 1, name: 'J.R.R.', surname: 'Tolkien', birth_year: 1892 }
102
-
103
- const orwell = await api.resources.authors.post({
104
- name: 'George',
105
- surname: 'Orwell',
106
- birth_year: 1903
107
- });
108
- // Returns: { id: 2, name: 'George', surname: 'Orwell', birth_year: 1903 }
109
-
110
- // Create genres
111
- const fantasyGenre = await api.resources.genres.post({
112
- name: 'Fantasy'
113
- });
114
- // Returns: { id: 1, name: 'Fantasy' }
115
-
116
- const adventureGenre = await api.resources.genres.post({
117
- name: 'Adventure'
118
- });
119
- // Returns: { id: 2, name: 'Adventure' }
120
-
121
- const classicGenre = await api.resources.genres.post({
122
- name: 'Classic'
123
- });
124
- // Returns: { id: 3, name: 'Classic' }
125
-
126
- const dystopianGenre = await api.resources.genres.post({
127
- name: 'Dystopian'
128
- });
129
- // Returns: { id: 4, name: 'Dystopian' }
130
-
131
- // Create a book with all relationships
132
- const hobbitBook = await api.resources.books.post({
133
- title: 'The Hobbit',
134
- isbn: '9780547928227',
135
- published_year: 1937,
136
- page_count: 310,
137
- author_id: tolkien.id,
138
- publisher_id: penguinPublisher.id,
139
- genres: [ fantasyGenre.id, adventureGenre.id, classicGenre.id ]
140
- });
141
-
142
- // console.log('hobbitBook:', inspect(hobbitBook));
143
-
144
- // Create more books by the same author (to demonstrate hasMany relationship)
145
- const lotrBook = await api.resources.books.post({
146
- title: 'The Lord of the Rings',
147
- isbn: '9780544003415',
148
- published_year: 1954,
149
- page_count: 1216,
150
- author_id: tolkien.id,
151
- publisher_id: penguinPublisher.id,
152
- genres: [ fantasyGenre.id, adventureGenre.id ]
153
-
154
- });
155
-
156
- const silmarillionBook = await api.resources.books.post({
157
- title: 'The Silmarillion',
158
- isbn: '9780544338012',
159
- published_year: 1977,
160
- page_count: 365,
161
- author_id: tolkien.id,
162
- publisher_id: penguinPublisher.id,
163
- genres: [ fantasyGenre.id ]
164
-
165
- });
166
- ```
167
-
168
- ### Our Complete Dataset
169
-
170
- After running the setup code above, we have:
171
-
172
- | Table | Records | Relationships |
173
- |-------|---------|---------------|
174
- | **publishers** | 2 records | Each has books (hasMany) |
175
- | **authors** | 2 records | Each has books (hasMany) |
176
- | **genres** | 4 records | - |
177
- | **books** | 3 records | • The Hobbit: belongsTo Tolkien & Penguin, has 3 genres<br>• LOTR: belongsTo Tolkien & Penguin, has 2 genres<br>• Silmarillion: belongsTo Tolkien & Penguin, has 1 genre |
178
- | **book_genres** | 6 records | Pivot table linking books to genres |
179
-
180
- ### Viewing the Current State
181
-
182
- ```javascript
183
- // Fetch The Hobbit with all relationships
184
- const currentBook = await api.resources.books.get({
185
- id: hobbitBook.id,
186
- queryParams: {
187
- include: ['author', 'publisher', 'genres']
188
- }
189
- });
190
-
191
- console.log('Current book state:', currentBook);
192
- ```
193
-
194
- ## PUT Operations: Complete Replacement
195
-
196
- **Important**: Each example in this section starts with the full dataset created above. The effects shown are what happens when you run that specific PUT operation on the original data.
197
-
198
- ### Example 1: PUT with All Relationships Specified
199
-
200
- ```javascript
201
- // Starting with our full dataset from above
202
- await api.resources.books.put({
203
- id: hobbitBook.id,
204
- title: 'The Hobbit: An Unexpected Journey',
205
- isbn: '9780547928227',
206
- published_year: 1937,
207
- page_count: 320,
208
- author_id: tolkien.id, // Same author
209
- publisher_id: harperPublisher.id, // Changed publisher
210
- genres: [
211
- fantasyGenre.id, // Kept Fantasy
212
- adventureGenre.id, // Kept Adventure
213
- dystopianGenre.id // Added Dystopian
214
- // Classic genre removed!
215
- ]
216
- });
217
- ```
218
-
219
- From now on, I will assume that we do this after every call:
220
-
221
- ```javascript
222
- const currentBookAfter = await api.resources.books.get({
223
- id: hobbitBook.id,
224
- queryParams: {
225
- include: ['author', 'publisher', 'genres']
226
- }
227
- });
228
-
229
- console.log('Book state AFTER the change:', currentBookAfter);
230
- ```
231
-
232
- **Effects on the database:**
233
-
234
- | Table | Changes |
235
- |-------|---------|
236
- | **books** | • title: Updated<br>• page_count: Changed from 310 to 320<br>• author_id: Unchanged (still Tolkien)<br>• publisher_id: Changed from 1 (Penguin) to 2 (Harper) |
237
- | **book_genres** | • Fantasy record: **PRESERVED** (with original created_at)<br>• Adventure record: **PRESERVED** (with original created_at)<br>• Classic record: **DELETED**<br>• Dystopian record: **CREATED** |
238
- | **Other author's books** | **NO CHANGES** - LOTR and Silmarillion still exist |
239
- | **publishers** | **NO CHANGES** - Both publishers still exist |
240
- | **authors** | **NO CHANGES** - Both authors still exist |
241
- | **genres** | **NO CHANGES** - All 4 genres still exist |
242
-
243
- The PUT operation follows the philosophy of **complete resource replacement**. When you PUT a resource, you're saying "replace the entire current state with exactly what I'm sending." This means:
244
- - All fields you send are updated to the new values
245
- - All fields you DON'T send are cleared (set to NULL or their defaults)
246
- - For many-to-many relationships, the system intelligently syncs: it keeps matching records (preserving their metadata), removes missing ones, and adds new ones
247
-
248
- **Expected result**:
249
-
250
- ```text
251
- Book state BEFORE the change: {
252
- id: '1',
253
- title: 'The Hobbit',
254
- isbn: '9780547928227',
255
- published_year: 1937,
256
- page_count: 310,
257
- genres_ids: [ '1', '2', '3' ],
258
- genres: [
259
- { id: '1', name: 'Fantasy' },
260
- { id: '2', name: 'Adventure' },
261
- { id: '3', name: 'Classic' }
262
- ],
263
- author_id: '1',
264
- author: {
265
- id: '1',
266
- name: 'J.R.R.',
267
- surname: 'Tolkien',
268
- birth_year: 1892,
269
- books_ids: []
270
- },
271
- publisher_id: '1',
272
- publisher: {
273
- id: '1',
274
- name: 'Penguin Random House',
275
- country: 'USA',
276
- books_ids: []
277
- }
278
- }
279
- Book state AFTER the change: {
280
- id: '1',
281
- title: 'The Hobbit: An Unexpected Journey',
282
- isbn: '9780547928227',
283
- published_year: 1937,
284
- page_count: 320,
285
- genres_ids: [ '1', '2', '4' ],
286
- genres: [
287
- { id: '1', name: 'Fantasy' },
288
- { id: '2', name: 'Adventure' },
289
- { id: '4', name: 'Dystopian' }
290
- ],
291
- author_id: '1',
292
- author: {
293
- id: '1',
294
- name: 'J.R.R.',
295
- surname: 'Tolkien',
296
- birth_year: 1892,
297
- books_ids: []
298
- },
299
- publisher_id: '2',
300
- publisher: { id: '2', name: 'HarperCollins', country: 'USA', books_ids: [] }
301
- }
302
- ```
303
- ### Example 2: PUT with Missing Relationships
304
-
305
- ```javascript
306
- // Starting with our full dataset from above
307
- await api.resources.books.put({
308
- id: hobbitBook.id,
309
- title: 'The Hobbit - Revised',
310
- isbn: '9780547928227',
311
- published_year: 1937,
312
- page_count: 310,
313
- author_id: tolkien.id
314
- // publisher_id and genres NOT included!
315
- });
316
- ```
317
- **Result: request rejected**
318
-
319
- PUT now fails fast when you omit a persisted field that already has a stored value. Since this book already has a `publisher_id`, leaving it out would silently discard data, so the API rejects the request instead of clearing it.
320
-
321
- Relationship collections still follow replacement semantics when you provide a `relationships` object. The rejection in this example happens because `publisher_id` is an existing stored belongsTo value that was omitted entirely.
322
-
323
- **Expected output**
324
-
325
- ```text
326
- RestApiValidationError: PUT request omits persisted fields that already have stored values
327
- fields: [ 'data.attributes.publisher_id' ]
328
- ```
329
-
330
- ### Example 3: PUT with Explicit Nulls
331
-
332
- ```javascript
333
- // Starting with our full dataset from above
334
- await api.resources.books.put({
335
- id: hobbitBook.id,
336
- title: 'The Hobbit - Standalone',
337
- isbn: '9780547928227',
338
- published_year: 1937,
339
- page_count: 310,
340
- author_id: tolkien.id, // Required field, cannot be null
341
- publisher_id: null, // Explicitly clearing publisher
342
- genres: [] // Explicitly clearing all genres
343
- });
344
- ```
345
-
346
- **Effects on the database:**
347
-
348
- | Table | Changes |
349
- |-------|---------|
350
- | **books** | • title: Updated<br>• author_id: Remains 1 (required field)<br>• publisher_id: **SET TO NULL** |
351
- | **book_genres** | **ALL 3 RECORDS DELETED** |
352
- | **Other books** | **NO CHANGES** |
353
-
354
- This example shows PUT with **explicit nulls and empty arrays**. Explicit clearing is still allowed when the schema permits it, but it must be intentional. This reinforces that PUT is about **complete state replacement**:
355
- - You must include ALL data you want to keep
356
- - Existing stored values are not cleared by omission; omission is rejected
357
- - Explicit `null` / `[]` is how you clear data intentionally
358
- - Required fields (like `author_id`) must always be provided and cannot be null
359
-
360
-
361
- **Expected output**:
362
-
363
- ```text
364
- Book state BEFORE the change: {
365
- id: '1',
366
- title: 'The Hobbit',
367
- isbn: '9780547928227',
368
- published_year: 1937,
369
- page_count: 310,
370
- genres_ids: [ '1', '2', '3' ],
371
- genres: [
372
- { id: '1', name: 'Fantasy' },
373
- { id: '2', name: 'Adventure' },
374
- { id: '3', name: 'Classic' }
375
- ],
376
- author_id: '1',
377
- author: {
378
- id: '1',
379
- name: 'J.R.R.',
380
- surname: 'Tolkien',
381
- birth_year: 1892,
382
- books_ids: []
383
- },
384
- publisher_id: '1',
385
- publisher: {
386
- id: '1',
387
- name: 'Penguin Random House',
388
- country: 'USA',
389
- books_ids: []
390
- }
391
- }
392
- Book state AFTER the change: {
393
- id: '1',
394
- title: 'The Hobbit - Standalone',
395
- isbn: '9780547928227',
396
- published_year: 1937,
397
- page_count: 310,
398
- genres_ids: [],
399
- author_id: '1',
400
- author: {
401
- id: '1',
402
- name: 'J.R.R.',
403
- surname: 'Tolkien',
404
- birth_year: 1892,
405
- books_ids: []
406
- }
407
- }
408
- ```
409
-
410
- ## PATCH Operations: Partial Updates
411
-
412
- **Important**: Each example in this section starts with the full dataset created above. The effects shown are what happens when you run that specific PATCH operation on the original data.
413
-
414
- ### Example 1: PATCH Updating Only Some Fields
415
-
416
- ```javascript
417
- // Starting with our full dataset from above
418
- await api.resources.books.patch({
419
- id: hobbitBook.id,
420
- title: 'The Hobbit: There and Back Again',
421
- publisher_id: harperPublisher.id
422
- // isbn, published_year, page_count, author_id, and genres NOT mentioned
423
- });
424
- ```
425
-
426
- **Effects on the database:**
427
-
428
- | Table | Changes |
429
- |-------|---------|
430
- | **books** | • title: Updated<br>• isbn: **UNCHANGED**<br>• published_year: **UNCHANGED**<br>• page_count: **UNCHANGED**<br>• author_id: **UNCHANGED** (still Tolkien)<br>• publisher_id: Changed from 1 to 2 |
431
- | **book_genres** | **NO CHANGES** - All 3 genre relationships preserved |
432
- | **Other books** | **NO CHANGES** |
433
-
434
- PATCH follows the philosophy of **partial updates** - it only modifies what you explicitly send. This is fundamentally different from PUT:
435
- - Fields you send are updated
436
- - Fields you DON'T send remain untouched
437
- - Only the `title` and `publisher_id` were mentioned, so only these changed
438
- - The `genres` relationship wasn't mentioned, so all 3 genre associations remain intact
439
-
440
-
441
- **Expected output**:
442
-
443
- ```text
444
- Book state BEFORE the change: {
445
- id: '1',
446
- title: 'The Hobbit',
447
- isbn: '9780547928227',
448
- published_year: 1937,
449
- page_count: 310,
450
- genres_ids: [ '1', '2', '3' ],
451
- genres: [
452
- { id: '1', name: 'Fantasy' },
453
- { id: '2', name: 'Adventure' },
454
- { id: '3', name: 'Classic' }
455
- ],
456
- author_id: '1',
457
- author: {
458
- id: '1',
459
- name: 'J.R.R.',
460
- surname: 'Tolkien',
461
- birth_year: 1892,
462
- books_ids: []
463
- },
464
- publisher_id: '1',
465
- publisher: {
466
- id: '1',
467
- name: 'Penguin Random House',
468
- country: 'USA',
469
- books_ids: []
470
- }
471
- }
472
- Book state AFTER the change: {
473
- id: '1',
474
- title: 'The Hobbit: There and Back Again',
475
- isbn: '9780547928227',
476
- published_year: 1937,
477
- page_count: 310,
478
- genres_ids: [ '1', '2', '3' ],
479
- genres: [
480
- { id: '1', name: 'Fantasy' },
481
- { id: '2', name: 'Adventure' },
482
- { id: '3', name: 'Classic' }
483
- ],
484
- author_id: '1',
485
- author: {
486
- id: '1',
487
- name: 'J.R.R.',
488
- surname: 'Tolkien',
489
- birth_year: 1892,
490
- books_ids: []
491
- },
492
- publisher_id: '2',
493
- publisher: { id: '2', name: 'HarperCollins', country: 'USA', books_ids: [] }
494
- }
495
- ```
496
-
497
- ### Example 2: PATCH Modifying Many-to-Many
498
-
499
- ```javascript
500
- await api.resources.books.patch({
501
- id: hobbitBook.id,
502
- genres: [
503
- fantasyGenre.id, // Keep Fantasy
504
- dystopianGenre.id // Add Dystopian
505
- // Adventure and Classic will be removed
506
- ]
507
- // All other fields NOT mentioned - unchanged
508
- });
509
- ```
510
-
511
- **Effects on the database:**
512
-
513
- | Table | Changes |
514
- |-------|---------|
515
- | **books** | • All fields: **UNCHANGED** |
516
- | **book_genres** | • Fantasy: **PRESERVED** (with original created_at and primary_genre values)<br>• Adventure: **DELETED**<br>• Classic: **DELETED**<br>• Dystopian: **CREATED** |
517
- | **Other tables** | **NO CHANGES** |
518
-
519
- With PATCH, when you DO mention a relationship, it gets completely replaced for that relationship only. Here we mentioned `genres`, so:
520
- - The genres relationship is updated to exactly what we specified
521
- - Other fields (title, author_id, etc.) remain unchanged because they weren't mentioned
522
- - The intelligent sync still applies to the genres: Fantasy is preserved with its metadata, others are added/removed as needed
523
-
524
- **Expected output**:
525
-
526
- ```text
527
- Book state BEFORE the change: {
528
- id: '1',
529
- title: 'The Hobbit',
530
- isbn: '9780547928227',
531
- published_year: 1937,
532
- page_count: 310,
533
- genres_ids: [ '1', '2', '3' ],
534
- genres: [
535
- { id: '1', name: 'Fantasy' },
536
- { id: '2', name: 'Adventure' },
537
- { id: '3', name: 'Classic' }
538
- ],
539
- author_id: '1',
540
- author: {
541
- id: '1',
542
- name: 'J.R.R.',
543
- surname: 'Tolkien',
544
- birth_year: 1892,
545
- books_ids: []
546
- },
547
- publisher_id: '1',
548
- publisher: {
549
- id: '1',
550
- name: 'Penguin Random House',
551
- country: 'USA',
552
- books_ids: []
553
- }
554
- }
555
- Book state AFTER the change: {
556
- id: '1',
557
- title: 'The Hobbit',
558
- isbn: '9780547928227',
559
- published_year: 1937,
560
- page_count: 310,
561
- genres_ids: [ '1', '4' ],
562
- genres: [ { id: '1', name: 'Fantasy' }, { id: '4', name: 'Dystopian' } ],
563
- author_id: '1',
564
- author: {
565
- id: '1',
566
- name: 'J.R.R.',
567
- surname: 'Tolkien',
568
- birth_year: 1892,
569
- books_ids: []
570
- },
571
- publisher_id: '1',
572
- publisher: {
573
- id: '1',
574
- name: 'Penguin Random House',
575
- country: 'USA',
576
- books_ids: []
577
- }
578
- }
579
- ```
580
-
581
- ### Example 3: PATCH Clearing Specific Relationships
582
-
583
- ```javascript
584
- // Starting with our full dataset from above
585
- await api.resources.books.patch({
586
- id: hobbitBook.id,
587
- publisher_id: null,
588
- genres: []
589
- // author_id and other fields NOT mentioned - unchanged
590
- });
591
- ```
592
-
593
- **Effects on the database:**
594
-
595
- | Table | Changes |
596
- |-------|---------|
597
- | **books** | • All attributes: **UNCHANGED**<br>• author_id: **UNCHANGED** (still Tolkien)<br>• publisher_id: **SET TO NULL** |
598
- | **book_genres** | **ALL 3 RECORDS DELETED** |
599
- | **Other tables** | **NO CHANGES** |
600
-
601
- This shows PATCH's **selective update** nature. We explicitly set `publisher_id` to null and `genres` to an empty array:
602
- - These specific relationships are cleared as requested
603
- - Everything else (title, author_id, etc.) remains unchanged
604
- - This is surgical precision - update only what you explicitly mention
605
- - To clear something with PATCH, you must explicitly set it to null or []
606
-
607
- **Expected output**:
608
-
609
- ```html
610
- Book state BEFORE the change: {
611
- id: '1',
612
- title: 'The Hobbit',
613
- isbn: '9780547928227',
614
- published_year: 1937,
615
- page_count: 310,
616
- genres_ids: [ '1', '2', '3' ],
617
- genres: [
618
- { id: '1', name: 'Fantasy' },
619
- { id: '2', name: 'Adventure' },
620
- { id: '3', name: 'Classic' }
621
- ],
622
- author_id: '1',
623
- author: {
624
- id: '1',
625
- name: 'J.R.R.',
626
- surname: 'Tolkien',
627
- birth_year: 1892,
628
- books_ids: []
629
- },
630
- publisher_id: '1',
631
- publisher: {
632
- id: '1',
633
- name: 'Penguin Random House',
634
- country: 'USA',
635
- books_ids: []
636
- }
637
- }
638
- Book state AFTER the change: {
639
- id: '1',
640
- title: 'The Hobbit',
641
- isbn: '9780547928227',
642
- published_year: 1937,
643
- page_count: 310,
644
- genres_ids: [],
645
- author_id: '1',
646
- author: {
647
- id: '1',
648
- name: 'J.R.R.',
649
- surname: 'Tolkien',
650
- birth_year: 1892,
651
- books_ids: []
652
- }
653
- }
654
- ```
655
-
656
- ## Key Differences Summary
657
-
658
- Starting with our complete dataset, here's how each operation type affects the data:
659
-
660
- | Operation | What You Send | Effect on Unmentioned Data |
661
- |-----------|---------------|---------------------------|
662
- | **PUT** | Complete resource replacement | • Attributes: Set to defaults/null<br>• BelongsTo: Set to null (unless required)<br>• Many-to-Many: All relationships removed |
663
- | **PATCH** | Only fields to update | • Attributes: Unchanged<br>• BelongsTo: Unchanged<br>• Many-to-Many: Unchanged |
664
-
665
- ### Effects by Relationship Type
666
-
667
- | Relationship | PUT (not mentioned) | PUT (null/[]) | PATCH (not mentioned) | PATCH (null/[]) |
668
- |--------------|-------------------|---------------|---------------------|----------------|
669
- | **BelongsTo (nullable)** | Set to NULL | Set to NULL | Unchanged | Set to NULL |
670
- | **BelongsTo (required)** | Must be provided | Cannot be NULL | Unchanged | Cannot be NULL |
671
- | **Many-to-Many** | All removed | All removed | Unchanged | All removed |
672
- | **HasMany** | No effect* | N/A | No effect* | N/A |
673
-
674
- *HasMany relationships (like author's other books) are never affected by updates to a single book.
675
-
676
- ## Why HasMany Relationships Are Never Affected
677
-
678
- It's crucial to understand why hasMany relationships (and polymorphic hasMany) are never affected by PUT or PATCH operations on the parent record. The reason is fundamental:
679
-
680
- **HasMany relationships point to actual records, not just links.**
681
-
682
- When a book "belongs to" an author:
683
- - The book has an `author_id` field (a simple foreign key)
684
- - This is just a reference that can be changed
685
-
686
- But when an author "has many" books:
687
- - Each book is a complete, independent record in the books table
688
- - These aren't just "links" that can be deleted - they're real data with their own lifecycle
689
- - To modify these relationships, you must update each book individually
690
-
691
- For example:
692
- ```javascript
693
- // This will NOT delete Tolkien's other books:
694
- await api.resources.authors.put({
695
- id: tolkien.id,
696
- name: 'J.R.R.',
697
- surname: 'Tolkien',
698
- birth_year: 1892
699
- // No mention of books - but they still exist!
700
- });
701
-
702
- // To actually remove a book from an author, you must update the book:
703
- await api.resources.books.patch({
704
- id: lotrBook.id,
705
- author_id: null // or another author's ID
706
- });
707
- ```
708
-
709
- This design prevents accidental data loss and maintains data integrity. Child records are independent entities that must be managed through their own endpoints.
710
-
711
- ## Understanding Pivot Table Preservation
712
-
713
- The intelligent synchronization for many-to-many relationships is important to understand:
714
-
715
- ```javascript
716
- // Looking at our book_genres table structure:
717
- // - book_id
718
- // - genre_id
719
- // - created_at
720
- // - primary_genre
721
-
722
- // When updating genres from [Fantasy, Adventure, Classic] to [Fantasy, Dystopian]:
723
- // - Fantasy record: KEPT with original created_at and primary_genre values
724
- // - Adventure record: DELETED
725
- // - Classic record: DELETED
726
- // - Dystopian record: CREATED with new created_at and default primary_genre (false)
727
- ```
728
-
729
- This preservation is crucial for:
730
- - Maintaining audit trails (when was this genre assigned?)
731
- - Preserving custom pivot data (is this the primary genre?)
732
- - Minimizing database operations (only change what needs changing)
733
-
734
- ## Best Practices
735
-
736
- 1. **Use PATCH for targeted updates** - When you only want to change specific fields
737
- 2. **Use PUT when replacing everything** - When you are explicitly resending the stored values you want to keep
738
- 3. **Always include existing stored fields you want to preserve with PUT** - Omitted persisted values are rejected
739
- 4. **Remember required fields** - PUT must include all required fields like author_id
740
- 5. **Child records are independent** - Other books by the same author are never affected
741
-
742
- ## Next Steps
743
-
744
- Now that you understand how updates affect relationships:
745
- - Practice with PATCH for surgical updates
746
- - Use PUT for explicit full-state replacements
747
- - Plan your API calls to avoid unintended data loss
748
- - Remember that the intelligent sync preserves pivot table metadata