json-rest-api 1.0.29 → 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.
- 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 +634 -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 +100 -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 +3827 -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 +36 -0
- package/index.js +1 -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 +64 -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 +213 -164
- 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 +25 -16
- 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 +116 -2485
- 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/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 +53 -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,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Many-to-many relationships"
|
|
3
|
+
chapter: 8
|
|
4
|
+
chapter_label: "08"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 08. Many-to-many relationships
|
|
8
|
+
|
|
9
|
+
Books can have several authors, and authors can write several books. A pivot
|
|
10
|
+
resource, `book_authors`, represents each connection and can also hold attributes
|
|
11
|
+
such as a contribution role. Declare that resource alongside the two endpoints.
|
|
12
|
+
|
|
13
|
+
Insert the following blocks in order into the [starting script](03-running-example.md)
|
|
14
|
+
with a fresh database, before the HTTP server starts. This chapter uses
|
|
15
|
+
`RestApiKnexPlugin` and ordinary pivot rows. Canonical storage keeps membership
|
|
16
|
+
in separate tenant-scoped links: direct pivot resource rows and their metadata
|
|
17
|
+
do not create or describe those links. The relationship-read/write API remains
|
|
18
|
+
available there, but the direct pivot examples below apply to table-backed storage.
|
|
19
|
+
|
|
20
|
+
## Define the resources
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
await api.addResource('books', {
|
|
24
|
+
schema: { title: { type: 'string', required: true, indexed: true } },
|
|
25
|
+
relationships: {
|
|
26
|
+
authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
|
|
27
|
+
},
|
|
28
|
+
searchSchema: {
|
|
29
|
+
authorName: { type: 'string', actualField: 'authors.name', filterOperator: 'contains' }
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
await api.addResource('authors', {
|
|
33
|
+
schema: { name: { type: 'string', required: true, indexed: true } },
|
|
34
|
+
relationships: {
|
|
35
|
+
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
|
|
36
|
+
},
|
|
37
|
+
searchSchema: {
|
|
38
|
+
bookTitle: { type: 'string', actualField: 'books.title', filterOperator: 'contains' }
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
await api.addResource('book_authors', {
|
|
42
|
+
schema: {
|
|
43
|
+
book_id: { type: 'id', belongsTo: 'books', as: 'book', required: true },
|
|
44
|
+
author_id: { type: 'id', belongsTo: 'authors', as: 'author', required: true },
|
|
45
|
+
contribution: { type: 'string', nullable: true }
|
|
46
|
+
},
|
|
47
|
+
searchSchema: { book: { type: 'id', actualField: 'book_id' } }
|
|
48
|
+
})
|
|
49
|
+
await api.resources.books.createKnexTable()
|
|
50
|
+
await api.resources.authors.createKnexTable()
|
|
51
|
+
await api.resources.book_authors.createKnexTable()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`through` names the pivot resource. `foreignKey` is its logical field pointing
|
|
55
|
+
to the current resource, and `otherKey` points to the related resource. The
|
|
56
|
+
reverse declaration swaps these fields. Storage mapping determines physical
|
|
57
|
+
columns. Each pivot record has its own resource ID.
|
|
58
|
+
|
|
59
|
+
## Create records with membership
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
const neil = await api.resources.authors.post({ data: { name: 'Neil Gaiman' } })
|
|
63
|
+
const terry = await api.resources.authors.post({ data: { name: 'Terry Pratchett' } })
|
|
64
|
+
const goodOmens = await api.resources.books.post({
|
|
65
|
+
data: { title: 'Good Omens', authors: [neil.id, terry.id] }
|
|
66
|
+
})
|
|
67
|
+
const americanGods = await api.resources.books.post({
|
|
68
|
+
format: 'jsonapi',
|
|
69
|
+
document: {
|
|
70
|
+
data: {
|
|
71
|
+
type: 'books',
|
|
72
|
+
attributes: { title: 'American Gods' },
|
|
73
|
+
relationships: { authors: { data: [{ type: 'authors', id: neil.id }] } }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The plain record uses an ID array under `data.authors`; JSON:API uses
|
|
80
|
+
identifier linkage under `data.relationships.authors.data`. These operations
|
|
81
|
+
create the necessary pivot connections. Do not also create direct pivot records
|
|
82
|
+
for those same connections.
|
|
83
|
+
|
|
84
|
+
## Read includes and filter through the pivot
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
const includedBook = await api.resources.books.get({
|
|
88
|
+
id: goodOmens.id, queryParams: { include: ['authors'] }
|
|
89
|
+
})
|
|
90
|
+
const includedDocument = await api.resources.books.get({
|
|
91
|
+
id: goodOmens.id, format: 'jsonapi', queryParams: { include: ['authors'] }
|
|
92
|
+
})
|
|
93
|
+
const booksByNeil = await api.resources.books.query({
|
|
94
|
+
queryParams: { filters: { authorName: 'Neil' }, include: ['authors'] }
|
|
95
|
+
})
|
|
96
|
+
const authorsOfGods = await api.resources.authors.query({
|
|
97
|
+
queryParams: { filters: { bookTitle: 'Gods' } }
|
|
98
|
+
})
|
|
99
|
+
console.log('Good Omens authors:', includedBook.authors)
|
|
100
|
+
console.log('JSON:API authors:', includedDocument.included)
|
|
101
|
+
console.log('Books by Neil:', booksByNeil.data)
|
|
102
|
+
console.log('Authors of Gods:', authorsOfGods.data)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Good Omens includes both authors. Plain responses embed included records;
|
|
106
|
+
JSON:API retains relationship identifiers and places related records in
|
|
107
|
+
`included`. Without an include, membership is represented by IDs rather than
|
|
108
|
+
full author attributes. The filter finds both books by Neil; its Good Omens
|
|
109
|
+
result still includes Terry as well. Filtering a parent does not filter the
|
|
110
|
+
included collection to just matching children. The reverse title filter finds
|
|
111
|
+
Neil alone.
|
|
112
|
+
|
|
113
|
+
## Work with pivot attributes directly
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const colorOfMagic = await api.resources.books.post({
|
|
117
|
+
data: { title: 'The Color of Magic' }
|
|
118
|
+
})
|
|
119
|
+
const contribution = await api.resources.book_authors.post({
|
|
120
|
+
data: { book: colorOfMagic.id, author: terry.id, contribution: 'primary' }
|
|
121
|
+
})
|
|
122
|
+
const omensPivots = await api.resources.book_authors.query({
|
|
123
|
+
queryParams: { filters: { book: goodOmens.id } }
|
|
124
|
+
})
|
|
125
|
+
for (const pivot of omensPivots.data) {
|
|
126
|
+
await api.resources.book_authors.patch({
|
|
127
|
+
id: pivot.id, data: { contribution: 'co-author' }, returning: 'none'
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
const updatedPivots = await api.resources.book_authors.query({
|
|
131
|
+
queryParams: { filters: { book: goodOmens.id } }
|
|
132
|
+
})
|
|
133
|
+
console.log('Direct pivot:', contribution)
|
|
134
|
+
console.log('Updated contributions:', updatedPivots.data)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The pivot is a resource: use its public relationship aliases `book` and `author`
|
|
138
|
+
in `data`. Its declared `book` filter finds the two Good Omens connection
|
|
139
|
+
records. Updating their attributes changes contribution metadata without
|
|
140
|
+
replacing membership. Query the pivot resource to retrieve these attributes;
|
|
141
|
+
a book's author include does not automatically attach pivot attributes to authors.
|
|
142
|
+
Direct pivot creation should not duplicate membership already created through
|
|
143
|
+
`authors`. Define the uniqueness constraints your persistent schema requires.
|
|
144
|
+
|
|
145
|
+
## Remove membership without deleting an author
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
await api.resources.books.deleteRelationship({
|
|
149
|
+
id: goodOmens.id,
|
|
150
|
+
relationshipName: 'authors',
|
|
151
|
+
relationshipData: [{ type: 'authors', id: terry.id }]
|
|
152
|
+
})
|
|
153
|
+
const remainingMembers = await api.resources.books.getRelationship({
|
|
154
|
+
id: goodOmens.id, relationshipName: 'authors'
|
|
155
|
+
})
|
|
156
|
+
const remainingPivots = await api.resources.book_authors.query({
|
|
157
|
+
queryParams: { filters: { book: goodOmens.id } }
|
|
158
|
+
})
|
|
159
|
+
const terryStillExists = await api.resources.authors.get({ id: terry.id })
|
|
160
|
+
console.log('Remaining authors:', remainingMembers.data)
|
|
161
|
+
console.log('Remaining pivot records:', remainingPivots.data)
|
|
162
|
+
console.log('Author remains:', terryStillExists.name)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The Good Omens connection to Terry is removed, including that pivot record's
|
|
166
|
+
contribution metadata. Neil's connection and Terry's author record remain.
|
|
167
|
+
The relationship method returns undefined and takes identifiers in
|
|
168
|
+
`relationshipData`. A resource PATCH with `data: { authors: [...] }`
|
|
169
|
+
replaces the membership set; use relationship POST/DELETE for incremental changes.
|
|
170
|
+
|
|
171
|
+
## HTTP endpoints
|
|
172
|
+
|
|
173
|
+
After running all the blocks, Good Omens has only Neil:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
curl --globoff 'http://localhost:3000/api/books/1?include=authors'
|
|
177
|
+
curl 'http://localhost:3000/api/books/1/relationships/authors'
|
|
178
|
+
curl --globoff 'http://localhost:3000/api/books?filter[authorName]=Neil'
|
|
179
|
+
curl --globoff 'http://localhost:3000/api/book_authors?filter[book]=1'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Generated HTTP endpoints use JSON:API documents, including for the pivot
|
|
183
|
+
resource. Use IDs returned by your server when adapting this example to an
|
|
184
|
+
existing database.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Pagination and sorting"
|
|
3
|
+
chapter: 9
|
|
4
|
+
chapter_label: "09"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 09. Pagination and sorting
|
|
8
|
+
|
|
9
|
+
Pagination applies to the collection queried. A publisher query pages publishers;
|
|
10
|
+
a publisher's to-many `getRelated` call pages its related records. Including
|
|
11
|
+
children does not make the parent's page options paginate those children.
|
|
12
|
+
|
|
13
|
+
Insert these blocks into the [starting script](03-running-example.md)
|
|
14
|
+
on a fresh database, before the server starts.
|
|
15
|
+
|
|
16
|
+
## Define a stable ordering
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
await api.addResource('countries', {
|
|
20
|
+
queryDefaultLimit: 2,
|
|
21
|
+
queryMaxLimit: 3,
|
|
22
|
+
defaultSort: ['name'],
|
|
23
|
+
schema: {
|
|
24
|
+
name: { type: 'string', required: true, search: true },
|
|
25
|
+
code: { type: 'string', required: true }
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
await api.resources.countries.createKnexTable()
|
|
29
|
+
for (const [name, code] of [
|
|
30
|
+
['France', 'FR'], ['Germany', 'DE'], ['Austria', 'AT'],
|
|
31
|
+
['Italy', 'IT'], ['United Kingdom', 'GB']
|
|
32
|
+
]) {
|
|
33
|
+
await api.resources.countries.post({ data: { name, code }, returning: 'none' })
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The resource default sorts by name. A query may override it using
|
|
38
|
+
`queryParams.sort`, for example `['-name']` for descending order. An ID tie-breaker
|
|
39
|
+
provides stable ordering where the selected values are equal. `defaultSort`
|
|
40
|
+
accepts a string or an array of strings; object-shaped defaults are rejected.
|
|
41
|
+
|
|
42
|
+
## Default limits and numbered pages
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
const defaultPage = await api.resources.countries.query({})
|
|
46
|
+
const numbered = await api.resources.countries.query({
|
|
47
|
+
queryParams: { page: { number: 2, size: 2 } }
|
|
48
|
+
})
|
|
49
|
+
const emptyPage = await api.resources.countries.query({
|
|
50
|
+
queryParams: { page: { number: 10, size: 2 } }
|
|
51
|
+
})
|
|
52
|
+
console.log('Default records:', defaultPage.data)
|
|
53
|
+
console.log('Second page:', numbered.data)
|
|
54
|
+
console.log('Numbered metadata:', numbered.meta.pagination)
|
|
55
|
+
console.log('Beyond the last page:', emptyPage.data)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Omitted page options, or `page: {}`, use the capped default limit and omit
|
|
59
|
+
pagination metadata. Here, the default result is Austria and France.
|
|
60
|
+
`page: { number: 2, size: 2 }` selects Germany and Italy. With pagination counts
|
|
61
|
+
enabled, metadata is:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "page": 2, "pageSize": 2, "pageCount": 3, "total": 5, "hasMore": true }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Page 10 returns an empty array. `enablePaginationCounts: false` suppresses the
|
|
68
|
+
count query and count-derived metadata; do not assume every collection response
|
|
69
|
+
contains totals. Every mode respects `queryMaxLimit`.
|
|
70
|
+
|
|
71
|
+
## Cursor pages
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const firstCursorPage = await api.resources.countries.query({
|
|
75
|
+
queryParams: { sort: ['name'], page: { size: 2 } }
|
|
76
|
+
})
|
|
77
|
+
const nextCursor = firstCursorPage.meta.pagination.cursor.next
|
|
78
|
+
const secondCursorPage = await api.resources.countries.query({
|
|
79
|
+
queryParams: { sort: ['name'], page: { size: 2, after: nextCursor } }
|
|
80
|
+
})
|
|
81
|
+
console.log('First cursor page:', firstCursorPage.data)
|
|
82
|
+
console.log('Second cursor page:', secondCursorPage.data)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`page: { size: 2 }` starts cursor pagination. It returns Austria and France;
|
|
86
|
+
the next page returns Germany and Italy. Cursor pages report `hasMore` and,
|
|
87
|
+
when available, an opaque next cursor under `meta.pagination.cursor.next`.
|
|
88
|
+
They do not count the collection. Treat cursors as opaque and keep the same sort,
|
|
89
|
+
filters and fields when continuing. Returned links preserve those options.
|
|
90
|
+
|
|
91
|
+
Use `after` or `before` with a returned cursor. Do not combine either boundary
|
|
92
|
+
with `number`, supply both boundaries, or use an empty cursor string. Omitted
|
|
93
|
+
page sizes use the resource default; explicitly keep the chosen size when
|
|
94
|
+
constructing the next call yourself.
|
|
95
|
+
|
|
96
|
+
## Sparse fields and ordering
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
const sparse = await api.resources.countries.query({
|
|
100
|
+
queryParams: {
|
|
101
|
+
fields: { countries: 'code' },
|
|
102
|
+
sort: ['name'],
|
|
103
|
+
page: { size: 2 }
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
const nextSparse = await api.resources.countries.query({
|
|
107
|
+
queryParams: {
|
|
108
|
+
fields: { countries: 'code' },
|
|
109
|
+
sort: ['name'],
|
|
110
|
+
page: { size: 2, after: sparse.meta.pagination.cursor.next }
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
console.log('Sparse first page:', sparse.data)
|
|
114
|
+
console.log('Sparse second page:', nextSparse.data)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The first sparse page contains IDs and codes AT/FR; the second contains DE/IT.
|
|
118
|
+
The library retains the sort values needed internally without exposing omitted
|
|
119
|
+
fields in the response. Sparse output is not permission to sort by hidden fields.
|
|
120
|
+
|
|
121
|
+
Stored attributes, logical IDs and SQL query projections keep their own sort
|
|
122
|
+
meaning when a search filter with the same name points elsewhere. A distinct
|
|
123
|
+
sortable search alias can target a scalar stored field. Cursor values and
|
|
124
|
+
relationship include `orderBy` follow the same field-resolution rules.
|
|
125
|
+
|
|
126
|
+
## Derived and structured values
|
|
127
|
+
|
|
128
|
+
Use [query projections](15-query-projections.md) when a derived SQL value
|
|
129
|
+
must participate in sorting and cursors. JavaScript computed fields run after
|
|
130
|
+
fetching and are not a replacement for SQL ordering.
|
|
131
|
+
|
|
132
|
+
Object and array fields do not define a generic whole-document order. They are
|
|
133
|
+
excluded from default sortable fields; explicitly making them sortable or using
|
|
134
|
+
them in a default sort rejects schema compilation. Project a scalar JSON value
|
|
135
|
+
when that is the intended ordering, and verify the SQL on your chosen backend.
|
|
136
|
+
Stored SQL values drive cursor comparison; getters format returned values.
|
|
137
|
+
|
|
138
|
+
## HTTP examples
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
curl --globoff 'http://localhost:3000/api/countries?page[number]=2&page[size]=2&sort=name'
|
|
142
|
+
curl --globoff 'http://localhost:3000/api/countries?page[size]=2&sort=name'
|
|
143
|
+
curl --globoff 'http://localhost:3000/api/countries?page[size]=2&sort=name&fields[countries]=code'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
HTTP responses are JSON:API documents. Follow their returned links to continue
|
|
147
|
+
pagination, preserving filters, fields, sort and size. See the
|
|
148
|
+
[migration contract](33-migrating-to-v2.md#pagination-modes-and-counts) for removed
|
|
149
|
+
options and count behavior.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "PUT and PATCH"
|
|
3
|
+
chapter: 10
|
|
4
|
+
chapter_label: "10"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 10. PUT and PATCH
|
|
8
|
+
|
|
9
|
+
PATCH changes supplied values. PUT describes a replacement and requires explicit
|
|
10
|
+
values for persisted attributes and belongs-to relationships that already have
|
|
11
|
+
values. PUT does not silently clear those stored values by omission.
|
|
12
|
+
|
|
13
|
+
Relationship collections have an additional PUT rule: supplying a JSON:API
|
|
14
|
+
`data.relationships` object makes omitted declared collections part of the
|
|
15
|
+
replacement. An empty object can therefore clear collections. PATCH changes
|
|
16
|
+
only the relationships named in its input.
|
|
17
|
+
|
|
18
|
+
The following six blocks form one runnable example. Insert them in order into
|
|
19
|
+
the [starting script](03-running-example.md) before starting its server,
|
|
20
|
+
on a fresh database. JSON:API input makes the relationship-object boundary
|
|
21
|
+
explicit; plain input is described below.
|
|
22
|
+
|
|
23
|
+
## Create the example records
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
await api.addResource('publishers', {
|
|
27
|
+
schema: { name: { type: 'string', required: true } },
|
|
28
|
+
relationships: { books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' } }
|
|
29
|
+
})
|
|
30
|
+
await api.addResource('books', {
|
|
31
|
+
schema: {
|
|
32
|
+
title: { type: 'string', required: true },
|
|
33
|
+
note: { type: 'string', nullable: true },
|
|
34
|
+
publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
await api.resources.publishers.createKnexTable()
|
|
38
|
+
await api.resources.books.createKnexTable()
|
|
39
|
+
const publisher = await api.resources.publishers.post({ data: { name: 'Example Press' } })
|
|
40
|
+
const book = await api.resources.books.post({
|
|
41
|
+
data: { title: 'First edition', note: 'Keep me', publisher: publisher.id }
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The plain create calls still put data inside `data`, and refer to the
|
|
46
|
+
relationship by its public name `publisher`, not its backing column.
|
|
47
|
+
|
|
48
|
+
## PATCH retains omitted values
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const patched = await api.resources.books.patch({
|
|
52
|
+
id: book.id, format: 'jsonapi',
|
|
53
|
+
document: { data: { type: 'books', id: book.id, attributes: { title: 'Second edition' } } }
|
|
54
|
+
})
|
|
55
|
+
console.log(patched.data.attributes, patched.data.relationships.publisher.data)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The title changes. The note remains `Keep me` and the publisher linkage remains.
|
|
59
|
+
A supplied to-many array replaces that relationship's membership; it is not an
|
|
60
|
+
append operation. Use [relationship endpoints](11-relationship-endpoints.md)
|
|
61
|
+
for explicit attachment/removal.
|
|
62
|
+
|
|
63
|
+
## PUT requires explicit existing values
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
let incompletePutError
|
|
67
|
+
try {
|
|
68
|
+
await api.resources.books.put({
|
|
69
|
+
id: book.id, format: 'jsonapi',
|
|
70
|
+
document: { data: { type: 'books', id: book.id, attributes: { title: 'Rejected edition' } } }
|
|
71
|
+
})
|
|
72
|
+
} catch (error) {
|
|
73
|
+
incompletePutError = error
|
|
74
|
+
}
|
|
75
|
+
const afterRejectedPut = await api.resources.books.get({ id: book.id })
|
|
76
|
+
console.log(incompletePutError?.cause?.message, afterRejectedPut.title)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The owned write rejects with `RestApiWriteError`; its cause is a
|
|
80
|
+
`RestApiValidationError` describing the omitted note and publisher. Its
|
|
81
|
+
`transactionOutcome` is `rolledBack`, and the title remains `Second edition`.
|
|
82
|
+
In an application, handle or propagate the error; the catch here lets the
|
|
83
|
+
remaining examples run.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
const replaced = await api.resources.books.put({
|
|
87
|
+
id: book.id, format: 'jsonapi',
|
|
88
|
+
document: {
|
|
89
|
+
data: {
|
|
90
|
+
type: 'books', id: book.id,
|
|
91
|
+
attributes: { title: 'Third edition', note: null },
|
|
92
|
+
relationships: { publisher: { data: { type: 'publishers', id: publisher.id } } }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
console.log(replaced.data.attributes, replaced.data.relationships.publisher.data)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This replacement explicitly clears the nullable note and retains the publisher.
|
|
100
|
+
To clear the publisher, supply `publisher: { data: null }` in JSON:API
|
|
101
|
+
relationships (or `publisher: null` in plain input). Required/non-nullable
|
|
102
|
+
relationships cannot be detached this way. Computed and virtual fields are not
|
|
103
|
+
persisted replacement values; ordinary required-field validation still applies.
|
|
104
|
+
|
|
105
|
+
## PUT's relationship-object boundary
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
await api.resources.publishers.put({
|
|
109
|
+
id: publisher.id, format: 'jsonapi',
|
|
110
|
+
document: { data: { type: 'publishers', id: publisher.id, attributes: { name: 'Renamed Press' } } }
|
|
111
|
+
})
|
|
112
|
+
const retainedBooks = await api.resources.publishers.getRelationship({ id: publisher.id, relationshipName: 'books' })
|
|
113
|
+
await api.resources.publishers.put({
|
|
114
|
+
id: publisher.id, format: 'jsonapi',
|
|
115
|
+
document: {
|
|
116
|
+
data: {
|
|
117
|
+
type: 'publishers', id: publisher.id, attributes: { name: 'Empty Press' },
|
|
118
|
+
relationships: {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
const clearedBooks = await api.resources.publishers.getRelationship({ id: publisher.id, relationshipName: 'books' })
|
|
123
|
+
const detachedBook = await api.resources.books.get({ id: book.id })
|
|
124
|
+
console.log(retainedBooks.data, clearedBooks.data, detachedBook)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The first PUT has no relationships object and keeps the book membership. The
|
|
128
|
+
second includes an empty object and detaches the book. The book record still
|
|
129
|
+
exists; its nullable publisher is cleared. Plain read output omits an unassigned
|
|
130
|
+
to-one relationship. A JSON:API linkage read represents it as `data: null`.
|
|
131
|
+
|
|
132
|
+
The same omission boundary applies to declared many-to-many collections and
|
|
133
|
+
reverse has-one relationships: replacing membership does not delete target
|
|
134
|
+
records. Reverse changes can invoke child PATCH lifecycles and fail when the
|
|
135
|
+
child cannot be detached. Ordinary through-row metadata and canonical links
|
|
136
|
+
have different storage behavior; see [many-to-many relationships](08-many-to-many.md).
|
|
137
|
+
|
|
138
|
+
## PATCH selects the relationship to replace
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
await api.resources.publishers.patch({
|
|
142
|
+
id: publisher.id, format: 'jsonapi',
|
|
143
|
+
document: {
|
|
144
|
+
data: {
|
|
145
|
+
type: 'publishers', id: publisher.id,
|
|
146
|
+
relationships: { books: { data: [{ type: 'books', id: book.id }] } }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
const reattachedBooks = await api.resources.publishers.getRelationship({ id: publisher.id, relationshipName: 'books' })
|
|
151
|
+
const unchangedPublisher = await api.resources.publishers.get({ id: publisher.id })
|
|
152
|
+
console.log(reattachedBooks.data, unchangedPublisher.name)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The book is attached again, and the publisher name remains `Empty Press`.
|
|
156
|
+
For either method, an explicitly supplied empty collection clears membership.
|
|
157
|
+
Omitted PATCH attributes and relationships remain unchanged.
|
|
158
|
+
|
|
159
|
+
## Plain input and choosing the method
|
|
160
|
+
|
|
161
|
+
Plain calls use `data: { title, note, publisher, ... }`. Supplying public
|
|
162
|
+
relationship names causes their conversion into JSON:API relationship entries.
|
|
163
|
+
A PUT that supplies one relationship can therefore clear other omitted
|
|
164
|
+
collections. Use PATCH for a partial update, or explicitly include every
|
|
165
|
+
relationship you intend to retain in a PUT replacement. Hooks that introduce a
|
|
166
|
+
relationships object also participate in this boundary.
|
|
167
|
+
|
|
168
|
+
Use `returning: 'full'`, `'minimal'` or `'none'` to select the write response;
|
|
169
|
+
none/minimal do not skip write validation or mutation. See
|
|
170
|
+
[hooks](13-hooks-and-lifecycle.md) for nested response reads and
|
|
171
|
+
[managed transactions](19-managed-transactions.md) for atomic multi-call changes.
|
|
172
|
+
Unconditional PUT can create a missing resource at the supplied ID; an
|
|
173
|
+
`expectedVersion` condition cannot match a missing row. See
|
|
174
|
+
[version-field migration](22-version-field-migrations.md) for conditional writes.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Relationship endpoints"
|
|
3
|
+
chapter: 11
|
|
4
|
+
chapter_label: "11"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 11. Relationship endpoints
|
|
8
|
+
|
|
9
|
+
Relationship methods are part of `RestApiPlugin`; no extra relationship plugin
|
|
10
|
+
is required. Install a storage plugin and an HTTP connector before registering
|
|
11
|
+
resources. Configure the connector's `mountPath`, for example `/api`, as in the
|
|
12
|
+
[quickstart](../QUICKSTART.md).
|
|
13
|
+
|
|
14
|
+
There are two kinds of read endpoint:
|
|
15
|
+
|
|
16
|
+
| Endpoint | Programmatic method | Response |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `/api/publishers/1/relationships/authors` | `getRelationship` | JSON:API identifier linkage |
|
|
19
|
+
| `/api/publishers/1/authors` | `getRelated` | Related resource records |
|
|
20
|
+
|
|
21
|
+
A linkage read returns a document even when programmatic record format is plain.
|
|
22
|
+
A related read respects `format`: plain to-one reads return a record or null,
|
|
23
|
+
plain to-many reads return a collection with `data`, and JSON:API reads return
|
|
24
|
+
a document. Related reads apply the target's fields, filtering and pagination;
|
|
25
|
+
they do not promise every related record in a single response.
|
|
26
|
+
|
|
27
|
+
## Set up a runnable example
|
|
28
|
+
|
|
29
|
+
Insert these blocks into the [starting script](03-running-example.md)
|
|
30
|
+
on a fresh database, before the HTTP server starts.
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
await api.addResource('publishers', {
|
|
34
|
+
schema: { name: { type: 'string', required: true } },
|
|
35
|
+
relationships: { authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' } }
|
|
36
|
+
})
|
|
37
|
+
await api.addResource('authors', {
|
|
38
|
+
schema: {
|
|
39
|
+
name: { type: 'string', required: true },
|
|
40
|
+
publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
await api.resources.publishers.createKnexTable()
|
|
44
|
+
await api.resources.authors.createKnexTable()
|
|
45
|
+
const publisher = await api.resources.publishers.post({ data: { name: 'Scribner' } })
|
|
46
|
+
const stephen = await api.resources.authors.post({ data: { name: 'Stephen King' } })
|
|
47
|
+
const peter = await api.resources.authors.post({ data: { name: 'Peter Straub' } })
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The inverse relationships are explicitly declared. A `belongsTo` field does not
|
|
51
|
+
automatically create an arbitrary reverse `hasOne` or `hasMany` endpoint.
|
|
52
|
+
|
|
53
|
+
## Add membership and read each representation
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
await api.resources.publishers.postRelationship({
|
|
57
|
+
id: publisher.id,
|
|
58
|
+
relationshipName: 'authors',
|
|
59
|
+
relationshipData: [
|
|
60
|
+
{ type: 'authors', id: stephen.id },
|
|
61
|
+
{ type: 'authors', id: peter.id }
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
const linkage = await api.resources.publishers.getRelationship({
|
|
65
|
+
id: publisher.id, relationshipName: 'authors'
|
|
66
|
+
})
|
|
67
|
+
const related = await api.resources.publishers.getRelated({
|
|
68
|
+
id: publisher.id,
|
|
69
|
+
relationshipName: 'authors',
|
|
70
|
+
queryParams: { fields: { authors: 'name' }, sort: ['name'] }
|
|
71
|
+
})
|
|
72
|
+
console.log('Identifiers:', linkage.data)
|
|
73
|
+
console.log('Records:', related.data)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Linkage contains `{ type: 'authors', id }` identifiers. Related records contain
|
|
77
|
+
IDs and names, ordered Peter then Stephen. Relationship-only writes take
|
|
78
|
+
`relationshipData`, not `data`; HTTP wraps that linkage in a `data` member.
|
|
79
|
+
POST adds members and DELETE removes specified members; both apply only to
|
|
80
|
+
supported to-many relationships. PATCH replaces a to-many set or a to-one target.
|
|
81
|
+
All three relationship write methods return undefined.
|
|
82
|
+
|
|
83
|
+
## Replace and remove members
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
await api.resources.publishers.patchRelationship({
|
|
87
|
+
id: publisher.id,
|
|
88
|
+
relationshipName: 'authors',
|
|
89
|
+
relationshipData: [{ type: 'authors', id: stephen.id }]
|
|
90
|
+
})
|
|
91
|
+
const replaced = await api.resources.publishers.getRelationship({
|
|
92
|
+
id: publisher.id, relationshipName: 'authors'
|
|
93
|
+
})
|
|
94
|
+
await api.resources.publishers.deleteRelationship({
|
|
95
|
+
id: publisher.id,
|
|
96
|
+
relationshipName: 'authors',
|
|
97
|
+
relationshipData: [{ type: 'authors', id: stephen.id }]
|
|
98
|
+
})
|
|
99
|
+
const empty = await api.resources.publishers.getRelationship({
|
|
100
|
+
id: publisher.id, relationshipName: 'authors'
|
|
101
|
+
})
|
|
102
|
+
console.log('After replacement:', replaced.data)
|
|
103
|
+
console.log('After removal:', empty.data)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Replacement leaves Stephen alone; removal leaves an empty array. Authors remain
|
|
107
|
+
in the database. With `hasMany`, detachment clears the child's nullable foreign
|
|
108
|
+
key. With ordinary many-to-many storage, it removes the pivot row. Required
|
|
109
|
+
relationships cannot be cleared by detaching a child.
|
|
110
|
+
|
|
111
|
+
## Change and clear a to-one target
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
await api.resources.authors.patchRelationship({
|
|
115
|
+
id: peter.id,
|
|
116
|
+
relationshipName: 'publisher',
|
|
117
|
+
relationshipData: { type: 'publishers', id: publisher.id }
|
|
118
|
+
})
|
|
119
|
+
const peterPublisher = await api.resources.authors.getRelated({
|
|
120
|
+
id: peter.id, relationshipName: 'publisher'
|
|
121
|
+
})
|
|
122
|
+
await api.resources.authors.patchRelationship({
|
|
123
|
+
id: peter.id, relationshipName: 'publisher', relationshipData: null
|
|
124
|
+
})
|
|
125
|
+
const cleared = await api.resources.authors.getRelationship({
|
|
126
|
+
id: peter.id, relationshipName: 'publisher'
|
|
127
|
+
})
|
|
128
|
+
console.log('Related publisher:', peterPublisher.name)
|
|
129
|
+
console.log('Cleared linkage:', cleared.data)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The related read returns Scribner; the final linkage is null. Use null only when
|
|
133
|
+
the relationship permits it. Polymorphic linkage also requires its target type;
|
|
134
|
+
see [polymorphic relationships](07-polymorphic-relationships.md).
|
|
135
|
+
|
|
136
|
+
## HTTP equivalents
|
|
137
|
+
|
|
138
|
+
After running all blocks, both authors are unassigned. These commands add them,
|
|
139
|
+
read both endpoints, and clear the collection again:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
curl -X POST http://localhost:3000/api/publishers/1/relationships/authors \
|
|
143
|
+
-H 'Content-Type: application/vnd.api+json' \
|
|
144
|
+
-d '{"data":[{"type":"authors","id":"1"},{"type":"authors","id":"2"}]}'
|
|
145
|
+
curl http://localhost:3000/api/publishers/1/relationships/authors
|
|
146
|
+
curl --globoff 'http://localhost:3000/api/publishers/1/authors?fields[authors]=name&sort=name'
|
|
147
|
+
curl -X PATCH http://localhost:3000/api/publishers/1/relationships/authors \
|
|
148
|
+
-H 'Content-Type: application/vnd.api+json' \
|
|
149
|
+
-d '{"data":[]}'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Successful generated relationship writes return HTTP 204 without a body. Use IDs
|
|
153
|
+
returned by the server for persistent databases. Resource PATCH is a different
|
|
154
|
+
body shape: `data.type`, `data.id`, and `data.relationships` alongside any
|
|
155
|
+
attributes being changed.
|
|
156
|
+
|
|
157
|
+
## Permissions and transactions
|
|
158
|
+
|
|
159
|
+
Relationship operations apply their method-specific permission checks and
|
|
160
|
+
resource/target visibility. Linkage discovery is not a bypass for related query
|
|
161
|
+
permissions. Ordinary pivot rows also have their own query visibility; canonical
|
|
162
|
+
links are separate from pivot resource rows. Configure hooks through
|
|
163
|
+
`api.customize` using the [hook guide](13-hooks-and-lifecycle.md).
|
|
164
|
+
|
|
165
|
+
A successful relationship write participates in the library's transaction
|
|
166
|
+
lifecycle. Compose several operations with a handle from
|
|
167
|
+
[`api.transaction`](19-managed-transactions.md). A loop of independent operations
|
|
168
|
+
is not a single atomic transaction. Failures report the
|
|
169
|
+
[write outcome](20-transaction-outcomes.md); do not blindly retry an unknown outcome.
|
|
170
|
+
|
|
171
|
+
Use relationship writes for membership-only changes, related reads for records,
|
|
172
|
+
and resource PATCH when updating attributes and relationships together. Where
|
|
173
|
+
provided, follow response links for discovery and pagination; relative links
|
|
174
|
+
must be resolved against the API URL by clients that require absolute URLs.
|