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
@@ -0,0 +1,177 @@
1
+ ---
2
+ title: "Belongs-to relationships"
3
+ chapter: 5
4
+ chapter_label: "05"
5
+ ---
6
+
7
+ # 05. Belongs-to relationships
8
+
9
+ A publisher belongs to a country. In table-backed storage, the publisher holds
10
+ the foreign key; several publishers can reference the same country. Use the
11
+ [starting script](03-running-example.md), and insert the JavaScript
12
+ blocks below in order before the server starts. Start with a fresh database.
13
+
14
+ ## Define resources and create records
15
+
16
+ ```javascript
17
+ await api.addResource('countries', {
18
+ schema: {
19
+ name: { type: 'string', required: true, max: 100 },
20
+ code: { type: 'string', max: 2, unique: true, indexed: true }
21
+ }
22
+ })
23
+ await api.addResource('publishers', {
24
+ schema: {
25
+ name: { type: 'string', required: true, max: 255 },
26
+ country_id: { type: 'id', belongsTo: 'countries', as: 'country', nullable: true }
27
+ },
28
+ searchSchema: {
29
+ country: { type: 'id', actualField: 'country_id', nullable: true },
30
+ countryCode: { type: 'string', actualField: 'countries.code' }
31
+ }
32
+ })
33
+ await api.resources.countries.createKnexTable()
34
+ await api.resources.publishers.createKnexTable()
35
+
36
+ const france = await api.resources.countries.post({
37
+ data: { name: 'France', code: 'FR' }
38
+ })
39
+ const uk = await api.resources.countries.post({
40
+ data: { name: 'United Kingdom', code: 'GB' }
41
+ })
42
+ const frenchPublisher = await api.resources.publishers.post({
43
+ data: { name: 'French Books Inc.', country: france.id }
44
+ })
45
+ await api.resources.publishers.post({
46
+ data: { name: 'Another French Publisher', country: france.id }
47
+ })
48
+ const britishPublisher = await api.resources.publishers.post({
49
+ data: { name: 'UK Books Ltd.', country: uk.id }
50
+ })
51
+ await api.resources.publishers.post({
52
+ data: { name: 'Global Publishing', country: null }
53
+ })
54
+ ```
55
+
56
+ Plain writes use the relationship alias `country` inside `data`.
57
+ Do not supply the foreign-key field `country_id` as a public relationship input.
58
+ Without an include, the plain response contains `country: { id: france.id }`;
59
+ an unassigned to-one relationship is omitted from plain output. JSON:API
60
+ represents its linkage with `data: null`.
61
+
62
+ `country_id` is the logical schema field. Its physical column can differ with
63
+ storage configuration. Hooks must follow their stage's context contract instead
64
+ of assuming that logical relationship fields are already SQL columns.
65
+
66
+ ## Include related records
67
+
68
+ ```javascript
69
+ const includedPublisher = await api.resources.publishers.get({
70
+ id: frenchPublisher.id,
71
+ queryParams: { include: ['country'] }
72
+ })
73
+ console.log('Included country:', includedPublisher.country)
74
+
75
+ const plainCollection = await api.resources.publishers.query({
76
+ queryParams: { include: ['country'], sort: ['id'] }
77
+ })
78
+ console.log('Publishers:', plainCollection.data)
79
+
80
+ const jsonapiCollection = await api.resources.publishers.query({
81
+ format: 'jsonapi',
82
+ queryParams: { include: ['country'], sort: ['id'] }
83
+ })
84
+ console.log('JSON:API collection:', jsonapiCollection)
85
+ ```
86
+
87
+ `includedPublisher.country.name` is `France`. Plain collections still have a
88
+ `data` array; included records are embedded into their corresponding parent
89
+ objects. JSON:API collections put related records in `included` and keep
90
+ identifiers under each primary record's `relationships.country.data`. The two
91
+ French publishers share one France entry in that document's `included` array.
92
+ The unassigned publisher retains null linkage.
93
+
94
+ ## Select sparse fields
95
+
96
+ ```javascript
97
+ const sparsePublisher = await api.resources.publishers.get({
98
+ id: frenchPublisher.id,
99
+ queryParams: {
100
+ include: ['country'],
101
+ fields: { publishers: 'name,country', countries: 'code' }
102
+ }
103
+ })
104
+ console.log('Sparse country:', sparsePublisher.country)
105
+ ```
106
+
107
+ Include the `country` relationship in the publisher's fieldset when its linkage
108
+ must appear. The related country has its ID and code `FR`, without its name.
109
+ Sparse fields select output; they are not an authorization policy.
110
+
111
+ ## Filter by a relationship
112
+
113
+ ```javascript
114
+ const fromFrance = await api.resources.publishers.query({
115
+ queryParams: { filters: { country: france.id } }
116
+ })
117
+ const fromUK = await api.resources.publishers.query({
118
+ queryParams: { filters: { countryCode: 'GB' } }
119
+ })
120
+ const unassigned = await api.resources.publishers.query({
121
+ queryParams: { filters: { country: null } }
122
+ })
123
+ console.log('French publishers:', fromFrance.data)
124
+ console.log('British publishers:', fromUK.data)
125
+ console.log('Unassigned publishers:', unassigned.data)
126
+ ```
127
+
128
+ The declared `country` filter selects two French publishers, `countryCode`
129
+ selects UK Books Ltd., and null selects Global Publishing. `actualField` maps
130
+ these public filter names to the ID field or the related resource's code.
131
+ Cross-table filtering requires the related code field to declare `indexed: true`.
132
+ Filtering does not automatically include the related record's attributes.
133
+
134
+ ## Change or clear the relationship
135
+
136
+ ```javascript
137
+ await api.resources.publishers.patch({
138
+ id: britishPublisher.id,
139
+ data: { country: france.id },
140
+ returning: 'none'
141
+ })
142
+ await api.resources.publishers.patchRelationship({
143
+ id: britishPublisher.id,
144
+ relationshipName: 'country',
145
+ relationshipData: null
146
+ })
147
+ const cleared = await api.resources.publishers.getRelationship({
148
+ id: britishPublisher.id, relationshipName: 'country'
149
+ })
150
+ console.log('Cleared linkage:', cleared.data)
151
+ ```
152
+
153
+ PATCH can update an attribute and relationship in the same record operation.
154
+ `patchRelationship` takes linkage in `relationshipData`, changes only the named
155
+ relationship and returns undefined.
156
+ Clearing it requires the relationship to allow null. The example ends with
157
+ `cleared.data === null`; linkage reads return a JSON:API linkage document.
158
+
159
+ ## HTTP calls
160
+
161
+ Generated routes use JSON:API. With the server running and the example's fresh
162
+ records, these read commands exercise includes, fieldsets and related filtering.
163
+ Use the IDs returned by your server for a persistent database.
164
+
165
+ ```bash
166
+ curl --globoff 'http://localhost:3000/api/publishers/1?include=country'
167
+ curl --globoff 'http://localhost:3000/api/publishers?include=country&fields[publishers]=name,country&fields[countries]=code'
168
+ curl --globoff 'http://localhost:3000/api/publishers?filter[countryCode]=FR'
169
+
170
+ curl -X PATCH http://localhost:3000/api/publishers/1/relationships/country \
171
+ -H 'Content-Type: application/vnd.api+json' \
172
+ -d '{"data":{"type":"countries","id":"2"}}'
173
+ ```
174
+
175
+ The relationship endpoint body is a linkage document, distinct from a resource
176
+ PATCH body with `data.type`, `data.id` and `data.relationships`. See the
177
+ [API reference](../API.md) for both forms and their error/transaction contracts.
@@ -0,0 +1,184 @@
1
+ ---
2
+ title: "Has-many relationships"
3
+ chapter: 6
4
+ chapter_label: "06"
5
+ ---
6
+
7
+ # 06. Has-many relationships
8
+
9
+ A publisher has many authors; each author belongs to one publisher. The
10
+ `hasMany` declaration points to the logical foreign-key field on the child.
11
+ It does not store an array of IDs in the publisher's table.
12
+
13
+ Insert these blocks in order into the [starting script](03-running-example.md)
14
+ on a fresh database, before the HTTP server starts.
15
+
16
+ ## Define and populate the resources
17
+
18
+ ```javascript
19
+ await api.addResource('publishers', {
20
+ schema: { name: { type: 'string', required: true, indexed: true } },
21
+ relationships: {
22
+ authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
23
+ },
24
+ searchSchema: {
25
+ authorSurname: { type: 'string', actualField: 'authors.surname', filterOperator: 'startsWith' }
26
+ }
27
+ })
28
+ await api.addResource('authors', {
29
+ schema: {
30
+ name: { type: 'string', required: true },
31
+ surname: { type: 'string', required: true, indexed: true },
32
+ publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
33
+ },
34
+ searchSchema: {
35
+ publisherName: { type: 'string', actualField: 'publishers.name', filterOperator: 'contains' }
36
+ }
37
+ })
38
+ await api.resources.publishers.createKnexTable()
39
+ await api.resources.authors.createKnexTable()
40
+
41
+ const frenchPublisher = await api.resources.publishers.post({
42
+ data: { name: 'French Books Inc.' }
43
+ })
44
+ const germanPublisher = await api.resources.publishers.post({
45
+ data: { name: 'German Press GmbH' }
46
+ })
47
+ const emptyPublisher = await api.resources.publishers.post({
48
+ data: { name: 'Global Publishing' }
49
+ })
50
+ const victor = await api.resources.authors.post({
51
+ data: { name: 'Victor', surname: 'Hugo', publisher: frenchPublisher.id }
52
+ })
53
+ const emile = await api.resources.authors.post({
54
+ data: { name: 'Émile', surname: 'Zola', publisher: frenchPublisher.id }
55
+ })
56
+ await api.resources.authors.post({
57
+ data: { name: 'Johann', surname: 'Goethe', publisher: germanPublisher.id }
58
+ })
59
+ const unassigned = await api.resources.authors.post({
60
+ data: { name: 'Unknown', surname: 'Author', publisher: null }
61
+ })
62
+ ```
63
+
64
+ Link children through the public `publisher` alias in `data`, not the
65
+ foreign-key field name. Declaring the inverse `belongsTo` also gives authors
66
+ a public relationship representation. Cross-table filter targets explicitly
67
+ declare `indexed: true`.
68
+
69
+ ## Read linkage and include child records
70
+
71
+ ```javascript
72
+ const identifiers = await api.resources.publishers.get({ id: frenchPublisher.id })
73
+ const embedded = await api.resources.publishers.get({
74
+ id: frenchPublisher.id,
75
+ queryParams: { include: ['authors'] }
76
+ })
77
+ const document = await api.resources.publishers.get({
78
+ id: frenchPublisher.id,
79
+ format: 'jsonapi',
80
+ queryParams: { include: ['authors'] }
81
+ })
82
+ const empty = await api.resources.publishers.get({ id: emptyPublisher.id })
83
+ console.log('Author identifiers:', identifiers.authors)
84
+ console.log('Included authors:', embedded.authors)
85
+ console.log('JSON:API document:', document)
86
+ console.log('Empty relationship:', empty.authors)
87
+ ```
88
+
89
+ Without includes, plain `authors` contains `{ id }` objects for Victor and Émile.
90
+ With includes it contains their selected attributes too. JSON:API retains
91
+ identifier linkage in `data.relationships.authors.data` and puts full child
92
+ records in `included`. An empty to-many relationship is an empty array.
93
+ Discovering child identifiers has a query cost even without fetching attributes.
94
+
95
+ ## Page related records and select fields
96
+
97
+ ```javascript
98
+ const firstAuthorPage = await api.resources.publishers.getRelated({
99
+ id: frenchPublisher.id,
100
+ relationshipName: 'authors',
101
+ queryParams: { sort: ['surname'], page: { number: 1, size: 1 } }
102
+ })
103
+ const sparse = await api.resources.publishers.get({
104
+ id: frenchPublisher.id,
105
+ queryParams: {
106
+ include: ['authors'],
107
+ fields: { publishers: 'name,authors', authors: 'surname' }
108
+ }
109
+ })
110
+ console.log('First related page:', firstAuthorPage)
111
+ console.log('Sparse authors:', sparse.authors)
112
+ ```
113
+
114
+ The first related page contains Hugo and has `meta.pagination.total === 2`.
115
+ `getRelated` returns a collection for a to-many relationship. Its page options
116
+ apply to children; page options on a publisher query apply to publishers.
117
+ The sparse include keeps author IDs and surnames and omits their names. Keep
118
+ `authors` in the parent fieldset when you need its linkage in the response.
119
+
120
+ ## Filter in either direction
121
+
122
+ ```javascript
123
+ const matchingPublishers = await api.resources.publishers.query({
124
+ queryParams: { filters: { authorSurname: 'Hu' } }
125
+ })
126
+ const matchingAuthors = await api.resources.authors.query({
127
+ queryParams: { filters: { publisherName: 'German' } }
128
+ })
129
+ console.log('Publishers with a matching author:', matchingPublishers.data)
130
+ console.log('Authors with a matching publisher:', matchingAuthors.data)
131
+ ```
132
+
133
+ These return French Books Inc. and Johann respectively. The server's search
134
+ schema declares allowed paths and operators. Filtering parents by matching
135
+ children does not itself request an include or restrict an included collection
136
+ to only those matching children.
137
+
138
+ ## Change membership
139
+
140
+ ```javascript
141
+ await api.resources.publishers.postRelationship({
142
+ id: frenchPublisher.id,
143
+ relationshipName: 'authors',
144
+ relationshipData: [{ type: 'authors', id: unassigned.id }]
145
+ })
146
+ await api.resources.publishers.deleteRelationship({
147
+ id: frenchPublisher.id,
148
+ relationshipName: 'authors',
149
+ relationshipData: [{ type: 'authors', id: victor.id }]
150
+ })
151
+ await api.resources.publishers.patch({
152
+ id: frenchPublisher.id,
153
+ data: { authors: [emile.id] },
154
+ returning: 'none'
155
+ })
156
+ const finalLinkage = await api.resources.publishers.getRelationship({
157
+ id: frenchPublisher.id, relationshipName: 'authors'
158
+ })
159
+ const detachedVictor = await api.resources.authors.get({ id: victor.id })
160
+ console.log('Final members:', finalLinkage.data)
161
+ console.log('Detached author still exists:', detachedVictor.name)
162
+ ```
163
+
164
+ POST relationship adds an existing child, DELETE relationship removes specified
165
+ membership, and the parent PATCH replaces the collection with the supplied IDs.
166
+ The final member is Émile. Removing membership does not delete the author:
167
+ Victor remains, with no publisher. Detaching children requires their relationship
168
+ to allow null; required relationships cannot be cleared this way. Relationship
169
+ writes use `relationshipData` and return undefined. Resource writes put their
170
+ record, including any relationships, under `data`.
171
+
172
+ ## HTTP examples
173
+
174
+ After the full example above, the French publisher has only Émile:
175
+
176
+ ```bash
177
+ curl --globoff 'http://localhost:3000/api/publishers/1?include=authors'
178
+ curl --globoff 'http://localhost:3000/api/publishers/1/authors?page[number]=1&page[size]=10'
179
+ curl 'http://localhost:3000/api/publishers/1/relationships/authors'
180
+ ```
181
+
182
+ The related endpoint returns author resources; the relationship endpoint returns
183
+ linkage. Generated HTTP routes use JSON:API documents. Use IDs from your server
184
+ when working with a persistent database.
@@ -0,0 +1,191 @@
1
+ ---
2
+ title: "Polymorphic relationships"
3
+ chapter: 7
4
+ chapter_label: "07"
5
+ ---
6
+
7
+ # 07. Polymorphic relationships
8
+
9
+ A review can belong to a publisher or an author. Its relationship identity has
10
+ two parts: the resource type and ID. A publisher and an author can share an ID
11
+ without sharing reviews.
12
+
13
+ The child declares `belongsToPolymorphic` with allowed target types and the two
14
+ logical storage fields. Each parent declares a reverse `hasMany` with `via`
15
+ pointing to that child relationship. Insert these blocks in order into the
16
+ [starting script](03-running-example.md), using a fresh database.
17
+
18
+ ## Define the relationships and filters
19
+
20
+ ```javascript
21
+ await api.addResource('publishers', {
22
+ schema: { name: { type: 'string', required: true, indexed: true } },
23
+ relationships: { reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } },
24
+ searchSchema: {
25
+ reviewComment: { type: 'string', actualField: 'reviews.comment', filterOperator: 'contains' }
26
+ }
27
+ })
28
+ await api.addResource('authors', {
29
+ schema: { name: { type: 'string', required: true, indexed: true } },
30
+ relationships: { reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } },
31
+ searchSchema: {
32
+ reviewComment: { type: 'string', actualField: 'reviews.comment', filterOperator: 'contains' }
33
+ }
34
+ })
35
+ await api.addResource('reviews', {
36
+ schema: {
37
+ rating: { type: 'number', required: true, min: 1, max: 5 },
38
+ comment: { type: 'string', required: true, indexed: true },
39
+ reviewable_type: { type: 'string', required: true },
40
+ reviewable_id: { type: 'id', required: true }
41
+ },
42
+ relationships: {
43
+ reviewable: {
44
+ belongsToPolymorphic: {
45
+ types: ['publishers', 'authors'],
46
+ typeField: 'reviewable_type',
47
+ idField: 'reviewable_id'
48
+ }
49
+ }
50
+ },
51
+ searchSchema: {
52
+ reviewableName: {
53
+ type: 'string',
54
+ polymorphicField: 'reviewable',
55
+ targetFields: { publishers: 'name', authors: 'name' },
56
+ filterOperator: 'contains'
57
+ }
58
+ }
59
+ })
60
+ await api.resources.publishers.createKnexTable()
61
+ await api.resources.authors.createKnexTable()
62
+ await api.resources.reviews.createKnexTable()
63
+ ```
64
+
65
+ Target name and review comment fields declare `indexed: true` for cross-table
66
+ filtering. `reviewableName` explicitly maps each allowed target type to its
67
+ search field. The two reverse `reviewComment` filters search only reviews of
68
+ the corresponding parent type.
69
+
70
+ ## Write plain records and JSON:API documents
71
+
72
+ ```javascript
73
+ const publisher = await api.resources.publishers.post({
74
+ data: { id: '1', name: 'French Books' }
75
+ })
76
+ const author = await api.resources.authors.post({
77
+ data: { id: '1', name: 'Victor Hugo' }
78
+ })
79
+ const publisherReview = await api.resources.reviews.post({
80
+ data: {
81
+ rating: 4, comment: 'Reliable publisher',
82
+ reviewable: { _type: 'publishers', id: publisher.id }
83
+ }
84
+ })
85
+ const authorReviewDocument = await api.resources.reviews.post({
86
+ format: 'jsonapi',
87
+ document: {
88
+ data: {
89
+ type: 'reviews',
90
+ attributes: { rating: 5, comment: 'Excellent author' },
91
+ relationships: {
92
+ reviewable: { data: { type: 'authors', id: author.id } }
93
+ }
94
+ }
95
+ }
96
+ })
97
+ ```
98
+
99
+ Plain polymorphic input uses `{ _type, id }` under the relationship alias.
100
+ JSON:API uses `{ type, id }` under `data.relationships.reviewable.data`.
101
+ Do not write `reviewable_type` or `reviewable_id` as public attributes. Those
102
+ logical fields describe storage; the public relationship represents them together.
103
+ Plain output uses `_type` to identify the related resource type as well.
104
+
105
+ ## Include either direction
106
+
107
+ ```javascript
108
+ const publisherWithReviews = await api.resources.publishers.get({
109
+ id: publisher.id, queryParams: { include: ['reviews'] }
110
+ })
111
+ const authorWithReviews = await api.resources.authors.get({
112
+ id: author.id, queryParams: { include: ['reviews'] }
113
+ })
114
+ const reviewsWithTargets = await api.resources.reviews.query({
115
+ queryParams: { include: ['reviewable'], sort: ['id'] }
116
+ })
117
+ const reviewDocument = await api.resources.reviews.query({
118
+ format: 'jsonapi',
119
+ queryParams: { include: ['reviewable'], sort: ['id'] }
120
+ })
121
+ console.log('Publisher reviews:', publisherWithReviews.reviews)
122
+ console.log('Author reviews:', authorWithReviews.reviews)
123
+ console.log('Embedded targets:', reviewsWithTargets.data)
124
+ console.log('JSON:API targets:', reviewDocument.included)
125
+ ```
126
+
127
+ Each parent has exactly its own review, despite both having ID `1`. Plain
128
+ included targets contain `_type`, ID and selected attributes. The JSON:API
129
+ `included` array contains both target resources, distinguished by `type` and
130
+ `id`; it must not merge them merely because their IDs match.
131
+
132
+ ## Filter forward and backward
133
+
134
+ ```javascript
135
+ const reviewsOfVictor = await api.resources.reviews.query({
136
+ queryParams: { filters: { reviewableName: 'Victor' } }
137
+ })
138
+ const reliablePublishers = await api.resources.publishers.query({
139
+ queryParams: { filters: { reviewComment: 'Reliable' } }
140
+ })
141
+ const reliableAuthors = await api.resources.authors.query({
142
+ queryParams: { filters: { reviewComment: 'Reliable' } }
143
+ })
144
+ console.log('Reviews of Victor:', reviewsOfVictor.data)
145
+ console.log('Reliable publishers:', reliablePublishers.data)
146
+ console.log('Reliable authors:', reliableAuthors.data)
147
+ ```
148
+
149
+ The forward filter finds the author review. The reverse filter finds French
150
+ Books; the same filter on authors returns no records. These are declared search
151
+ contracts, not arbitrary client-selected SQL paths. Filtering does not itself
152
+ request included resource attributes.
153
+
154
+ ## Move a review to a different target type
155
+
156
+ ```javascript
157
+ await api.resources.reviews.patchRelationship({
158
+ id: publisherReview.id,
159
+ relationshipName: 'reviewable',
160
+ relationshipData: { type: 'authors', id: author.id }
161
+ })
162
+ const publisherLinkage = await api.resources.publishers.getRelationship({
163
+ id: publisher.id, relationshipName: 'reviews'
164
+ })
165
+ const authorLinkage = await api.resources.authors.getRelationship({
166
+ id: author.id, relationshipName: 'reviews'
167
+ })
168
+ console.log('Publisher linkage after move:', publisherLinkage.data)
169
+ console.log('Author linkage after move:', authorLinkage.data)
170
+ ```
171
+
172
+ The target ID stays `1` but its type changes. The publisher then has no reviews,
173
+ and the author has both. Relationship-only writes take `relationshipData` and
174
+ return undefined. Because this schema makes the type and ID fields required,
175
+ the relationship cannot be cleared to null. Use a nullable relationship design
176
+ when reviews are allowed to remain unassigned.
177
+
178
+ ## HTTP representation
179
+
180
+ For a server running the completed example, read either side with:
181
+
182
+ ```bash
183
+ curl --globoff 'http://localhost:3000/api/authors/1?include=reviews'
184
+ curl --globoff 'http://localhost:3000/api/reviews?include=reviewable'
185
+ curl --globoff 'http://localhost:3000/api/reviews?filter[reviewableName]=Victor'
186
+ ```
187
+
188
+ HTTP uses JSON:API type/ID linkage. To move a review, PATCH
189
+ `/api/reviews/{id}/relationships/reviewable` with a body such as
190
+ `{"data":{"type":"publishers","id":"1"}}` and content type
191
+ `application/vnd.api+json`. Use returned IDs for an existing database.