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,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.