json-rest-api 1.0.28 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +746 -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} +90 -31
- 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 -174
- 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 +67 -330
- 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 +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
- 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 +111 -287
- 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 +109 -44
- 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 +142 -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 -2509
- 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 -1069
- 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 -385
- 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 -145
- 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 +38 -69
- 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 +39 -82
- 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 -1238
- 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 -150
- 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 -118
- package/tests/fixtures/api-configs.js +0 -2509
- 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-cursor-multifield.test.js +0 -643
- 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/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Getting started"
|
|
3
|
+
chapter: 1
|
|
4
|
+
chapter_label: "01"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 01. Getting started
|
|
8
|
+
|
|
9
|
+
The guide uses a book catalog with countries, publishers, authors, books and
|
|
10
|
+
book-author links. Start with countries here; subsequent chapters add the
|
|
11
|
+
related resources. The [quickstart](../QUICKSTART.md) provides a complete
|
|
12
|
+
Express server with related publishers and authors.
|
|
13
|
+
|
|
14
|
+
Use Node 24 or newer; development verification runs on Node 24. These examples
|
|
15
|
+
use the revised contract in the [migration guide](33-migrating-to-v2.md). Until
|
|
16
|
+
its release is published, install the intended library tarball instead of the
|
|
17
|
+
registry package.
|
|
18
|
+
|
|
19
|
+
## Install and create a resource
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm init -y
|
|
23
|
+
npm pkg set type=module
|
|
24
|
+
npm install json-rest-api knex better-sqlite3
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Save this as `index.js`. The database is in memory, so records disappear when
|
|
28
|
+
the process exits. Resource schemas are flat field maps. Related resources are
|
|
29
|
+
not declared in this first example because their definitions come later.
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
import { JsonRestApi, RestApiPlugin, RestApiKnexPlugin } from 'json-rest-api'
|
|
33
|
+
import knexLib from 'knex'
|
|
34
|
+
|
|
35
|
+
const knex = knexLib({
|
|
36
|
+
client: 'better-sqlite3',
|
|
37
|
+
connection: { filename: ':memory:' },
|
|
38
|
+
useNullAsDefault: true
|
|
39
|
+
})
|
|
40
|
+
const api = new JsonRestApi({
|
|
41
|
+
name: 'book-catalog-api',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await api.use(RestApiPlugin)
|
|
46
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
47
|
+
await api.addResource('countries', {
|
|
48
|
+
schema: {
|
|
49
|
+
name: { type: 'string', required: true, max: 100, search: true },
|
|
50
|
+
code: { type: 'string', max: 2, unique: true, search: true }
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
await api.resources.countries.createKnexTable()
|
|
54
|
+
|
|
55
|
+
const country = await api.resources.countries.post({
|
|
56
|
+
data: { name: 'United States', code: 'US' }
|
|
57
|
+
})
|
|
58
|
+
const refetched = await api.resources.countries.get({ id: country.id })
|
|
59
|
+
console.log('Country:', refetched)
|
|
60
|
+
|
|
61
|
+
const matches = await api.resources.countries.query({
|
|
62
|
+
queryParams: {
|
|
63
|
+
filters: { code: 'US' },
|
|
64
|
+
sort: ['name'],
|
|
65
|
+
page: { number: 1, size: 10 }
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
console.log('Matches:', matches.data)
|
|
69
|
+
|
|
70
|
+
const changed = await api.resources.countries.patch({
|
|
71
|
+
id: country.id,
|
|
72
|
+
data: { name: 'United States of America' },
|
|
73
|
+
returning: 'minimal'
|
|
74
|
+
})
|
|
75
|
+
console.log('Changed identifier:', changed)
|
|
76
|
+
|
|
77
|
+
const document = await api.resources.countries.get({
|
|
78
|
+
id: country.id,
|
|
79
|
+
format: 'jsonapi'
|
|
80
|
+
})
|
|
81
|
+
console.log('JSON:API attributes:', document.data.attributes)
|
|
82
|
+
} finally {
|
|
83
|
+
await knex.destroy()
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run `node index.js`. The first record has name `United States` and code `US`;
|
|
88
|
+
the query returns that record. PATCH returns `{ type: 'countries', id: country.id }`.
|
|
89
|
+
The final JSON:API read has `data.attributes.name` equal to
|
|
90
|
+
`United States of America`.
|
|
91
|
+
|
|
92
|
+
When adapting later examples to this script, insert their operations inside the
|
|
93
|
+
`try` block after resource registration and before the database is destroyed.
|
|
94
|
+
|
|
95
|
+
## Logging
|
|
96
|
+
|
|
97
|
+
Logging is silent unless you supply a logger. For example:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const api = new JsonRestApi({
|
|
101
|
+
name: 'app',
|
|
102
|
+
logger: { warn: console.warn, error: console.error }
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The logger can supply `trace`, `debug`, `info`, `warn`, `error` and `fatal` methods.
|
|
107
|
+
Missing methods are silent. Your logger controls formatting and filtering.
|
|
108
|
+
Resource and connector diagnostics use the library's bounded error formatting;
|
|
109
|
+
see the [API reference](../API.md) for diagnostic and error contracts.
|
|
110
|
+
The old `log` and `logging` constructor options are rejected.
|
|
111
|
+
|
|
112
|
+
## Database configuration
|
|
113
|
+
|
|
114
|
+
`RestApiKnexPlugin` uses the supplied Knex instance. Install the driver for the
|
|
115
|
+
selected database. For PostgreSQL, use `npm install pg` and configure:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
const knex = knexLib({
|
|
119
|
+
client: 'pg',
|
|
120
|
+
connection: {
|
|
121
|
+
host: '127.0.0.1',
|
|
122
|
+
user: 'your_username',
|
|
123
|
+
password: 'your_password',
|
|
124
|
+
database: 'your_database_name',
|
|
125
|
+
port: 5432
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For MySQL 8, use `npm install mysql2` and configure:
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
const knex = knexLib({
|
|
134
|
+
client: 'mysql2',
|
|
135
|
+
connection: {
|
|
136
|
+
host: '127.0.0.1',
|
|
137
|
+
user: 'your_username',
|
|
138
|
+
password: 'your_password',
|
|
139
|
+
database: 'your_database_name',
|
|
140
|
+
port: 3306
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The verified combinations are SQLite through better-sqlite3, PostgreSQL 16
|
|
146
|
+
through pg and MySQL 8 through mysql2. Review the
|
|
147
|
+
[backend capabilities](30-backend-capabilities.md) before switching: precision,
|
|
148
|
+
collation, schema evolution and transaction behavior have backend-specific
|
|
149
|
+
limits. Other Knex clients and server versions are unverified.
|
|
150
|
+
|
|
151
|
+
## Method arguments and response options
|
|
152
|
+
|
|
153
|
+
Call resources through `api.resources.countries`. Methods accept operation
|
|
154
|
+
parameters first and optional application context second. Plain write records
|
|
155
|
+
go in `data`; JSON:API write documents go in `document`. Supply exactly one of
|
|
156
|
+
these keys. The target ID, transaction handle and response options
|
|
157
|
+
remain outside the record. Query selection goes in `queryParams`.
|
|
158
|
+
|
|
159
|
+
| Option | Values | Programmatic default |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `format` | `'plain'`, `'jsonapi'` | `'plain'` |
|
|
162
|
+
| `returning` | `'none'`, `'minimal'`, `'full'` | `'full'` |
|
|
163
|
+
|
|
164
|
+
`format` selects the output representation independently of input. Plain `get`
|
|
165
|
+
returns a record; plain `query` returns a collection whose `data` contains
|
|
166
|
+
records. JSON:API output is a document with `data`. Either output can be requested
|
|
167
|
+
with either input key. Record attributes never become top-level method parameters.
|
|
168
|
+
|
|
169
|
+
For POST, PUT and PATCH, `returning: 'minimal'` returns `{ type, id }` in plain
|
|
170
|
+
format or `{ data: { type, id } }` in JSON:API format. `returning: 'none'` returns
|
|
171
|
+
undefined. Full responses run the resource read/enrichment path, including its
|
|
172
|
+
visibility rules and selected fields. DELETE and relationship writes return
|
|
173
|
+
undefined. `returning` does not change read results.
|
|
174
|
+
|
|
175
|
+
You can set programmatic defaults when installing `RestApiPlugin`, override
|
|
176
|
+
them in a resource definition, and override those defaults for a particular
|
|
177
|
+
call. Each setting accepts a single value, not a per-method map or boolean.
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
await api.use(RestApiPlugin, { format: 'plain', returning: 'minimal' })
|
|
181
|
+
await api.addResource('countries', {
|
|
182
|
+
returning: 'full',
|
|
183
|
+
schema: { name: { type: 'string', required: true } }
|
|
184
|
+
})
|
|
185
|
+
const result = await api.resources.countries.post({
|
|
186
|
+
data: { name: 'Canada' },
|
|
187
|
+
returning: 'none'
|
|
188
|
+
})
|
|
189
|
+
// result is undefined
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This is an alternative configuration for a fresh API instance; do not install
|
|
193
|
+
the plugin or register the same resource twice in the initial script.
|
|
194
|
+
|
|
195
|
+
PUT creates a missing target or replaces a visible existing record according
|
|
196
|
+
to schema requirements, defaults and nullability. PATCH changes supplied fields.
|
|
197
|
+
Both accept the target ID as `params.id` or in the input record; conflicting
|
|
198
|
+
IDs are rejected. For exact payloads, relationships and pagination, see the
|
|
199
|
+
[API reference](../API.md).
|
|
200
|
+
|
|
201
|
+
## HTTP endpoints
|
|
202
|
+
|
|
203
|
+
The library provides [Express](../QUICKSTART.md) and
|
|
204
|
+
[Fastify](23-fastify.md) connectors. Install the connector before resource
|
|
205
|
+
registration. Express exposes `api.http.express.router` and
|
|
206
|
+
`api.http.express.notFoundRouter`, which your application mounts with `app.use`.
|
|
207
|
+
The quickstart contains the complete setup and shutdown code.
|
|
208
|
+
|
|
209
|
+
Generated HTTP routes explicitly use JSON:API format and full write responses.
|
|
210
|
+
They do not inherit a resource's programmatic plain/minimal defaults. There is
|
|
211
|
+
no separate simplified-transport switch.
|
|
212
|
+
|
|
213
|
+
For a server exposing the countries resource at `/api`, create a country with:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
curl -i -X POST http://localhost:3000/api/countries \
|
|
217
|
+
-H 'Content-Type: application/vnd.api+json' \
|
|
218
|
+
-d '{"data":{"type":"countries","attributes":{"name":"United Kingdom","code":"GB"}}}'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Successful POST returns 201 with a JSON:API document. Use its `data.id` in a
|
|
222
|
+
subsequent GET, for example `curl http://localhost:3000/api/countries/1` on an
|
|
223
|
+
otherwise empty database. Successful PUT/PATCH with a response body return 200;
|
|
224
|
+
DELETE returns 204 with no body. A custom endpoint selecting no write result
|
|
225
|
+
returns 204; it must not send a JSON body with that status. These describe normal
|
|
226
|
+
successful responses; application hooks can reject requests or customize responses.
|
|
227
|
+
|
|
228
|
+
## Limits and resource configuration
|
|
229
|
+
|
|
230
|
+
Configure limits through documented plugin/resource options, rather than
|
|
231
|
+
mutating compiled resource metadata after registration.
|
|
232
|
+
|
|
233
|
+
| Option | Default | Purpose |
|
|
234
|
+
| --- | --- | --- |
|
|
235
|
+
| `queryDefaultLimit` | `20` | Default collection size |
|
|
236
|
+
| `queryMaxLimit` | `100` | Maximum requested collection size |
|
|
237
|
+
| `includeDepthLimit` | `3` | Maximum nested include depth |
|
|
238
|
+
| `enablePaginationCounts` | `true` | Counts for numbered pagination |
|
|
239
|
+
| `idProperty` | `'id'` | Physical primary-key column for table-backed resources |
|
|
240
|
+
| `normalizeId` | Built-in normalizer | Canonical resource identifiers |
|
|
241
|
+
|
|
242
|
+
Counts do not turn cursor pagination into numbered pagination. Omitted or empty
|
|
243
|
+
page options use the default collection cap without pagination metadata.
|
|
244
|
+
`sortableFields` and `defaultSort` control resource sorting; see
|
|
245
|
+
[pagination and ordering](09-pagination-and-sorting.md) for their contracts.
|
|
246
|
+
|
|
247
|
+
## Logical IDs and Physical Columns
|
|
248
|
+
|
|
249
|
+
For table-backed resources, `idProperty` defines the physical primary-key column. The API surface still uses the logical resource id.
|
|
250
|
+
|
|
251
|
+
- plain writes use `data.id`
|
|
252
|
+
- JSON:API writes use `document.data.id`
|
|
253
|
+
- responses expose `id` or `data.id`
|
|
254
|
+
- the resource id is not part of `attributes`
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
await api.addResource('profiles', {
|
|
258
|
+
idProperty: 'user_id',
|
|
259
|
+
schema: {
|
|
260
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
261
|
+
displayName: { type: 'string', required: true },
|
|
262
|
+
loginCount: { type: 'number', defaultTo: 0 }
|
|
263
|
+
},
|
|
264
|
+
tableName: 'profiles'
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
For table-backed resources, storage columns default to snake_case, so `displayName` maps to `display_name` and `loginCount` maps to `login_count`. Use `storage.column` for a per-field override, or `storage: { naming: 'exact' }` when physical column names should match logical field names exactly.
|
|
269
|
+
|
|
270
|
+
## ID normalization
|
|
271
|
+
|
|
272
|
+
`normalizeId` lets you canonicalize resource identifiers before the library reads, writes, validates, or links records.
|
|
273
|
+
|
|
274
|
+
The default normalizer already handles the common cases:
|
|
275
|
+
|
|
276
|
+
- trims surrounding whitespace from strings
|
|
277
|
+
- converts finite numbers and bigints to strings
|
|
278
|
+
- rejects values that normalize to an empty identifier
|
|
279
|
+
|
|
280
|
+
You only need to provide `normalizeId` when your resource ids need additional canonicalization, such as uppercasing, lowercasing, or removing formatting characters.
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
await api.use(RestApiPlugin, {
|
|
284
|
+
normalizeId: (value) => {
|
|
285
|
+
if (value === null || value === undefined) return null
|
|
286
|
+
|
|
287
|
+
const normalized = String(value).trim()
|
|
288
|
+
return normalized ? normalized.toUpperCase() : null
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
await api.addResource('countries', {
|
|
293
|
+
// Override the plugin-wide normalizer for this one resource
|
|
294
|
+
normalizeId: (value) => {
|
|
295
|
+
if (value === null || value === undefined) return null
|
|
296
|
+
|
|
297
|
+
const normalized = String(value).trim()
|
|
298
|
+
return normalized ? normalized.toLowerCase() : null
|
|
299
|
+
},
|
|
300
|
+
schema: {
|
|
301
|
+
name: { type: 'string', required: true }
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Normalization is applied consistently to:
|
|
307
|
+
|
|
308
|
+
- `get`, `put`, `patch`, `delete`, `getRelated`, and `getRelationship` parent ids
|
|
309
|
+
- explicit resource ids in `POST`, `PUT`, and `PATCH` documents
|
|
310
|
+
- relationship identifiers supplied to `postRelationship`, `patchRelationship`, and `deleteRelationship`
|
|
311
|
+
|
|
312
|
+
The resource-level `normalizeId` override also applies to relationship identifiers when that resource appears as the related target.
|
|
313
|
+
|
|
314
|
+
If an id normalizes to an empty value:
|
|
315
|
+
|
|
316
|
+
- existing-resource operations behave as `not_found`
|
|
317
|
+
- explicit `POST` document ids are rejected as validation errors on `data.id`
|
|
318
|
+
|
|
319
|
+
For full examples of table creation, physical column naming, and migration generation, see [Knex Schema and Migrations](21-schema-and-migrations.md).
|
|
320
|
+
|
|
321
|
+
## Transactions and database helpers
|
|
322
|
+
|
|
323
|
+
Use `api.transaction(callback, context?)` for a unit of library writes and pass
|
|
324
|
+
its managed handle as each operation's `transaction` parameter. Raw Knex
|
|
325
|
+
transactions are available for direct SQL and reads; they are not accepted as
|
|
326
|
+
owners of library writes. See [managed transactions](19-managed-transactions.md)
|
|
327
|
+
and [transaction outcomes](20-transaction-outcomes.md) before composing operations.
|
|
328
|
+
|
|
329
|
+
`api.knex.instance` exposes the supplied Knex instance and
|
|
330
|
+
`api.knex.capabilities` describes detected backend capabilities. Table-backed
|
|
331
|
+
resources provide `createKnexTable`, `introspectKnexTableSnapshot`,
|
|
332
|
+
`generateKnexMigration`, `generateKnexMigrationDiff`, `addKnexFields` and
|
|
333
|
+
`alterKnexFields`. These helpers have different applicability to canonical
|
|
334
|
+
storage; use the [schema and migration guide](21-schema-and-migrations.md)
|
|
335
|
+
for examples and limitations.
|
|
336
|
+
|
|
337
|
+
Continue with [the starting point](03-running-example.md) or return to
|
|
338
|
+
the [guide index](index.md).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Resources and relationships"
|
|
3
|
+
chapter: 2
|
|
4
|
+
chapter_label: "02"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 02. Resources and relationships
|
|
8
|
+
|
|
9
|
+
These tutorials cover resource CRUD, relationship linkage and related-record
|
|
10
|
+
queries. Use the same public relationship names in plain input and JSON:API
|
|
11
|
+
relationships; backing foreign-key columns are storage details.
|
|
12
|
+
|
|
13
|
+
Each runnable scenario starts from a fresh database unless it explicitly says
|
|
14
|
+
otherwise. Do not concatenate different chapters' resource declarations into
|
|
15
|
+
one API: several deliberately reuse the same resource names.
|
|
16
|
+
|
|
17
|
+
| Tutorial | What it demonstrates |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| [Starting point](03-running-example.md) | API, database and Express setup. |
|
|
20
|
+
| [Manipulating and searching tables](04-creating-and-querying.md) | CRUD, declared filters, custom search and sparse fields. |
|
|
21
|
+
| [Belongs-to relationships](05-belongs-to.md) | Public relationship input, linkage, includes, filtering and clearing. |
|
|
22
|
+
| [Has-many relationships](06-has-many.md) | Reverse membership, includes, related queries, attachment and removal. |
|
|
23
|
+
| [Polymorphic relationships](07-polymorphic-relationships.md) | Typed identifiers, distinct resources sharing an ID, includes and search. |
|
|
24
|
+
| [Many-to-many relationships](08-many-to-many.md) | Membership and ordinary pivot metadata, with canonical-storage distinctions. |
|
|
25
|
+
| [Pagination and ordering](09-pagination-and-sorting.md) | Default caps, numbered/cursor pages, sorting and sparse results. |
|
|
26
|
+
| [PUT and PATCH](10-put-and-patch.md) | Explicit replacement, omitted-value rejection and relationship-object boundaries. |
|
|
27
|
+
| [Relationship URLs](11-relationship-endpoints.md) | Identifier linkage versus related records, and relationship mutation endpoints. |
|
|
28
|
+
|
|
29
|
+
For query projections and their SQL ordering, use
|
|
30
|
+
[Query Projections](15-query-projections.md). For mandatory visibility before
|
|
31
|
+
counts and pagination, use [Row Policies](17-row-policies.md) or
|
|
32
|
+
[Autofiltering](16-autofiltering.md), according to the required policy.
|
|
33
|
+
See [backend capabilities](30-backend-capabilities.md) for database differences.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Running example"
|
|
3
|
+
chapter: 3
|
|
4
|
+
chapter_label: "03"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 03. Running example
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { JsonRestApi, RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api';
|
|
11
|
+
import knexLib from 'knex';
|
|
12
|
+
import util from 'util';
|
|
13
|
+
import express from 'express';
|
|
14
|
+
|
|
15
|
+
// Utility used throughout this guide
|
|
16
|
+
const inspect = (obj) => util.inspect(obj, { depth: 8 })
|
|
17
|
+
|
|
18
|
+
// Create a Knex instance connected to SQLite in-memory database
|
|
19
|
+
const knex = knexLib({
|
|
20
|
+
client: 'better-sqlite3',
|
|
21
|
+
connection: {
|
|
22
|
+
filename: ':memory:'
|
|
23
|
+
},
|
|
24
|
+
useNullAsDefault: true
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Create API instance
|
|
28
|
+
const api = new JsonRestApi({ name: 'book-catalog-api' });
|
|
29
|
+
|
|
30
|
+
// Install plugins
|
|
31
|
+
await api.use(RestApiPlugin);
|
|
32
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
33
|
+
await api.use(ExpressPlugin, { mountPath: '/api' }); // Links are generated under /api
|
|
34
|
+
|
|
35
|
+
/// *** ...programmatic calls here... ***
|
|
36
|
+
|
|
37
|
+
// Create the express server and add the API's routes
|
|
38
|
+
const app = express();
|
|
39
|
+
app.use(api.http.express.router);
|
|
40
|
+
app.use(api.http.express.notFoundRouter);
|
|
41
|
+
|
|
42
|
+
app.listen(3000, () => {
|
|
43
|
+
console.log('Express server started on port 3000. API available at http://localhost:3000/api');
|
|
44
|
+
}).on('error', (err) => {
|
|
45
|
+
console.error('Failed to start server:', err);
|
|
46
|
+
process.exit(1)
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Note that every time we provide a snippet of code, it will be assumed that
|
|
51
|
+
|
|
52
|
+
1. The script is edited in the section `/// *** ...programmatic calls here... ***`
|
|
53
|
+
2. The code is stopped with CTRL-C and then restarted.
|
|
54
|
+
3. The code proposed in each snippet _replaces_ the code provided earlier.
|
|
55
|
+
|
|
56
|
+
This will ensure that each example has a fresh start.
|
|
57
|
+
|
|
58
|
+
Each example will be introduced programmatically first, and then via HTTP. The HTTP calls will be run assuming that the API calls (and any data created with them) stay. The use of the in-memory database will be assumed, which means that the data will start afresh each time.
|
|
59
|
+
|
|
60
|
+
## Logical fields and storage columns
|
|
61
|
+
|
|
62
|
+
Resource definitions describe logical fields and relationships. Table-backed
|
|
63
|
+
storage maps those fields to physical columns: snake_case by default, explicit
|
|
64
|
+
`storage.column` overrides, or exact naming when configured. Canonical storage
|
|
65
|
+
uses its own allocation and descriptor mapping. A logical field name is not a
|
|
66
|
+
portable physical SQL column name.
|
|
67
|
+
|
|
68
|
+
For a definition such as `country_id: { type: 'id', belongsTo: 'countries',
|
|
69
|
+
as: 'country' }`, callers write the relationship through
|
|
70
|
+
`data: { country: countryId }`. Plain output contains a `country` object
|
|
71
|
+
with its ID; an unassigned to-one relationship is omitted from plain output.
|
|
72
|
+
Including the country adds its selected attributes.
|
|
73
|
+
JSON:API output represents linkage under `data.relationships.country` and
|
|
74
|
+
included records under `included`.
|
|
75
|
+
|
|
76
|
+
Hooks operate at specific processing stages. The write pipeline converts plain
|
|
77
|
+
input into its internal JSON:API representation before validation and storage.
|
|
78
|
+
Do not assume every hook receives raw columns or the original public payload.
|
|
79
|
+
Use the [hook guide](13-hooks-and-lifecycle.md) and the
|
|
80
|
+
[API reference](../API.md) for the context available at each stage.
|
|
81
|
+
|
|
82
|
+
Search schemas name the filters callers may use and map them to logical fields
|
|
83
|
+
or declared related-resource fields. Raw SQL remains available through Knex,
|
|
84
|
+
but code that uses physical tables/columns must account for the selected storage
|
|
85
|
+
backend and does not automatically inherit resource access policies.
|
|
86
|
+
|
|
87
|
+
---
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Creating and querying records"
|
|
3
|
+
chapter: 4
|
|
4
|
+
chapter_label: "04"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 04. Creating and querying records
|
|
8
|
+
|
|
9
|
+
This example uses countries without relationships. Insert the blocks below in
|
|
10
|
+
order into the [starting script](03-running-example.md), with a fresh
|
|
11
|
+
database and before starting the HTTP server.
|
|
12
|
+
|
|
13
|
+
## Declare the record and search schemas
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
await api.addResource('countries', {
|
|
17
|
+
schema: {
|
|
18
|
+
name: { type: 'string', required: true, max: 100, search: true },
|
|
19
|
+
code: { type: 'string', max: 2, nullable: true, unique: true, search: true },
|
|
20
|
+
population: { type: 'number', nullable: true }
|
|
21
|
+
},
|
|
22
|
+
searchSchema: {
|
|
23
|
+
name: { type: 'string', filterOperator: '=' },
|
|
24
|
+
nameContains: { type: 'string', actualField: 'name', filterOperator: 'contains' },
|
|
25
|
+
populationRange: { type: 'array', actualField: 'population', filterOperator: 'between' },
|
|
26
|
+
words: {
|
|
27
|
+
type: 'string', oneOf: ['name', 'code'],
|
|
28
|
+
filterOperator: 'contains', splitBy: ' ', matchAll: true
|
|
29
|
+
},
|
|
30
|
+
nameOrCode: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
applyFilter (query, input, { column }) {
|
|
33
|
+
query.where(function () {
|
|
34
|
+
this.whereRaw('LOWER(??) LIKE LOWER(?)', [column('name'), `%${input}%`])
|
|
35
|
+
.orWhereRaw('LOWER(??) = LOWER(?)', [column('code'), input])
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
await api.resources.countries.createKnexTable()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The record schema validates stored data. `searchSchema` declares public filter
|
|
45
|
+
names and their meaning. An explicit search field overrides the same field
|
|
46
|
+
inferred from `search: true`; other inferred fields remain available. Here,
|
|
47
|
+
`name` explicitly uses equality and `code` remains searchable through its marker.
|
|
48
|
+
The record's `population` field is searchable through `populationRange` only.
|
|
49
|
+
|
|
50
|
+
Filter names need not be column names. `actualField` maps an alias to a logical
|
|
51
|
+
resource field. `oneOf` applies a filter across several fields. Search schemas
|
|
52
|
+
are an allowlist and input-validation contract, not arbitrary client-supplied SQL.
|
|
53
|
+
|
|
54
|
+
## Create, patch, replace and delete records
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
const france = await api.resources.countries.post({
|
|
58
|
+
data: { name: 'France', code: 'FR', population: 68 }
|
|
59
|
+
})
|
|
60
|
+
const italy = await api.resources.countries.post({
|
|
61
|
+
data: { name: 'Italyy', code: 'IT', population: 59 }
|
|
62
|
+
})
|
|
63
|
+
const germany = await api.resources.countries.post({
|
|
64
|
+
data: { name: 'Germ', code: 'DE', population: 84 }
|
|
65
|
+
})
|
|
66
|
+
await api.resources.countries.patch({
|
|
67
|
+
id: germany.id, data: { name: 'Germany' }, returning: 'none'
|
|
68
|
+
})
|
|
69
|
+
const patched = await api.resources.countries.get({ id: germany.id })
|
|
70
|
+
const replaced = await api.resources.countries.put({
|
|
71
|
+
id: italy.id, data: { name: 'Italy', code: null, population: null }
|
|
72
|
+
})
|
|
73
|
+
console.log('Patched:', patched)
|
|
74
|
+
console.log('Replaced:', replaced)
|
|
75
|
+
await api.resources.countries.delete({ id: italy.id })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
These population values are illustrative numbers for demonstrating filters.
|
|
79
|
+
PATCH changes the supplied name while retaining Germany's code and population.
|
|
80
|
+
PUT replaces Italy's record and explicitly clears its nullable fields. A
|
|
81
|
+
replacement must include attributes that already have values; omitting them
|
|
82
|
+
rejects with a complete-replacement validation error. Use null explicitly when
|
|
83
|
+
the schema permits clearing a value. PUT can also create a missing target. DELETE returns
|
|
84
|
+
undefined. Writes always place record attributes inside `data`.
|
|
85
|
+
|
|
86
|
+
## Query and filter
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
await api.resources.countries.post({
|
|
90
|
+
data: { name: 'United States', code: 'US', population: 330 }
|
|
91
|
+
})
|
|
92
|
+
await api.resources.countries.post({
|
|
93
|
+
data: { name: 'United Kingdom', code: 'GB', population: 67 }
|
|
94
|
+
})
|
|
95
|
+
await api.resources.countries.post({
|
|
96
|
+
data: { name: 'Austria', code: 'AT', population: 9 }
|
|
97
|
+
})
|
|
98
|
+
const exact = await api.resources.countries.query({
|
|
99
|
+
queryParams: { filters: { name: 'France' } }
|
|
100
|
+
})
|
|
101
|
+
const byCode = await api.resources.countries.query({
|
|
102
|
+
queryParams: { filters: { code: 'DE' } }
|
|
103
|
+
})
|
|
104
|
+
const contains = await api.resources.countries.query({
|
|
105
|
+
queryParams: { filters: { nameContains: 'United' }, sort: ['name'] }
|
|
106
|
+
})
|
|
107
|
+
const range = await api.resources.countries.query({
|
|
108
|
+
queryParams: { filters: { populationRange: [60, 90] }, sort: ['name'] }
|
|
109
|
+
})
|
|
110
|
+
console.log('Exact:', exact.data)
|
|
111
|
+
console.log('By code:', byCode.data)
|
|
112
|
+
console.log('Contains:', contains.data)
|
|
113
|
+
console.log('Range:', range.data)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The results are France, Germany, the two United countries, and France/Germany/
|
|
117
|
+
United Kingdom respectively. Query returns an object with a `data` array, not a
|
|
118
|
+
bare array. Pagination metadata is supplied for explicit pagination requests;
|
|
119
|
+
it is not guaranteed on every collection. Multiple filter keys combine as
|
|
120
|
+
constraints. The selected operator and database collation determine string
|
|
121
|
+
comparison behavior; avoid assuming all databases ignore case.
|
|
122
|
+
|
|
123
|
+
## Search multiple fields and words
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
const allWords = await api.resources.countries.query({
|
|
127
|
+
queryParams: { filters: { words: 'United US' } }
|
|
128
|
+
})
|
|
129
|
+
const custom = await api.resources.countries.query({
|
|
130
|
+
queryParams: { filters: { nameOrCode: 'at' }, sort: ['name'] }
|
|
131
|
+
})
|
|
132
|
+
console.log('All words:', allWords.data)
|
|
133
|
+
console.log('Custom filter:', custom.data)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`words` splits on spaces and requires every term to match at least one of name
|
|
137
|
+
or code. `United US` matches United States: one term matches its name, the other
|
|
138
|
+
its code. Setting `matchAll: false` would allow any term to match.
|
|
139
|
+
|
|
140
|
+
The custom filter searches a case-folded name substring or an exact case-folded
|
|
141
|
+
code. It finds Austria and United States for `at`, not United Kingdom.
|
|
142
|
+
`applyFilter(query, input, { column, value, context, scopeName })` runs
|
|
143
|
+
synchronously and mutates the supplied Knex builder. `column(field)` translates
|
|
144
|
+
logical fields and `value(field, input)` converts values for their storage
|
|
145
|
+
representation. Group OR conditions so they do not bypass other filters or
|
|
146
|
+
access constraints. This example binds user input as values; SQL LIKE wildcard
|
|
147
|
+
characters in that input retain their SQL meaning. Custom SQL functions and
|
|
148
|
+
collations must be verified on the database you deploy.
|
|
149
|
+
|
|
150
|
+
## Select fields and paginate
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
const page = await api.resources.countries.query({
|
|
154
|
+
queryParams: {
|
|
155
|
+
fields: { countries: 'name,code' },
|
|
156
|
+
sort: ['name'],
|
|
157
|
+
page: { number: 1, size: 2 }
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
console.log('Selected fields:', page.data)
|
|
161
|
+
console.log('Pagination:', page.meta.pagination)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This returns Austria and France, with IDs, names and codes but no populations.
|
|
165
|
+
The numbered pagination total is five. Fieldsets select output and do not grant
|
|
166
|
+
access to hidden fields. See [pagination and ordering](09-pagination-and-sorting.md)
|
|
167
|
+
for cursors and collection limits.
|
|
168
|
+
|
|
169
|
+
## HTTP queries
|
|
170
|
+
|
|
171
|
+
With the example server running:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
curl --globoff 'http://localhost:3000/api/countries?filter[nameContains]=United'
|
|
175
|
+
curl --globoff 'http://localhost:3000/api/countries?filter[words]=United%20US'
|
|
176
|
+
curl --globoff 'http://localhost:3000/api/countries?fields[countries]=name,code&page[number]=1&page[size]=2&sort=name'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
HTTP filters use `filter[...]`; programmatic queries use `queryParams.filters`.
|
|
180
|
+
Generated HTTP responses are JSON:API documents. For payload changes from the
|
|
181
|
+
older API, use the [migration guide](33-migrating-to-v2.md).
|