json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,184 @@
1
+ ---
2
+ title: "Many-to-many relationships"
3
+ chapter: 8
4
+ chapter_label: "08"
5
+ ---
6
+
7
+ # 08. Many-to-many relationships
8
+
9
+ Books can have several authors, and authors can write several books. A pivot
10
+ resource, `book_authors`, represents each connection and can also hold attributes
11
+ such as a contribution role. Declare that resource alongside the two endpoints.
12
+
13
+ Insert the following blocks in order into the [starting script](03-running-example.md)
14
+ with a fresh database, before the HTTP server starts. This chapter uses
15
+ `RestApiKnexPlugin` and ordinary pivot rows. Canonical storage keeps membership
16
+ in separate tenant-scoped links: direct pivot resource rows and their metadata
17
+ do not create or describe those links. The relationship-read/write API remains
18
+ available there, but the direct pivot examples below apply to table-backed storage.
19
+
20
+ ## Define the resources
21
+
22
+ ```javascript
23
+ await api.addResource('books', {
24
+ schema: { title: { type: 'string', required: true, indexed: true } },
25
+ relationships: {
26
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
27
+ },
28
+ searchSchema: {
29
+ authorName: { type: 'string', actualField: 'authors.name', filterOperator: 'contains' }
30
+ }
31
+ })
32
+ await api.addResource('authors', {
33
+ schema: { name: { type: 'string', required: true, indexed: true } },
34
+ relationships: {
35
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
36
+ },
37
+ searchSchema: {
38
+ bookTitle: { type: 'string', actualField: 'books.title', filterOperator: 'contains' }
39
+ }
40
+ })
41
+ await api.addResource('book_authors', {
42
+ schema: {
43
+ book_id: { type: 'id', belongsTo: 'books', as: 'book', required: true },
44
+ author_id: { type: 'id', belongsTo: 'authors', as: 'author', required: true },
45
+ contribution: { type: 'string', nullable: true }
46
+ },
47
+ searchSchema: { book: { type: 'id', actualField: 'book_id' } }
48
+ })
49
+ await api.resources.books.createKnexTable()
50
+ await api.resources.authors.createKnexTable()
51
+ await api.resources.book_authors.createKnexTable()
52
+ ```
53
+
54
+ `through` names the pivot resource. `foreignKey` is its logical field pointing
55
+ to the current resource, and `otherKey` points to the related resource. The
56
+ reverse declaration swaps these fields. Storage mapping determines physical
57
+ columns. Each pivot record has its own resource ID.
58
+
59
+ ## Create records with membership
60
+
61
+ ```javascript
62
+ const neil = await api.resources.authors.post({ data: { name: 'Neil Gaiman' } })
63
+ const terry = await api.resources.authors.post({ data: { name: 'Terry Pratchett' } })
64
+ const goodOmens = await api.resources.books.post({
65
+ data: { title: 'Good Omens', authors: [neil.id, terry.id] }
66
+ })
67
+ const americanGods = await api.resources.books.post({
68
+ format: 'jsonapi',
69
+ document: {
70
+ data: {
71
+ type: 'books',
72
+ attributes: { title: 'American Gods' },
73
+ relationships: { authors: { data: [{ type: 'authors', id: neil.id }] } }
74
+ }
75
+ }
76
+ })
77
+ ```
78
+
79
+ The plain record uses an ID array under `data.authors`; JSON:API uses
80
+ identifier linkage under `data.relationships.authors.data`. These operations
81
+ create the necessary pivot connections. Do not also create direct pivot records
82
+ for those same connections.
83
+
84
+ ## Read includes and filter through the pivot
85
+
86
+ ```javascript
87
+ const includedBook = await api.resources.books.get({
88
+ id: goodOmens.id, queryParams: { include: ['authors'] }
89
+ })
90
+ const includedDocument = await api.resources.books.get({
91
+ id: goodOmens.id, format: 'jsonapi', queryParams: { include: ['authors'] }
92
+ })
93
+ const booksByNeil = await api.resources.books.query({
94
+ queryParams: { filters: { authorName: 'Neil' }, include: ['authors'] }
95
+ })
96
+ const authorsOfGods = await api.resources.authors.query({
97
+ queryParams: { filters: { bookTitle: 'Gods' } }
98
+ })
99
+ console.log('Good Omens authors:', includedBook.authors)
100
+ console.log('JSON:API authors:', includedDocument.included)
101
+ console.log('Books by Neil:', booksByNeil.data)
102
+ console.log('Authors of Gods:', authorsOfGods.data)
103
+ ```
104
+
105
+ Good Omens includes both authors. Plain responses embed included records;
106
+ JSON:API retains relationship identifiers and places related records in
107
+ `included`. Without an include, membership is represented by IDs rather than
108
+ full author attributes. The filter finds both books by Neil; its Good Omens
109
+ result still includes Terry as well. Filtering a parent does not filter the
110
+ included collection to just matching children. The reverse title filter finds
111
+ Neil alone.
112
+
113
+ ## Work with pivot attributes directly
114
+
115
+ ```javascript
116
+ const colorOfMagic = await api.resources.books.post({
117
+ data: { title: 'The Color of Magic' }
118
+ })
119
+ const contribution = await api.resources.book_authors.post({
120
+ data: { book: colorOfMagic.id, author: terry.id, contribution: 'primary' }
121
+ })
122
+ const omensPivots = await api.resources.book_authors.query({
123
+ queryParams: { filters: { book: goodOmens.id } }
124
+ })
125
+ for (const pivot of omensPivots.data) {
126
+ await api.resources.book_authors.patch({
127
+ id: pivot.id, data: { contribution: 'co-author' }, returning: 'none'
128
+ })
129
+ }
130
+ const updatedPivots = await api.resources.book_authors.query({
131
+ queryParams: { filters: { book: goodOmens.id } }
132
+ })
133
+ console.log('Direct pivot:', contribution)
134
+ console.log('Updated contributions:', updatedPivots.data)
135
+ ```
136
+
137
+ The pivot is a resource: use its public relationship aliases `book` and `author`
138
+ in `data`. Its declared `book` filter finds the two Good Omens connection
139
+ records. Updating their attributes changes contribution metadata without
140
+ replacing membership. Query the pivot resource to retrieve these attributes;
141
+ a book's author include does not automatically attach pivot attributes to authors.
142
+ Direct pivot creation should not duplicate membership already created through
143
+ `authors`. Define the uniqueness constraints your persistent schema requires.
144
+
145
+ ## Remove membership without deleting an author
146
+
147
+ ```javascript
148
+ await api.resources.books.deleteRelationship({
149
+ id: goodOmens.id,
150
+ relationshipName: 'authors',
151
+ relationshipData: [{ type: 'authors', id: terry.id }]
152
+ })
153
+ const remainingMembers = await api.resources.books.getRelationship({
154
+ id: goodOmens.id, relationshipName: 'authors'
155
+ })
156
+ const remainingPivots = await api.resources.book_authors.query({
157
+ queryParams: { filters: { book: goodOmens.id } }
158
+ })
159
+ const terryStillExists = await api.resources.authors.get({ id: terry.id })
160
+ console.log('Remaining authors:', remainingMembers.data)
161
+ console.log('Remaining pivot records:', remainingPivots.data)
162
+ console.log('Author remains:', terryStillExists.name)
163
+ ```
164
+
165
+ The Good Omens connection to Terry is removed, including that pivot record's
166
+ contribution metadata. Neil's connection and Terry's author record remain.
167
+ The relationship method returns undefined and takes identifiers in
168
+ `relationshipData`. A resource PATCH with `data: { authors: [...] }`
169
+ replaces the membership set; use relationship POST/DELETE for incremental changes.
170
+
171
+ ## HTTP endpoints
172
+
173
+ After running all the blocks, Good Omens has only Neil:
174
+
175
+ ```bash
176
+ curl --globoff 'http://localhost:3000/api/books/1?include=authors'
177
+ curl 'http://localhost:3000/api/books/1/relationships/authors'
178
+ curl --globoff 'http://localhost:3000/api/books?filter[authorName]=Neil'
179
+ curl --globoff 'http://localhost:3000/api/book_authors?filter[book]=1'
180
+ ```
181
+
182
+ Generated HTTP endpoints use JSON:API documents, including for the pivot
183
+ resource. Use IDs returned by your server when adapting this example to an
184
+ existing database.
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: "Pagination and sorting"
3
+ chapter: 9
4
+ chapter_label: "09"
5
+ ---
6
+
7
+ # 09. Pagination and sorting
8
+
9
+ Pagination applies to the collection queried. A publisher query pages publishers;
10
+ a publisher's to-many `getRelated` call pages its related records. Including
11
+ children does not make the parent's page options paginate those children.
12
+
13
+ Insert these blocks into the [starting script](03-running-example.md)
14
+ on a fresh database, before the server starts.
15
+
16
+ ## Define a stable ordering
17
+
18
+ ```javascript
19
+ await api.addResource('countries', {
20
+ queryDefaultLimit: 2,
21
+ queryMaxLimit: 3,
22
+ defaultSort: ['name'],
23
+ schema: {
24
+ name: { type: 'string', required: true, search: true },
25
+ code: { type: 'string', required: true }
26
+ }
27
+ })
28
+ await api.resources.countries.createKnexTable()
29
+ for (const [name, code] of [
30
+ ['France', 'FR'], ['Germany', 'DE'], ['Austria', 'AT'],
31
+ ['Italy', 'IT'], ['United Kingdom', 'GB']
32
+ ]) {
33
+ await api.resources.countries.post({ data: { name, code }, returning: 'none' })
34
+ }
35
+ ```
36
+
37
+ The resource default sorts by name. A query may override it using
38
+ `queryParams.sort`, for example `['-name']` for descending order. An ID tie-breaker
39
+ provides stable ordering where the selected values are equal. `defaultSort`
40
+ accepts a string or an array of strings; object-shaped defaults are rejected.
41
+
42
+ ## Default limits and numbered pages
43
+
44
+ ```javascript
45
+ const defaultPage = await api.resources.countries.query({})
46
+ const numbered = await api.resources.countries.query({
47
+ queryParams: { page: { number: 2, size: 2 } }
48
+ })
49
+ const emptyPage = await api.resources.countries.query({
50
+ queryParams: { page: { number: 10, size: 2 } }
51
+ })
52
+ console.log('Default records:', defaultPage.data)
53
+ console.log('Second page:', numbered.data)
54
+ console.log('Numbered metadata:', numbered.meta.pagination)
55
+ console.log('Beyond the last page:', emptyPage.data)
56
+ ```
57
+
58
+ Omitted page options, or `page: {}`, use the capped default limit and omit
59
+ pagination metadata. Here, the default result is Austria and France.
60
+ `page: { number: 2, size: 2 }` selects Germany and Italy. With pagination counts
61
+ enabled, metadata is:
62
+
63
+ ```json
64
+ { "page": 2, "pageSize": 2, "pageCount": 3, "total": 5, "hasMore": true }
65
+ ```
66
+
67
+ Page 10 returns an empty array. `enablePaginationCounts: false` suppresses the
68
+ count query and count-derived metadata; do not assume every collection response
69
+ contains totals. Every mode respects `queryMaxLimit`.
70
+
71
+ ## Cursor pages
72
+
73
+ ```javascript
74
+ const firstCursorPage = await api.resources.countries.query({
75
+ queryParams: { sort: ['name'], page: { size: 2 } }
76
+ })
77
+ const nextCursor = firstCursorPage.meta.pagination.cursor.next
78
+ const secondCursorPage = await api.resources.countries.query({
79
+ queryParams: { sort: ['name'], page: { size: 2, after: nextCursor } }
80
+ })
81
+ console.log('First cursor page:', firstCursorPage.data)
82
+ console.log('Second cursor page:', secondCursorPage.data)
83
+ ```
84
+
85
+ `page: { size: 2 }` starts cursor pagination. It returns Austria and France;
86
+ the next page returns Germany and Italy. Cursor pages report `hasMore` and,
87
+ when available, an opaque next cursor under `meta.pagination.cursor.next`.
88
+ They do not count the collection. Treat cursors as opaque and keep the same sort,
89
+ filters and fields when continuing. Returned links preserve those options.
90
+
91
+ Use `after` or `before` with a returned cursor. Do not combine either boundary
92
+ with `number`, supply both boundaries, or use an empty cursor string. Omitted
93
+ page sizes use the resource default; explicitly keep the chosen size when
94
+ constructing the next call yourself.
95
+
96
+ ## Sparse fields and ordering
97
+
98
+ ```javascript
99
+ const sparse = await api.resources.countries.query({
100
+ queryParams: {
101
+ fields: { countries: 'code' },
102
+ sort: ['name'],
103
+ page: { size: 2 }
104
+ }
105
+ })
106
+ const nextSparse = await api.resources.countries.query({
107
+ queryParams: {
108
+ fields: { countries: 'code' },
109
+ sort: ['name'],
110
+ page: { size: 2, after: sparse.meta.pagination.cursor.next }
111
+ }
112
+ })
113
+ console.log('Sparse first page:', sparse.data)
114
+ console.log('Sparse second page:', nextSparse.data)
115
+ ```
116
+
117
+ The first sparse page contains IDs and codes AT/FR; the second contains DE/IT.
118
+ The library retains the sort values needed internally without exposing omitted
119
+ fields in the response. Sparse output is not permission to sort by hidden fields.
120
+
121
+ Stored attributes, logical IDs and SQL query projections keep their own sort
122
+ meaning when a search filter with the same name points elsewhere. A distinct
123
+ sortable search alias can target a scalar stored field. Cursor values and
124
+ relationship include `orderBy` follow the same field-resolution rules.
125
+
126
+ ## Derived and structured values
127
+
128
+ Use [query projections](15-query-projections.md) when a derived SQL value
129
+ must participate in sorting and cursors. JavaScript computed fields run after
130
+ fetching and are not a replacement for SQL ordering.
131
+
132
+ Object and array fields do not define a generic whole-document order. They are
133
+ excluded from default sortable fields; explicitly making them sortable or using
134
+ them in a default sort rejects schema compilation. Project a scalar JSON value
135
+ when that is the intended ordering, and verify the SQL on your chosen backend.
136
+ Stored SQL values drive cursor comparison; getters format returned values.
137
+
138
+ ## HTTP examples
139
+
140
+ ```bash
141
+ curl --globoff 'http://localhost:3000/api/countries?page[number]=2&page[size]=2&sort=name'
142
+ curl --globoff 'http://localhost:3000/api/countries?page[size]=2&sort=name'
143
+ curl --globoff 'http://localhost:3000/api/countries?page[size]=2&sort=name&fields[countries]=code'
144
+ ```
145
+
146
+ HTTP responses are JSON:API documents. Follow their returned links to continue
147
+ pagination, preserving filters, fields, sort and size. See the
148
+ [migration contract](33-migrating-to-v2.md#pagination-modes-and-counts) for removed
149
+ options and count behavior.
@@ -0,0 +1,174 @@
1
+ ---
2
+ title: "PUT and PATCH"
3
+ chapter: 10
4
+ chapter_label: "10"
5
+ ---
6
+
7
+ # 10. PUT and PATCH
8
+
9
+ PATCH changes supplied values. PUT describes a replacement and requires explicit
10
+ values for persisted attributes and belongs-to relationships that already have
11
+ values. PUT does not silently clear those stored values by omission.
12
+
13
+ Relationship collections have an additional PUT rule: supplying a JSON:API
14
+ `data.relationships` object makes omitted declared collections part of the
15
+ replacement. An empty object can therefore clear collections. PATCH changes
16
+ only the relationships named in its input.
17
+
18
+ The following six blocks form one runnable example. Insert them in order into
19
+ the [starting script](03-running-example.md) before starting its server,
20
+ on a fresh database. JSON:API input makes the relationship-object boundary
21
+ explicit; plain input is described below.
22
+
23
+ ## Create the example records
24
+
25
+ ```javascript
26
+ await api.addResource('publishers', {
27
+ schema: { name: { type: 'string', required: true } },
28
+ relationships: { books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' } }
29
+ })
30
+ await api.addResource('books', {
31
+ schema: {
32
+ title: { type: 'string', required: true },
33
+ note: { type: 'string', nullable: true },
34
+ publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
35
+ }
36
+ })
37
+ await api.resources.publishers.createKnexTable()
38
+ await api.resources.books.createKnexTable()
39
+ const publisher = await api.resources.publishers.post({ data: { name: 'Example Press' } })
40
+ const book = await api.resources.books.post({
41
+ data: { title: 'First edition', note: 'Keep me', publisher: publisher.id }
42
+ })
43
+ ```
44
+
45
+ The plain create calls still put data inside `data`, and refer to the
46
+ relationship by its public name `publisher`, not its backing column.
47
+
48
+ ## PATCH retains omitted values
49
+
50
+ ```javascript
51
+ const patched = await api.resources.books.patch({
52
+ id: book.id, format: 'jsonapi',
53
+ document: { data: { type: 'books', id: book.id, attributes: { title: 'Second edition' } } }
54
+ })
55
+ console.log(patched.data.attributes, patched.data.relationships.publisher.data)
56
+ ```
57
+
58
+ The title changes. The note remains `Keep me` and the publisher linkage remains.
59
+ A supplied to-many array replaces that relationship's membership; it is not an
60
+ append operation. Use [relationship endpoints](11-relationship-endpoints.md)
61
+ for explicit attachment/removal.
62
+
63
+ ## PUT requires explicit existing values
64
+
65
+ ```javascript
66
+ let incompletePutError
67
+ try {
68
+ await api.resources.books.put({
69
+ id: book.id, format: 'jsonapi',
70
+ document: { data: { type: 'books', id: book.id, attributes: { title: 'Rejected edition' } } }
71
+ })
72
+ } catch (error) {
73
+ incompletePutError = error
74
+ }
75
+ const afterRejectedPut = await api.resources.books.get({ id: book.id })
76
+ console.log(incompletePutError?.cause?.message, afterRejectedPut.title)
77
+ ```
78
+
79
+ The owned write rejects with `RestApiWriteError`; its cause is a
80
+ `RestApiValidationError` describing the omitted note and publisher. Its
81
+ `transactionOutcome` is `rolledBack`, and the title remains `Second edition`.
82
+ In an application, handle or propagate the error; the catch here lets the
83
+ remaining examples run.
84
+
85
+ ```javascript
86
+ const replaced = await api.resources.books.put({
87
+ id: book.id, format: 'jsonapi',
88
+ document: {
89
+ data: {
90
+ type: 'books', id: book.id,
91
+ attributes: { title: 'Third edition', note: null },
92
+ relationships: { publisher: { data: { type: 'publishers', id: publisher.id } } }
93
+ }
94
+ }
95
+ })
96
+ console.log(replaced.data.attributes, replaced.data.relationships.publisher.data)
97
+ ```
98
+
99
+ This replacement explicitly clears the nullable note and retains the publisher.
100
+ To clear the publisher, supply `publisher: { data: null }` in JSON:API
101
+ relationships (or `publisher: null` in plain input). Required/non-nullable
102
+ relationships cannot be detached this way. Computed and virtual fields are not
103
+ persisted replacement values; ordinary required-field validation still applies.
104
+
105
+ ## PUT's relationship-object boundary
106
+
107
+ ```javascript
108
+ await api.resources.publishers.put({
109
+ id: publisher.id, format: 'jsonapi',
110
+ document: { data: { type: 'publishers', id: publisher.id, attributes: { name: 'Renamed Press' } } }
111
+ })
112
+ const retainedBooks = await api.resources.publishers.getRelationship({ id: publisher.id, relationshipName: 'books' })
113
+ await api.resources.publishers.put({
114
+ id: publisher.id, format: 'jsonapi',
115
+ document: {
116
+ data: {
117
+ type: 'publishers', id: publisher.id, attributes: { name: 'Empty Press' },
118
+ relationships: {}
119
+ }
120
+ }
121
+ })
122
+ const clearedBooks = await api.resources.publishers.getRelationship({ id: publisher.id, relationshipName: 'books' })
123
+ const detachedBook = await api.resources.books.get({ id: book.id })
124
+ console.log(retainedBooks.data, clearedBooks.data, detachedBook)
125
+ ```
126
+
127
+ The first PUT has no relationships object and keeps the book membership. The
128
+ second includes an empty object and detaches the book. The book record still
129
+ exists; its nullable publisher is cleared. Plain read output omits an unassigned
130
+ to-one relationship. A JSON:API linkage read represents it as `data: null`.
131
+
132
+ The same omission boundary applies to declared many-to-many collections and
133
+ reverse has-one relationships: replacing membership does not delete target
134
+ records. Reverse changes can invoke child PATCH lifecycles and fail when the
135
+ child cannot be detached. Ordinary through-row metadata and canonical links
136
+ have different storage behavior; see [many-to-many relationships](08-many-to-many.md).
137
+
138
+ ## PATCH selects the relationship to replace
139
+
140
+ ```javascript
141
+ await api.resources.publishers.patch({
142
+ id: publisher.id, format: 'jsonapi',
143
+ document: {
144
+ data: {
145
+ type: 'publishers', id: publisher.id,
146
+ relationships: { books: { data: [{ type: 'books', id: book.id }] } }
147
+ }
148
+ }
149
+ })
150
+ const reattachedBooks = await api.resources.publishers.getRelationship({ id: publisher.id, relationshipName: 'books' })
151
+ const unchangedPublisher = await api.resources.publishers.get({ id: publisher.id })
152
+ console.log(reattachedBooks.data, unchangedPublisher.name)
153
+ ```
154
+
155
+ The book is attached again, and the publisher name remains `Empty Press`.
156
+ For either method, an explicitly supplied empty collection clears membership.
157
+ Omitted PATCH attributes and relationships remain unchanged.
158
+
159
+ ## Plain input and choosing the method
160
+
161
+ Plain calls use `data: { title, note, publisher, ... }`. Supplying public
162
+ relationship names causes their conversion into JSON:API relationship entries.
163
+ A PUT that supplies one relationship can therefore clear other omitted
164
+ collections. Use PATCH for a partial update, or explicitly include every
165
+ relationship you intend to retain in a PUT replacement. Hooks that introduce a
166
+ relationships object also participate in this boundary.
167
+
168
+ Use `returning: 'full'`, `'minimal'` or `'none'` to select the write response;
169
+ none/minimal do not skip write validation or mutation. See
170
+ [hooks](13-hooks-and-lifecycle.md) for nested response reads and
171
+ [managed transactions](19-managed-transactions.md) for atomic multi-call changes.
172
+ Unconditional PUT can create a missing resource at the supplied ID; an
173
+ `expectedVersion` condition cannot match a missing row. See
174
+ [version-field migration](22-version-field-migrations.md) for conditional writes.
@@ -0,0 +1,174 @@
1
+ ---
2
+ title: "Relationship endpoints"
3
+ chapter: 11
4
+ chapter_label: "11"
5
+ ---
6
+
7
+ # 11. Relationship endpoints
8
+
9
+ Relationship methods are part of `RestApiPlugin`; no extra relationship plugin
10
+ is required. Install a storage plugin and an HTTP connector before registering
11
+ resources. Configure the connector's `mountPath`, for example `/api`, as in the
12
+ [quickstart](../QUICKSTART.md).
13
+
14
+ There are two kinds of read endpoint:
15
+
16
+ | Endpoint | Programmatic method | Response |
17
+ | --- | --- | --- |
18
+ | `/api/publishers/1/relationships/authors` | `getRelationship` | JSON:API identifier linkage |
19
+ | `/api/publishers/1/authors` | `getRelated` | Related resource records |
20
+
21
+ A linkage read returns a document even when programmatic record format is plain.
22
+ A related read respects `format`: plain to-one reads return a record or null,
23
+ plain to-many reads return a collection with `data`, and JSON:API reads return
24
+ a document. Related reads apply the target's fields, filtering and pagination;
25
+ they do not promise every related record in a single response.
26
+
27
+ ## Set up a runnable example
28
+
29
+ Insert these blocks into the [starting script](03-running-example.md)
30
+ on a fresh database, before the HTTP server starts.
31
+
32
+ ```javascript
33
+ await api.addResource('publishers', {
34
+ schema: { name: { type: 'string', required: true } },
35
+ relationships: { authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' } }
36
+ })
37
+ await api.addResource('authors', {
38
+ schema: {
39
+ name: { type: 'string', required: true },
40
+ publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
41
+ }
42
+ })
43
+ await api.resources.publishers.createKnexTable()
44
+ await api.resources.authors.createKnexTable()
45
+ const publisher = await api.resources.publishers.post({ data: { name: 'Scribner' } })
46
+ const stephen = await api.resources.authors.post({ data: { name: 'Stephen King' } })
47
+ const peter = await api.resources.authors.post({ data: { name: 'Peter Straub' } })
48
+ ```
49
+
50
+ The inverse relationships are explicitly declared. A `belongsTo` field does not
51
+ automatically create an arbitrary reverse `hasOne` or `hasMany` endpoint.
52
+
53
+ ## Add membership and read each representation
54
+
55
+ ```javascript
56
+ await api.resources.publishers.postRelationship({
57
+ id: publisher.id,
58
+ relationshipName: 'authors',
59
+ relationshipData: [
60
+ { type: 'authors', id: stephen.id },
61
+ { type: 'authors', id: peter.id }
62
+ ]
63
+ })
64
+ const linkage = await api.resources.publishers.getRelationship({
65
+ id: publisher.id, relationshipName: 'authors'
66
+ })
67
+ const related = await api.resources.publishers.getRelated({
68
+ id: publisher.id,
69
+ relationshipName: 'authors',
70
+ queryParams: { fields: { authors: 'name' }, sort: ['name'] }
71
+ })
72
+ console.log('Identifiers:', linkage.data)
73
+ console.log('Records:', related.data)
74
+ ```
75
+
76
+ Linkage contains `{ type: 'authors', id }` identifiers. Related records contain
77
+ IDs and names, ordered Peter then Stephen. Relationship-only writes take
78
+ `relationshipData`, not `data`; HTTP wraps that linkage in a `data` member.
79
+ POST adds members and DELETE removes specified members; both apply only to
80
+ supported to-many relationships. PATCH replaces a to-many set or a to-one target.
81
+ All three relationship write methods return undefined.
82
+
83
+ ## Replace and remove members
84
+
85
+ ```javascript
86
+ await api.resources.publishers.patchRelationship({
87
+ id: publisher.id,
88
+ relationshipName: 'authors',
89
+ relationshipData: [{ type: 'authors', id: stephen.id }]
90
+ })
91
+ const replaced = await api.resources.publishers.getRelationship({
92
+ id: publisher.id, relationshipName: 'authors'
93
+ })
94
+ await api.resources.publishers.deleteRelationship({
95
+ id: publisher.id,
96
+ relationshipName: 'authors',
97
+ relationshipData: [{ type: 'authors', id: stephen.id }]
98
+ })
99
+ const empty = await api.resources.publishers.getRelationship({
100
+ id: publisher.id, relationshipName: 'authors'
101
+ })
102
+ console.log('After replacement:', replaced.data)
103
+ console.log('After removal:', empty.data)
104
+ ```
105
+
106
+ Replacement leaves Stephen alone; removal leaves an empty array. Authors remain
107
+ in the database. With `hasMany`, detachment clears the child's nullable foreign
108
+ key. With ordinary many-to-many storage, it removes the pivot row. Required
109
+ relationships cannot be cleared by detaching a child.
110
+
111
+ ## Change and clear a to-one target
112
+
113
+ ```javascript
114
+ await api.resources.authors.patchRelationship({
115
+ id: peter.id,
116
+ relationshipName: 'publisher',
117
+ relationshipData: { type: 'publishers', id: publisher.id }
118
+ })
119
+ const peterPublisher = await api.resources.authors.getRelated({
120
+ id: peter.id, relationshipName: 'publisher'
121
+ })
122
+ await api.resources.authors.patchRelationship({
123
+ id: peter.id, relationshipName: 'publisher', relationshipData: null
124
+ })
125
+ const cleared = await api.resources.authors.getRelationship({
126
+ id: peter.id, relationshipName: 'publisher'
127
+ })
128
+ console.log('Related publisher:', peterPublisher.name)
129
+ console.log('Cleared linkage:', cleared.data)
130
+ ```
131
+
132
+ The related read returns Scribner; the final linkage is null. Use null only when
133
+ the relationship permits it. Polymorphic linkage also requires its target type;
134
+ see [polymorphic relationships](07-polymorphic-relationships.md).
135
+
136
+ ## HTTP equivalents
137
+
138
+ After running all blocks, both authors are unassigned. These commands add them,
139
+ read both endpoints, and clear the collection again:
140
+
141
+ ```bash
142
+ curl -X POST http://localhost:3000/api/publishers/1/relationships/authors \
143
+ -H 'Content-Type: application/vnd.api+json' \
144
+ -d '{"data":[{"type":"authors","id":"1"},{"type":"authors","id":"2"}]}'
145
+ curl http://localhost:3000/api/publishers/1/relationships/authors
146
+ curl --globoff 'http://localhost:3000/api/publishers/1/authors?fields[authors]=name&sort=name'
147
+ curl -X PATCH http://localhost:3000/api/publishers/1/relationships/authors \
148
+ -H 'Content-Type: application/vnd.api+json' \
149
+ -d '{"data":[]}'
150
+ ```
151
+
152
+ Successful generated relationship writes return HTTP 204 without a body. Use IDs
153
+ returned by the server for persistent databases. Resource PATCH is a different
154
+ body shape: `data.type`, `data.id`, and `data.relationships` alongside any
155
+ attributes being changed.
156
+
157
+ ## Permissions and transactions
158
+
159
+ Relationship operations apply their method-specific permission checks and
160
+ resource/target visibility. Linkage discovery is not a bypass for related query
161
+ permissions. Ordinary pivot rows also have their own query visibility; canonical
162
+ links are separate from pivot resource rows. Configure hooks through
163
+ `api.customize` using the [hook guide](13-hooks-and-lifecycle.md).
164
+
165
+ A successful relationship write participates in the library's transaction
166
+ lifecycle. Compose several operations with a handle from
167
+ [`api.transaction`](19-managed-transactions.md). A loop of independent operations
168
+ is not a single atomic transaction. Failures report the
169
+ [write outcome](20-transaction-outcomes.md); do not blindly retry an unknown outcome.
170
+
171
+ Use relationship writes for membership-only changes, related reads for records,
172
+ and resource PATCH when updating attributes and relationships together. Where
173
+ provided, follow response links for discovery and pagination; relative links
174
+ must be resolved against the API URL by clients that require absolute URLs.