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.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- 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.
|