json-rest-api 1.0.29 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,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