json-rest-api 1.0.6 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +99 -33
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,796 @@
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: { hasMany: '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: { hasMany: '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
+ hasMany: 'genres',
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
+ **Effects on the database:**
318
+
319
+ | Table | Changes |
320
+ |-------|---------|
321
+ | **books** | • title: Updated<br>• author_id: Remains 1 (Tolkien)<br>• publisher_id: **SET TO NULL** |
322
+ | **book_genres** | **ALL 3 RECORDS DELETED** - Book no longer has any genres |
323
+ | **Other books** | **NO CHANGES** - LOTR and Silmarillion unchanged |
324
+
325
+ Again, PUT is a **complete replacement** operation. Since we didn't include `publisher_id` or `genres` in our request, the API treats this as "I want a book with no publisher and no genres." The result:
326
+ - `publisher_id` becomes NULL (it's nullable, so this is allowed)
327
+ - All genre relationships are removed from the pivot table
328
+ - Missing fields are NOT preserved from the current state - they're cleared
329
+
330
+ **Expected output**
331
+
332
+ ```text
333
+ Book state BEFORE the change: {
334
+ id: '1',
335
+ title: 'The Hobbit',
336
+ isbn: '9780547928227',
337
+ published_year: 1937,
338
+ page_count: 310,
339
+ genres_ids: [ '1', '2', '3' ],
340
+ genres: [
341
+ { id: '1', name: 'Fantasy' },
342
+ { id: '2', name: 'Adventure' },
343
+ { id: '3', name: 'Classic' }
344
+ ],
345
+ author_id: '1',
346
+ author: {
347
+ id: '1',
348
+ name: 'J.R.R.',
349
+ surname: 'Tolkien',
350
+ birth_year: 1892,
351
+ books_ids: []
352
+ },
353
+ publisher_id: '1',
354
+ publisher: {
355
+ id: '1',
356
+ name: 'Penguin Random House',
357
+ country: 'USA',
358
+ books_ids: []
359
+ }
360
+ }
361
+ Book state AFTER the change: {
362
+ id: '1',
363
+ title: 'The Hobbit - Revised',
364
+ isbn: '9780547928227',
365
+ published_year: 1937,
366
+ page_count: 310,
367
+ genres_ids: [],
368
+ author_id: '1',
369
+ author: {
370
+ id: '1',
371
+ name: 'J.R.R.',
372
+ surname: 'Tolkien',
373
+ birth_year: 1892,
374
+ books_ids: []
375
+ }
376
+ }
377
+ ```
378
+
379
+ ### Example 3: PUT with Explicit Nulls
380
+
381
+ ```javascript
382
+ // Starting with our full dataset from above
383
+ await api.resources.books.put({
384
+ id: hobbitBook.id,
385
+ title: 'The Hobbit - Standalone',
386
+ isbn: '9780547928227',
387
+ published_year: 1937,
388
+ page_count: 310,
389
+ author_id: tolkien.id, // Required field, cannot be null
390
+ publisher_id: null, // Explicitly clearing publisher
391
+ genres: [] // Explicitly clearing all genres
392
+ });
393
+ ```
394
+
395
+ **Effects on the database:**
396
+
397
+ | Table | Changes |
398
+ |-------|---------|
399
+ | **books** | • title: Updated<br>• author_id: Remains 1 (required field)<br>• publisher_id: **SET TO NULL** |
400
+ | **book_genres** | **ALL 3 RECORDS DELETED** |
401
+ | **Other books** | **NO CHANGES** |
402
+
403
+ This example shows PUT with **explicit nulls and empty arrays**. There's no difference between omitting a field and explicitly setting it to null/[] in a PUT operation - both result in clearing the data. This reinforces that PUT is about **complete state replacement**:
404
+ - You must include ALL data you want to keep
405
+ - Anything missing or null is cleared
406
+ - Required fields (like `author_id`) must always be provided and cannot be null
407
+
408
+
409
+ **Expected output**:
410
+
411
+ ```text
412
+ Book state BEFORE the change: {
413
+ id: '1',
414
+ title: 'The Hobbit',
415
+ isbn: '9780547928227',
416
+ published_year: 1937,
417
+ page_count: 310,
418
+ genres_ids: [ '1', '2', '3' ],
419
+ genres: [
420
+ { id: '1', name: 'Fantasy' },
421
+ { id: '2', name: 'Adventure' },
422
+ { id: '3', name: 'Classic' }
423
+ ],
424
+ author_id: '1',
425
+ author: {
426
+ id: '1',
427
+ name: 'J.R.R.',
428
+ surname: 'Tolkien',
429
+ birth_year: 1892,
430
+ books_ids: []
431
+ },
432
+ publisher_id: '1',
433
+ publisher: {
434
+ id: '1',
435
+ name: 'Penguin Random House',
436
+ country: 'USA',
437
+ books_ids: []
438
+ }
439
+ }
440
+ Book state AFTER the change: {
441
+ id: '1',
442
+ title: 'The Hobbit - Standalone',
443
+ isbn: '9780547928227',
444
+ published_year: 1937,
445
+ page_count: 310,
446
+ genres_ids: [],
447
+ author_id: '1',
448
+ author: {
449
+ id: '1',
450
+ name: 'J.R.R.',
451
+ surname: 'Tolkien',
452
+ birth_year: 1892,
453
+ books_ids: []
454
+ }
455
+ }
456
+ ```
457
+
458
+ ## PATCH Operations: Partial Updates
459
+
460
+ **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.
461
+
462
+ ### Example 1: PATCH Updating Only Some Fields
463
+
464
+ ```javascript
465
+ // Starting with our full dataset from above
466
+ await api.resources.books.patch({
467
+ id: hobbitBook.id,
468
+ title: 'The Hobbit: There and Back Again',
469
+ publisher_id: harperPublisher.id
470
+ // isbn, published_year, page_count, author_id, and genres NOT mentioned
471
+ });
472
+ ```
473
+
474
+ **Effects on the database:**
475
+
476
+ | Table | Changes |
477
+ |-------|---------|
478
+ | **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 |
479
+ | **book_genres** | **NO CHANGES** - All 3 genre relationships preserved |
480
+ | **Other books** | **NO CHANGES** |
481
+
482
+ PATCH follows the philosophy of **partial updates** - it only modifies what you explicitly send. This is fundamentally different from PUT:
483
+ - Fields you send are updated
484
+ - Fields you DON'T send remain untouched
485
+ - Only the `title` and `publisher_id` were mentioned, so only these changed
486
+ - The `genres` relationship wasn't mentioned, so all 3 genre associations remain intact
487
+
488
+
489
+ **Expected output**:
490
+
491
+ ```text
492
+ Book state BEFORE the change: {
493
+ id: '1',
494
+ title: 'The Hobbit',
495
+ isbn: '9780547928227',
496
+ published_year: 1937,
497
+ page_count: 310,
498
+ genres_ids: [ '1', '2', '3' ],
499
+ genres: [
500
+ { id: '1', name: 'Fantasy' },
501
+ { id: '2', name: 'Adventure' },
502
+ { id: '3', name: 'Classic' }
503
+ ],
504
+ author_id: '1',
505
+ author: {
506
+ id: '1',
507
+ name: 'J.R.R.',
508
+ surname: 'Tolkien',
509
+ birth_year: 1892,
510
+ books_ids: []
511
+ },
512
+ publisher_id: '1',
513
+ publisher: {
514
+ id: '1',
515
+ name: 'Penguin Random House',
516
+ country: 'USA',
517
+ books_ids: []
518
+ }
519
+ }
520
+ Book state AFTER the change: {
521
+ id: '1',
522
+ title: 'The Hobbit: There and Back Again',
523
+ isbn: '9780547928227',
524
+ published_year: 1937,
525
+ page_count: 310,
526
+ genres_ids: [ '1', '2', '3' ],
527
+ genres: [
528
+ { id: '1', name: 'Fantasy' },
529
+ { id: '2', name: 'Adventure' },
530
+ { id: '3', name: 'Classic' }
531
+ ],
532
+ author_id: '1',
533
+ author: {
534
+ id: '1',
535
+ name: 'J.R.R.',
536
+ surname: 'Tolkien',
537
+ birth_year: 1892,
538
+ books_ids: []
539
+ },
540
+ publisher_id: '2',
541
+ publisher: { id: '2', name: 'HarperCollins', country: 'USA', books_ids: [] }
542
+ }
543
+ ```
544
+
545
+ ### Example 2: PATCH Modifying Many-to-Many
546
+
547
+ ```javascript
548
+ await api.resources.books.patch({
549
+ id: hobbitBook.id,
550
+ genres: [
551
+ fantasyGenre.id, // Keep Fantasy
552
+ dystopianGenre.id // Add Dystopian
553
+ // Adventure and Classic will be removed
554
+ ]
555
+ // All other fields NOT mentioned - unchanged
556
+ });
557
+ ```
558
+
559
+ **Effects on the database:**
560
+
561
+ | Table | Changes |
562
+ |-------|---------|
563
+ | **books** | • All fields: **UNCHANGED** |
564
+ | **book_genres** | • Fantasy: **PRESERVED** (with original created_at and primary_genre values)<br>• Adventure: **DELETED**<br>• Classic: **DELETED**<br>• Dystopian: **CREATED** |
565
+ | **Other tables** | **NO CHANGES** |
566
+
567
+ With PATCH, when you DO mention a relationship, it gets completely replaced for that relationship only. Here we mentioned `genres`, so:
568
+ - The genres relationship is updated to exactly what we specified
569
+ - Other fields (title, author_id, etc.) remain unchanged because they weren't mentioned
570
+ - The intelligent sync still applies to the genres: Fantasy is preserved with its metadata, others are added/removed as needed
571
+
572
+ **Expected output**:
573
+
574
+ ```text
575
+ Book state BEFORE the change: {
576
+ id: '1',
577
+ title: 'The Hobbit',
578
+ isbn: '9780547928227',
579
+ published_year: 1937,
580
+ page_count: 310,
581
+ genres_ids: [ '1', '2', '3' ],
582
+ genres: [
583
+ { id: '1', name: 'Fantasy' },
584
+ { id: '2', name: 'Adventure' },
585
+ { id: '3', name: 'Classic' }
586
+ ],
587
+ author_id: '1',
588
+ author: {
589
+ id: '1',
590
+ name: 'J.R.R.',
591
+ surname: 'Tolkien',
592
+ birth_year: 1892,
593
+ books_ids: []
594
+ },
595
+ publisher_id: '1',
596
+ publisher: {
597
+ id: '1',
598
+ name: 'Penguin Random House',
599
+ country: 'USA',
600
+ books_ids: []
601
+ }
602
+ }
603
+ Book state AFTER the change: {
604
+ id: '1',
605
+ title: 'The Hobbit',
606
+ isbn: '9780547928227',
607
+ published_year: 1937,
608
+ page_count: 310,
609
+ genres_ids: [ '1', '4' ],
610
+ genres: [ { id: '1', name: 'Fantasy' }, { id: '4', name: 'Dystopian' } ],
611
+ author_id: '1',
612
+ author: {
613
+ id: '1',
614
+ name: 'J.R.R.',
615
+ surname: 'Tolkien',
616
+ birth_year: 1892,
617
+ books_ids: []
618
+ },
619
+ publisher_id: '1',
620
+ publisher: {
621
+ id: '1',
622
+ name: 'Penguin Random House',
623
+ country: 'USA',
624
+ books_ids: []
625
+ }
626
+ }
627
+ ```
628
+
629
+ ### Example 3: PATCH Clearing Specific Relationships
630
+
631
+ ```javascript
632
+ // Starting with our full dataset from above
633
+ await api.resources.books.patch({
634
+ id: hobbitBook.id,
635
+ publisher_id: null,
636
+ genres: []
637
+ // author_id and other fields NOT mentioned - unchanged
638
+ });
639
+ ```
640
+
641
+ **Effects on the database:**
642
+
643
+ | Table | Changes |
644
+ |-------|---------|
645
+ | **books** | • All attributes: **UNCHANGED**<br>• author_id: **UNCHANGED** (still Tolkien)<br>• publisher_id: **SET TO NULL** |
646
+ | **book_genres** | **ALL 3 RECORDS DELETED** |
647
+ | **Other tables** | **NO CHANGES** |
648
+
649
+ This shows PATCH's **selective update** nature. We explicitly set `publisher_id` to null and `genres` to an empty array:
650
+ - These specific relationships are cleared as requested
651
+ - Everything else (title, author_id, etc.) remains unchanged
652
+ - This is surgical precision - update only what you explicitly mention
653
+ - To clear something with PATCH, you must explicitly set it to null or []
654
+
655
+ **Expected output**:
656
+
657
+ ```html
658
+ Book state BEFORE the change: {
659
+ id: '1',
660
+ title: 'The Hobbit',
661
+ isbn: '9780547928227',
662
+ published_year: 1937,
663
+ page_count: 310,
664
+ genres_ids: [ '1', '2', '3' ],
665
+ genres: [
666
+ { id: '1', name: 'Fantasy' },
667
+ { id: '2', name: 'Adventure' },
668
+ { id: '3', name: 'Classic' }
669
+ ],
670
+ author_id: '1',
671
+ author: {
672
+ id: '1',
673
+ name: 'J.R.R.',
674
+ surname: 'Tolkien',
675
+ birth_year: 1892,
676
+ books_ids: []
677
+ },
678
+ publisher_id: '1',
679
+ publisher: {
680
+ id: '1',
681
+ name: 'Penguin Random House',
682
+ country: 'USA',
683
+ books_ids: []
684
+ }
685
+ }
686
+ Book state AFTER the change: {
687
+ id: '1',
688
+ title: 'The Hobbit',
689
+ isbn: '9780547928227',
690
+ published_year: 1937,
691
+ page_count: 310,
692
+ genres_ids: [],
693
+ author_id: '1',
694
+ author: {
695
+ id: '1',
696
+ name: 'J.R.R.',
697
+ surname: 'Tolkien',
698
+ birth_year: 1892,
699
+ books_ids: []
700
+ }
701
+ }
702
+ ```
703
+
704
+ ## Key Differences Summary
705
+
706
+ Starting with our complete dataset, here's how each operation type affects the data:
707
+
708
+ | Operation | What You Send | Effect on Unmentioned Data |
709
+ |-----------|---------------|---------------------------|
710
+ | **PUT** | Complete resource replacement | • Attributes: Set to defaults/null<br>• BelongsTo: Set to null (unless required)<br>• Many-to-Many: All relationships removed |
711
+ | **PATCH** | Only fields to update | • Attributes: Unchanged<br>• BelongsTo: Unchanged<br>• Many-to-Many: Unchanged |
712
+
713
+ ### Effects by Relationship Type
714
+
715
+ | Relationship | PUT (not mentioned) | PUT (null/[]) | PATCH (not mentioned) | PATCH (null/[]) |
716
+ |--------------|-------------------|---------------|---------------------|----------------|
717
+ | **BelongsTo (nullable)** | Set to NULL | Set to NULL | Unchanged | Set to NULL |
718
+ | **BelongsTo (required)** | Must be provided | Cannot be NULL | Unchanged | Cannot be NULL |
719
+ | **Many-to-Many** | All removed | All removed | Unchanged | All removed |
720
+ | **HasMany** | No effect* | N/A | No effect* | N/A |
721
+
722
+ *HasMany relationships (like author's other books) are never affected by updates to a single book.
723
+
724
+ ## Why HasMany Relationships Are Never Affected
725
+
726
+ 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:
727
+
728
+ **HasMany relationships point to actual records, not just links.**
729
+
730
+ When a book "belongs to" an author:
731
+ - The book has an `author_id` field (a simple foreign key)
732
+ - This is just a reference that can be changed
733
+
734
+ But when an author "has many" books:
735
+ - Each book is a complete, independent record in the books table
736
+ - These aren't just "links" that can be deleted - they're real data with their own lifecycle
737
+ - To modify these relationships, you must update each book individually
738
+
739
+ For example:
740
+ ```javascript
741
+ // This will NOT delete Tolkien's other books:
742
+ await api.resources.authors.put({
743
+ id: tolkien.id,
744
+ name: 'J.R.R.',
745
+ surname: 'Tolkien',
746
+ birth_year: 1892
747
+ // No mention of books - but they still exist!
748
+ });
749
+
750
+ // To actually remove a book from an author, you must update the book:
751
+ await api.resources.books.patch({
752
+ id: lotrBook.id,
753
+ author_id: null // or another author's ID
754
+ });
755
+ ```
756
+
757
+ This design prevents accidental data loss and maintains data integrity. Child records are independent entities that must be managed through their own endpoints.
758
+
759
+ ## Understanding Pivot Table Preservation
760
+
761
+ The intelligent synchronization for many-to-many relationships is important to understand:
762
+
763
+ ```javascript
764
+ // Looking at our book_genres table structure:
765
+ // - book_id
766
+ // - genre_id
767
+ // - created_at
768
+ // - primary_genre
769
+
770
+ // When updating genres from [Fantasy, Adventure, Classic] to [Fantasy, Dystopian]:
771
+ // - Fantasy record: KEPT with original created_at and primary_genre values
772
+ // - Adventure record: DELETED
773
+ // - Classic record: DELETED
774
+ // - Dystopian record: CREATED with new created_at and default primary_genre (false)
775
+ ```
776
+
777
+ This preservation is crucial for:
778
+ - Maintaining audit trails (when was this genre assigned?)
779
+ - Preserving custom pivot data (is this the primary genre?)
780
+ - Minimizing database operations (only change what needs changing)
781
+
782
+ ## Best Practices
783
+
784
+ 1. **Use PATCH for targeted updates** - When you only want to change specific fields
785
+ 2. **Use PUT when replacing everything** - When you have the complete new state
786
+ 3. **Always include relationships you want to keep with PUT** - They will be cleared otherwise
787
+ 4. **Remember required fields** - PUT must include all required fields like author_id
788
+ 5. **Child records are independent** - Other books by the same author are never affected
789
+
790
+ ## Next Steps
791
+
792
+ Now that you understand how updates affect relationships:
793
+ - Practice with PATCH for surgical updates
794
+ - Use PUT for complete replacements
795
+ - Plan your API calls to avoid unintended data loss
796
+ - Remember that the intelligent sync preserves pivot table metadata