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
|
@@ -1,808 +0,0 @@
|
|
|
1
|
-
# 2.2 Manipulating and searching tables with no relationships
|
|
2
|
-
|
|
3
|
-
## Setting up the schema
|
|
4
|
-
|
|
5
|
-
First of all, define a resource with a schema:
|
|
6
|
-
|
|
7
|
-
```javascript
|
|
8
|
-
// Countries table
|
|
9
|
-
await api.addResource('countries', {
|
|
10
|
-
schema: {
|
|
11
|
-
name: { type: 'string', required: true, max: 100, search: true },
|
|
12
|
-
code: { type: 'string', max: 2, unique: true, search: true }, // ISO country code
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
await api.resources.countries.createKnexTable()
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Adding data
|
|
19
|
-
|
|
20
|
-
Note that we create the database table at the same time.
|
|
21
|
-
This resource has just two fields, name and code, both searchable.
|
|
22
|
-
|
|
23
|
-
Programmatically you can `post` and `get` easily:
|
|
24
|
-
|
|
25
|
-
```javascript
|
|
26
|
-
const addedFrance = await api.resources.countries.post({ name: 'France', code: 'FR' });
|
|
27
|
-
console.log('Added record :', inspect(addedFrance))
|
|
28
|
-
|
|
29
|
-
const fetchedFrance = await api.resources.countries.get({ id: addedFrance.id });
|
|
30
|
-
console.log('Refetched record:', inspect(fetchedFrance))
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
After the logging messages, you will see:
|
|
34
|
-
|
|
35
|
-
```text
|
|
36
|
-
Added record : { id: '1', name: 'France', code: 'FR' }
|
|
37
|
-
Refetched record: { id: '1', name: 'France', code: 'FR' }
|
|
38
|
-
Express server started on port 3000. API available at http://localhost:3000/api
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
You can do the same thing talking to the server directly (although you will be dealing with JSON:API results).
|
|
42
|
-
Leaving the code as it is, you will add nother country:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
$ curl -i -X POST -H "Content-Type: application/vnd.api+json" -d '{
|
|
47
|
-
"data": {
|
|
48
|
-
"type": "countries",
|
|
49
|
-
"attributes": {
|
|
50
|
-
"name": "United Kingdom",
|
|
51
|
-
"code": "UK"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}' http://localhost:3000/api/countries
|
|
55
|
-
```
|
|
56
|
-
```text
|
|
57
|
-
HTTP/1.1 204 No Content
|
|
58
|
-
X-Powered-By: Express
|
|
59
|
-
Location: /api/countries/2
|
|
60
|
-
ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI"
|
|
61
|
-
Date: Wed, 23 Jul 2025 07:54:09 GMT
|
|
62
|
-
Connection: keep-alive
|
|
63
|
-
Keep-Alive: timeout=5
|
|
64
|
-
```
|
|
65
|
-
```bash
|
|
66
|
-
$ curl -i -X GET http://localhost:3000/api/countries/2
|
|
67
|
-
```
|
|
68
|
-
```txt
|
|
69
|
-
HTTP/1.1 200 OK
|
|
70
|
-
X-Powered-By: Express
|
|
71
|
-
Content-Type: application/vnd.api+json; charset=utf-8
|
|
72
|
-
Content-Length: 169
|
|
73
|
-
ETag: W/"a9-lnEVXaZ/V6qra0YgjpoEBUTZ3EY"
|
|
74
|
-
Date: Wed, 23 Jul 2025 07:54:12 GMT
|
|
75
|
-
Connection: keep-alive
|
|
76
|
-
Keep-Alive: timeout=5
|
|
77
|
-
|
|
78
|
-
{"data":{"type":"countries","id":"2","attributes":{"name":"United Kingdom","code":"UK"},"links":{"self":"/api/countries/2"}},"links":{"self":"/api/countries/2"}}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Note that in "transport" mode after the POST the record was not returned to the client: instead, a status `204 No Content` was returned. However, the client will be aware of the ID of the newly created record thanks to the `Location` header.
|
|
82
|
-
|
|
83
|
-
## Manipulating data
|
|
84
|
-
|
|
85
|
-
Replace the data commands with these:
|
|
86
|
-
|
|
87
|
-
```javascript
|
|
88
|
-
await api.addResource('countries', {
|
|
89
|
-
schema: {
|
|
90
|
-
id: { type: 'string' },
|
|
91
|
-
name: { type: 'string', required: true, max: 100, search: true, filterOperator: 'like' },
|
|
92
|
-
code: { type: 'string', max: 2, unique: true, search: true }, // ISO country code
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
await api.resources.countries.createKnexTable()
|
|
96
|
-
|
|
97
|
-
const fr = await api.resources.countries.post({ name: 'France', code: 'FR' });
|
|
98
|
-
const it = await api.resources.countries.post({ name: 'Italyy', code: 'IT' }); // Typo intentional
|
|
99
|
-
const de = await api.resources.countries.post({ name: 'Germ', code: 'DE' }); // Typo intentional
|
|
100
|
-
|
|
101
|
-
// Patching Germany. It will only change the name
|
|
102
|
-
await api.resources.countries.patch({id: de.id, name: 'Germany' })
|
|
103
|
-
let deFromDb = await api.resources.countries.get({ id: de.id });
|
|
104
|
-
|
|
105
|
-
// Putting France. Note that this will actually reset any attributes that were not passed
|
|
106
|
-
await api.resources.countries.put({id: it.id, name: 'Italy' })
|
|
107
|
-
let itFromDb = await api.resources.countries.get({ id: it.id });
|
|
108
|
-
|
|
109
|
-
console.log('Patched record (Germany):', inspect(deFromDb))
|
|
110
|
-
console.log('Put record: (Italy)', inspect(itFromDb))
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
The result will be:
|
|
114
|
-
|
|
115
|
-
```text
|
|
116
|
-
Patched record (Germany): { id: '3', name: 'Germany', code: 'DE' }
|
|
117
|
-
Put record: (Italy) { id: '2', name: 'Italy', code: null }
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
As you can see, using PUT on Italy was a problem: since put didn't include the `code` field, and since PUT assumes a FULL record, the `code` field was reset to null. On the other hand, since the method PATCH assumes a partial update, the update for Germany did not _not_ overwrite the `code` field. This is a very important distinction, and it's the reason why most clients avoid PUT calls.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
## Search (Filtering)
|
|
124
|
-
|
|
125
|
-
The API supports powerful filtering capabilities out of the box for any fields you've marked as `search: true` in your schema.
|
|
126
|
-
|
|
127
|
-
**Important Note about Query Results in Simplified Mode:**
|
|
128
|
-
|
|
129
|
-
When using `query()` in simplified mode (the default for programmatic access), the return value is an object containing:
|
|
130
|
-
- `data`: The array of matching records
|
|
131
|
-
- `meta`: Metadata about the results (e.g., pagination info)
|
|
132
|
-
- `links`: Links for pagination and related resources
|
|
133
|
-
|
|
134
|
-
For example:
|
|
135
|
-
```javascript
|
|
136
|
-
const result = await api.resources.countries.query({ /* ... */ });
|
|
137
|
-
// result is: { data: [...], meta: {...}, links: {...} }
|
|
138
|
-
// To access the records: result.data
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
**Programmatic Example: Searching for countries**
|
|
142
|
-
|
|
143
|
-
Change the code to add some countries:
|
|
144
|
-
|
|
145
|
-
```javascript
|
|
146
|
-
const fr = await api.resources.countries.post({ name: 'France', code: 'FR' });
|
|
147
|
-
const it = await api.resources.countries.post({ name: 'Italy', code: 'IT' });
|
|
148
|
-
const de = await api.resources.countries.post({ name: 'Germany', code: 'DE' });
|
|
149
|
-
const au = await api.resources.countries.post({ name: 'Australia', code: 'AU' });
|
|
150
|
-
const at = await api.resources.countries.post({ name: 'Austria', code: 'AT' });
|
|
151
|
-
const ge = await api.resources.countries.post({ name: 'Georgia', code: 'GE' });
|
|
152
|
-
|
|
153
|
-
const searchAustralia = await api.resources.countries.query({
|
|
154
|
-
queryParams: {
|
|
155
|
-
filters: {
|
|
156
|
-
name: 'Australia'
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
// HTTP: GET /api/countries?filter[name]=Australia
|
|
161
|
-
// Returns: {
|
|
162
|
-
// data: [{ id: '2', name: 'Australia', code: 'AU' }]
|
|
163
|
-
// }
|
|
164
|
-
|
|
165
|
-
const searchAustr = await api.resources.countries.query({
|
|
166
|
-
queryParams: {
|
|
167
|
-
filters: {
|
|
168
|
-
name: 'Austr'
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
// HTTP: GET /api/countries?filter[name]=Austr
|
|
173
|
-
// Returns: {
|
|
174
|
-
// data: []
|
|
175
|
-
// }
|
|
176
|
-
|
|
177
|
-
console.log('Search for "Australia":', inspect(searchAustralia))
|
|
178
|
-
console.log('Search for "Austr":', inspect(searchAustr))
|
|
179
|
-
// Note: searchAustralia and searchAustr contain { data, meta, links } objects
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
The result will be:
|
|
183
|
-
|
|
184
|
-
```
|
|
185
|
-
Search for Australia: { data: [ { id: '4', name: 'Australia', code: 'AU' } ], meta: {...}, links: {...} }
|
|
186
|
-
Search for "Austr": { data: [], meta: {...}, links: {...} }
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
It's clear that the search is only matching precise results.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
There are two ways to enable search on a field in your schema. The first one is the one we are currently using, with `search: true`.
|
|
193
|
-
As seen above, the only time filtering works is when there is an exact match.
|
|
194
|
-
However, rather than `true` or `false`, `search` can also be an object.
|
|
195
|
-
Changing the definition of `name` in the `countries` resource to this:
|
|
196
|
-
|
|
197
|
-
```javascript
|
|
198
|
-
name: { type: 'string', required: true, max: 100, search: { filterOperator: 'like' } },
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Will give you the expected results:
|
|
202
|
-
|
|
203
|
-
```text
|
|
204
|
-
Search for Australia: [ { id: '4', name: 'Australia', code: 'AU' } ]
|
|
205
|
-
Search for "Austr": [
|
|
206
|
-
{ id: '4', name: 'Australia', code: 'AU' },
|
|
207
|
-
{ id: '5', name: 'Austria', code: 'AT' }
|
|
208
|
-
]
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
This is the
|
|
212
|
-
|
|
213
|
-
**Available operators for `filterOperator`:**
|
|
214
|
-
- `'='` - Exact match (default)
|
|
215
|
-
- `'like'` - Partial match with % wildcards automatically added on both sides
|
|
216
|
-
- `'>'`, `'>='`, `'<'`, `'<='` - Comparison operators for numeric/date fields
|
|
217
|
-
- Other SQL operators are passed through directly (e.g., `'!='`, `'<>'`)
|
|
218
|
-
|
|
219
|
-
Note: Only `'like'` receives special handling (automatic % wildcards). All other operators are passed directly to the SQL query.
|
|
220
|
-
|
|
221
|
-
You can also define multiple search patterns from a single field:
|
|
222
|
-
|
|
223
|
-
```javascript
|
|
224
|
-
await api.addResource('countries', {
|
|
225
|
-
schema: {
|
|
226
|
-
name: {
|
|
227
|
-
type: 'string', required: true, search: {
|
|
228
|
-
name: { filterOperator: '=', type: 'string' },
|
|
229
|
-
nameLike: { filterOperator: 'like', type: 'string' }
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
code: { type: 'string', unique: true, search: true }
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
await api.resources.countries.createKnexTable()
|
|
236
|
-
|
|
237
|
-
await api.resources.countries.post({ name: 'Georgia', code: 'GE' });
|
|
238
|
-
await api.resources.countries.post({ name: 'France', code: 'FR' });
|
|
239
|
-
await api.resources.countries.post({ name: 'Italy', code: 'IT' });
|
|
240
|
-
await api.resources.countries.post({ name: 'Germany', code: 'DE' });
|
|
241
|
-
await api.resources.countries.post({ name: 'Australia', code: 'AU' });
|
|
242
|
-
await api.resources.countries.post({ name: 'Austria', code: 'AT' });
|
|
243
|
-
|
|
244
|
-
const searchAustralia = await api.resources.countries.query({
|
|
245
|
-
queryParams: {
|
|
246
|
-
filters: {
|
|
247
|
-
name: 'Australia'
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
// HTTP: GET /api/countries?filter[name]=Australia
|
|
252
|
-
// Returns: {
|
|
253
|
-
// data: [{ id: '2', name: 'Australia', code: 'AU' }]
|
|
254
|
-
// }
|
|
255
|
-
|
|
256
|
-
const searchAustr = await api.resources.countries.query({
|
|
257
|
-
queryParams: {
|
|
258
|
-
filters: {
|
|
259
|
-
nameLike: 'Austr'
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
// HTTP: GET /api/countries?filter[nameLike]=Austr
|
|
264
|
-
// Returns: {
|
|
265
|
-
// data: [{ id: '2', name: 'Australia', code: 'AU' }, { id: '3', name: 'Austria', code: 'AT' }]
|
|
266
|
-
// }
|
|
267
|
-
|
|
268
|
-
console.log('Search for "Australia":', inspect(searchAustralia))
|
|
269
|
-
console.log('Search for "Austr":', inspect(searchAustr))
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
This is very powerful in that it allows you to define multiple ways of filtering a field depending on needs.
|
|
273
|
-
|
|
274
|
-
There is another, even more powerful way to define how to search in a resource: define a whole searchSchema that is completely independent to the main schema.
|
|
275
|
-
|
|
276
|
-
Under the hood, `rest-api-plugin` actually creates a `searchSchema` object based on the option on the default schema. However, it's very possible for an attribute to define a `searchSchema` directly:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
```javascript
|
|
280
|
-
await api.addResource('countries', {
|
|
281
|
-
schema: {
|
|
282
|
-
name: { type: 'string', required: true, max: 100 },
|
|
283
|
-
code: { type: 'string', max: 2, unique: true }
|
|
284
|
-
},
|
|
285
|
-
searchSchema: {
|
|
286
|
-
// Define searchable fields explicitly
|
|
287
|
-
name: { type: 'string', filterOperator: '=' },
|
|
288
|
-
code: { type: 'string', filterOperator: '=' },
|
|
289
|
-
nameLike: { type: 'string', actualField: 'name', filterOperator: 'like' }
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
await api.resources.countries.createKnexTable()
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Note that the definition above is functionally _identical_ to the one provided a few paragraphs above.
|
|
296
|
-
|
|
297
|
-
**Important: When you define a `searchSchema`, it takes precedence over fields marked with `search: true` in the main schema.** However, fields marked with `search: true` that are NOT in the explicit `searchSchema` will be automatically added to enable searching. This allows you to:
|
|
298
|
-
- Use `searchSchema` to override specific field behaviors
|
|
299
|
-
- Still benefit from `search: true` markers for additional fields
|
|
300
|
-
- Have the explicit `searchSchema` always win in case of conflicts
|
|
301
|
-
|
|
302
|
-
The `searchSchema` gives you:
|
|
303
|
-
|
|
304
|
-
* Complete control where needed: Override specific fields while keeping the convenience of `search: true` for others
|
|
305
|
-
* No mixing of concerns: Your data schema stays focused on data validation and storage, while searchSchema handles complex search behavior
|
|
306
|
-
* Flexibility to extend: Add virtual fields or complex filters via searchSchema while basic fields use `search: true`
|
|
307
|
-
* Best of both worlds: Explicit control for complex cases, automatic setup for simple ones
|
|
308
|
-
|
|
309
|
-
`searchSchema` also defines the public filter names your API accepts. Those keys do not need to match database columns or even resource field names. Treat `searchSchema` as an allowlist plus normalization layer for filter input: the actual meaning of each key is still implemented by the backend through mappings such as `actualField`, multi-field rules such as `oneOf`, standard operators, or custom `applyFilter` logic. It is not a portable filter DSL.
|
|
310
|
-
|
|
311
|
-
The merge rules and validation surface are shared across storage backends. Advanced execution semantics are still backend-owned. In practice, that means alias filters such as `oneOf` and custom `applyFilter` functions should be validated against the backend you actually run, and the SQL-heavy examples in this chapter assume the Knex-backed query layer.
|
|
312
|
-
|
|
313
|
-
`searchSchema` also gives you the ability to define a search field that will search in multiple fields. For example:
|
|
314
|
-
|
|
315
|
-
```javascript
|
|
316
|
-
searchSchema: {
|
|
317
|
-
search: {
|
|
318
|
-
type: 'string',
|
|
319
|
-
oneOf: ['name', 'code'],
|
|
320
|
-
filterOperator: 'like'
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const searchGe = await api.resources.countries.query({
|
|
325
|
-
queryParams: {
|
|
326
|
-
filters: {
|
|
327
|
-
search: 'ge'
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
// HTTP: GET /api/countries?filter[search]=ge
|
|
332
|
-
// Returns: {
|
|
333
|
-
// data: [{ id: '3', name: 'Georgia', code: 'GE' }, { id: '6', name: 'Germany', code: 'DE' }]
|
|
334
|
-
// }
|
|
335
|
-
|
|
336
|
-
console.log('Search for "ge":', inspect(searchGe))
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
Will return:
|
|
340
|
-
|
|
341
|
-
```
|
|
342
|
-
Search for "ge": { data: [
|
|
343
|
-
{ id: '3', name: 'Georgia', code: 'GE' },
|
|
344
|
-
{ id: '6', name: 'Germany', code: 'DE' }
|
|
345
|
-
], meta: {...}, links: {...} }
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
This common pattern will give you the ability to create "global" search fields that will look in multiple fields.
|
|
349
|
-
|
|
350
|
-
### Merging searchSchema with search:true Fields
|
|
351
|
-
|
|
352
|
-
When you combine `searchSchema` with fields marked `search: true`, the library intelligently merges them:
|
|
353
|
-
|
|
354
|
-
```javascript
|
|
355
|
-
await api.addResource('products', {
|
|
356
|
-
schema: {
|
|
357
|
-
name: { type: 'string', search: true }, // Will be added to searchSchema
|
|
358
|
-
description: { type: 'string', search: true }, // Will be added to searchSchema
|
|
359
|
-
price: { type: 'number', search: true }, // Overridden by searchSchema below
|
|
360
|
-
category_id: { type: 'number' }, // Not searchable
|
|
361
|
-
sku: { type: 'string', search: true } // Will be added to searchSchema
|
|
362
|
-
},
|
|
363
|
-
searchSchema: {
|
|
364
|
-
// Explicit searchSchema entries take precedence
|
|
365
|
-
price: {
|
|
366
|
-
type: 'number',
|
|
367
|
-
filterOperator: 'between' // Overrides the search:true with custom operator
|
|
368
|
-
},
|
|
369
|
-
category_name: { // Virtual field (doesn't exist in schema)
|
|
370
|
-
type: 'string',
|
|
371
|
-
actualField: 'category.name',
|
|
372
|
-
filterOperator: 'like'
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// Result: The effective searchSchema includes:
|
|
378
|
-
// - name (from search:true, uses default '=' operator)
|
|
379
|
-
// - description (from search:true, uses default '=' operator)
|
|
380
|
-
// - sku (from search:true, uses default '=' operator)
|
|
381
|
-
// - price (from explicit searchSchema, uses 'between' operator)
|
|
382
|
-
// - category_name (from explicit searchSchema, virtual field)
|
|
383
|
-
|
|
384
|
-
// You can now search using all these fields:
|
|
385
|
-
const results = await api.resources.products.query({
|
|
386
|
-
queryParams: {
|
|
387
|
-
filters: {
|
|
388
|
-
name: 'Widget', // From search:true
|
|
389
|
-
price: [10, 100], // From searchSchema with 'between'
|
|
390
|
-
category_name: 'Electronics' // Virtual field from searchSchema
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
This merge behavior gives you the best of both worlds:
|
|
397
|
-
- Quick setup with `search: true` for simple fields
|
|
398
|
-
- Precise control with `searchSchema` for complex requirements
|
|
399
|
-
- No need to repeat fields in searchSchema that already have `search: true`
|
|
400
|
-
|
|
401
|
-
### Multi-word Search with AND Logic
|
|
402
|
-
|
|
403
|
-
The `oneOf` search feature becomes even more powerful when combined with `splitBy` and `matchAll` options. This allows you to search for multiple words where ALL must appear somewhere in the specified fields.
|
|
404
|
-
|
|
405
|
-
```javascript
|
|
406
|
-
await api.addResource('countries', {
|
|
407
|
-
schema: {
|
|
408
|
-
name: { type: 'string', required: true },
|
|
409
|
-
code: { type: 'string', unique: true }
|
|
410
|
-
},
|
|
411
|
-
searchSchema: {
|
|
412
|
-
search: {
|
|
413
|
-
type: 'string',
|
|
414
|
-
oneOf: ['name', 'code'],
|
|
415
|
-
filterOperator: 'like',
|
|
416
|
-
splitBy: ' ', // Split search terms by space
|
|
417
|
-
matchAll: true // Require ALL terms to match (AND logic)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
await api.resources.countries.createKnexTable()
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
With this configuration, searching becomes much more precise:
|
|
425
|
-
|
|
426
|
-
```javascript
|
|
427
|
-
// Add some countries
|
|
428
|
-
await api.resources.countries.post({ name: 'United States', code: 'US' });
|
|
429
|
-
await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
|
|
430
|
-
await api.resources.countries.post({ name: 'United Arab Emirates', code: 'AE' });
|
|
431
|
-
await api.resources.countries.post({ name: 'South Africa', code: 'ZA' });
|
|
432
|
-
|
|
433
|
-
// Search for "united states" - both words must appear
|
|
434
|
-
const results = await api.resources.countries.query({
|
|
435
|
-
queryParams: {
|
|
436
|
-
filters: {
|
|
437
|
-
search: 'united states'
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
// HTTP: GET /api/countries?filter[search]=united%20states
|
|
442
|
-
// Returns: {
|
|
443
|
-
// data: [{ id: '1', name: 'United States', code: 'US' }]
|
|
444
|
-
// }
|
|
445
|
-
|
|
446
|
-
console.log('Found:', results);
|
|
447
|
-
// Note: results contains { data, meta, links } - access results.data for the array
|
|
448
|
-
// Note: results now contains { data, meta, links } - access results.data for the array
|
|
449
|
-
// Returns: [{ id: '1', name: 'United States', code: 'US' }]
|
|
450
|
-
// Does NOT return United Kingdom or United Arab Emirates
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
**How it works:**
|
|
454
|
-
|
|
455
|
-
1. The search term "united states" is split by space into ["united", "states"]
|
|
456
|
-
2. With `matchAll: true`, the query requires BOTH terms to appear
|
|
457
|
-
3. Each term can appear in ANY of the fields listed in `oneOf`
|
|
458
|
-
4. The SQL generated looks like:
|
|
459
|
-
|
|
460
|
-
```sql
|
|
461
|
-
WHERE (
|
|
462
|
-
(countries.name LIKE '%united%' OR countries.code LIKE '%united%')
|
|
463
|
-
AND
|
|
464
|
-
(countries.name LIKE '%states%' OR countries.code LIKE '%states%')
|
|
465
|
-
)
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
**More examples:**
|
|
469
|
-
|
|
470
|
-
```javascript
|
|
471
|
-
// Search for "south africa" - finds only South Africa
|
|
472
|
-
const southAfrica = await api.resources.countries.query({
|
|
473
|
-
queryParams: { filters: { search: 'south africa' } }
|
|
474
|
-
});
|
|
475
|
-
// HTTP: GET /api/countries?filter[search]=south%20africa
|
|
476
|
-
// Returns: {
|
|
477
|
-
// data: [{ id: '4', name: 'South Africa', code: 'ZA' }]
|
|
478
|
-
// }
|
|
479
|
-
|
|
480
|
-
// Search for "united arab" - finds only United Arab Emirates
|
|
481
|
-
const uae = await api.resources.countries.query({
|
|
482
|
-
queryParams: { filters: { search: 'united arab' } }
|
|
483
|
-
});
|
|
484
|
-
// HTTP: GET /api/countries?filter[search]=united%20arab
|
|
485
|
-
// Returns: {
|
|
486
|
-
// data: [{ id: '3', name: 'United Arab Emirates', code: 'AE' }]
|
|
487
|
-
// }
|
|
488
|
-
|
|
489
|
-
// Single word searches still work normally
|
|
490
|
-
const allUnited = await api.resources.countries.query({
|
|
491
|
-
queryParams: { filters: { search: 'united' } }
|
|
492
|
-
});
|
|
493
|
-
// HTTP: GET /api/countries?filter[search]=united
|
|
494
|
-
// Returns: {
|
|
495
|
-
// data: [
|
|
496
|
-
// { id: '1', name: 'United States', code: 'US' },
|
|
497
|
-
// { id: '2', name: 'United Kingdom', code: 'UK' },
|
|
498
|
-
// { id: '3', name: 'United Arab Emirates', code: 'AE' }
|
|
499
|
-
// ]
|
|
500
|
-
// }
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
**Alternative configurations:**
|
|
504
|
-
|
|
505
|
-
You can also use different separators and OR logic:
|
|
506
|
-
|
|
507
|
-
```javascript
|
|
508
|
-
searchSchema: {
|
|
509
|
-
// Comma-separated OR search
|
|
510
|
-
tags: {
|
|
511
|
-
type: 'string',
|
|
512
|
-
oneOf: ['tags', 'categories', 'keywords'],
|
|
513
|
-
filterOperator: 'like',
|
|
514
|
-
splitBy: ',', // Split by comma
|
|
515
|
-
matchAll: false // OR logic (default) - match ANY term
|
|
516
|
-
},
|
|
517
|
-
|
|
518
|
-
// Exact match with AND logic
|
|
519
|
-
codes: {
|
|
520
|
-
type: 'string',
|
|
521
|
-
oneOf: ['primary_code', 'secondary_code'],
|
|
522
|
-
filterOperator: '=', // Exact match
|
|
523
|
-
splitBy: ' ',
|
|
524
|
-
matchAll: true // All codes must match exactly
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
This feature is particularly useful for:
|
|
530
|
-
- Full-text search functionality where users type multiple words
|
|
531
|
-
- Tag or keyword searches where all terms must be present
|
|
532
|
-
- Product searches matching multiple criteria
|
|
533
|
-
- Finding records that match complex multi-word queries
|
|
534
|
-
|
|
535
|
-
### Custom Search Functions
|
|
536
|
-
|
|
537
|
-
If you need even more complex searches, you can use `searchSchema` to define search fields with custom query logic:
|
|
538
|
-
|
|
539
|
-
```javascript
|
|
540
|
-
await api.addResource('countries', {
|
|
541
|
-
schema: {
|
|
542
|
-
name: { type: 'string', required: true },
|
|
543
|
-
code: { type: 'string', unique: true }
|
|
544
|
-
},
|
|
545
|
-
searchSchema: {
|
|
546
|
-
// Standard fields
|
|
547
|
-
name: { type: 'string', filterOperator: '=' },
|
|
548
|
-
code: { type: 'string', filterOperator: '=' },
|
|
549
|
-
|
|
550
|
-
// Custom search using a function
|
|
551
|
-
nameOrCode: {
|
|
552
|
-
type: 'string',
|
|
553
|
-
applyFilter: function(query, filterValue) {
|
|
554
|
-
// Custom SQL logic: case-insensitive search in name OR exact match on code
|
|
555
|
-
query.where(function() {
|
|
556
|
-
this.whereRaw('LOWER(name) LIKE LOWER(?)', [`%${filterValue}%`])
|
|
557
|
-
.orWhereRaw('LOWER(code) = LOWER(?)', [filterValue]);
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
await api.resources.countries.createKnexTable()
|
|
564
|
-
|
|
565
|
-
await api.resources.countries.post({ name: 'United States', code: 'US' });
|
|
566
|
-
await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
|
|
567
|
-
await api.resources.countries.post({ name: 'United Arab Emirates', code: 'AE' });
|
|
568
|
-
await api.resources.countries.post({ name: 'South Africa', code: 'ZA' });
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
When `filterOperator` is a function instead of an operator string, it receives:
|
|
572
|
-
- `query` - The Knex query builder instance
|
|
573
|
-
- `filterValue` - The search value from the user
|
|
574
|
-
- `fieldName` - The name of the search field (optional third parameter)
|
|
575
|
-
|
|
576
|
-
This gives you complete control over the SQL generated for that search field.
|
|
577
|
-
|
|
578
|
-
**Example usage:**
|
|
579
|
-
|
|
580
|
-
```javascript
|
|
581
|
-
// This custom search will find both:
|
|
582
|
-
// - Countries with "at" in the name (United States, United Kingdom, United Arab Emirates)
|
|
583
|
-
// - Countries with code "at"
|
|
584
|
-
const results = await api.resources.countries.query({
|
|
585
|
-
queryParams: {
|
|
586
|
-
filters: {
|
|
587
|
-
nameOrCode: 'at'
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
// HTTP: GET /api/countries?filter[nameOrCode]=at
|
|
592
|
-
// Returns: {
|
|
593
|
-
// data: [
|
|
594
|
-
// { id: '1', name: 'United States', code: 'US' },
|
|
595
|
-
// { id: '2', name: 'United Kingdom', code: 'UK' },
|
|
596
|
-
// { id: '3', name: 'United Arab Emirates', code: 'AE' },
|
|
597
|
-
// { id: '5', name: 'Austria', code: 'AT' }
|
|
598
|
-
// ]
|
|
599
|
-
// }
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
The result is:
|
|
603
|
-
|
|
604
|
-
```text
|
|
605
|
-
Query results: [
|
|
606
|
-
{ id: '1', name: 'United States', code: 'US' },
|
|
607
|
-
{ id: '3', name: 'United Arab Emirates', code: 'AE' }
|
|
608
|
-
]
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
The function approach is powerful for:
|
|
612
|
-
- Case-insensitive searches
|
|
613
|
-
- Complex conditions combining multiple fields
|
|
614
|
-
- Database-specific functions
|
|
615
|
-
- Custom business logic in searches
|
|
616
|
-
|
|
617
|
-
Since `applyFilter` functions tend to be database-dependent, it's best to avoid using it unless necessary.
|
|
618
|
-
|
|
619
|
-
### Sparse Fieldsets
|
|
620
|
-
|
|
621
|
-
The JSON:API specification includes a powerful feature called "sparse fieldsets" that allows you to request only specific fields from a resource. This is essential for optimizing API performance by reducing payload sizes and network traffic.
|
|
622
|
-
|
|
623
|
-
**How Sparse Fieldsets Work:**
|
|
624
|
-
|
|
625
|
-
By default, API responses include all fields defined in the schema. With sparse fieldsets, you can specify exactly which fields you want returned. The `id` field is always included automatically as it's required by JSON:API.
|
|
626
|
-
|
|
627
|
-
Let's work with our countries table:
|
|
628
|
-
|
|
629
|
-
```javascript
|
|
630
|
-
await api.addResource('countries', {
|
|
631
|
-
schema: {
|
|
632
|
-
name: { type: 'string', required: true, max: 100, search: true },
|
|
633
|
-
code: { type: 'string', max: 2, unique: true, search: true }
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
await api.resources.countries.createKnexTable()
|
|
637
|
-
|
|
638
|
-
// Add some test data
|
|
639
|
-
await api.resources.countries.post({ name: 'France', code: 'FR' });
|
|
640
|
-
await api.resources.countries.post({ name: 'Germany', code: 'DE' });
|
|
641
|
-
await api.resources.countries.post({ name: 'Italy', code: 'IT' });
|
|
642
|
-
await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
|
|
643
|
-
await api.resources.countries.post({ name: 'United States', code: 'US' });
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
**Sparse Fieldsets with `get()` - Single Record:**
|
|
647
|
-
|
|
648
|
-
```javascript
|
|
649
|
-
// Fetch a single country with all fields (default behavior)
|
|
650
|
-
const fullCountry = await api.resources.countries.get({ id: '1' });
|
|
651
|
-
console.log('Full record:', inspect(fullCountry));
|
|
652
|
-
|
|
653
|
-
// Fetch only the name field
|
|
654
|
-
const nameOnly = await api.resources.countries.get({
|
|
655
|
-
id: '1',
|
|
656
|
-
queryParams: { fields: { countries: 'name' } }
|
|
657
|
-
});
|
|
658
|
-
console.log('Name only:', inspect(nameOnly));
|
|
659
|
-
|
|
660
|
-
// Fetch only the code field
|
|
661
|
-
const codeOnly = await api.resources.countries.get({
|
|
662
|
-
id: '1',
|
|
663
|
-
queryParams: { fields: { countries: 'code' } }
|
|
664
|
-
});
|
|
665
|
-
console.log('Code only:', inspect(codeOnly));
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
The output will be:
|
|
669
|
-
|
|
670
|
-
```text
|
|
671
|
-
Full record: { id: '1', name: 'France', code: 'FR' }
|
|
672
|
-
Name only: { id: '1', name: 'France' }
|
|
673
|
-
Code only: { id: '1', code: 'FR' }
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
**Sparse Fieldsets with `query()` - Multiple Records:**
|
|
677
|
-
|
|
678
|
-
Sparse fieldsets become even more valuable when fetching collections, as they can significantly reduce the response size:
|
|
679
|
-
|
|
680
|
-
```javascript
|
|
681
|
-
// Query all countries starting with 'United' - full records
|
|
682
|
-
const fullRecords = await api.resources.countries.query({
|
|
683
|
-
queryParams: {
|
|
684
|
-
filters: { name: 'United' }
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
// HTTP: GET /api/countries?filter[name]=United
|
|
688
|
-
// Returns: {
|
|
689
|
-
// data: [
|
|
690
|
-
// { id: '4', name: 'United Kingdom', code: 'UK' },
|
|
691
|
-
// { id: '5', name: 'United States', code: 'US' }
|
|
692
|
-
// ]
|
|
693
|
-
// }
|
|
694
|
-
|
|
695
|
-
console.log('Full records:', inspect(fullRecords));
|
|
696
|
-
// Note: fullRecords contains { data, meta, links }
|
|
697
|
-
|
|
698
|
-
// Query with only names returned
|
|
699
|
-
const namesOnly = await api.resources.countries.query({
|
|
700
|
-
queryParams: {
|
|
701
|
-
filters: { name: 'United' },
|
|
702
|
-
fields: { countries: 'name' }
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
// HTTP: GET /api/countries?filter[name]=United&fields[countries]=name
|
|
706
|
-
// Returns: {
|
|
707
|
-
// data: [
|
|
708
|
-
// { id: '4', name: 'United Kingdom' },
|
|
709
|
-
// { id: '5', name: 'United States' }
|
|
710
|
-
// ]
|
|
711
|
-
// }
|
|
712
|
-
|
|
713
|
-
console.log('Names only:', inspect(namesOnly));
|
|
714
|
-
// Note: namesOnly contains { data, meta, links }
|
|
715
|
-
|
|
716
|
-
// Query with only codes returned
|
|
717
|
-
const codesOnly = await api.resources.countries.query({
|
|
718
|
-
queryParams: {
|
|
719
|
-
filters: { name: 'United' },
|
|
720
|
-
fields: { countries: 'code' }
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
// HTTP: GET /api/countries?filter[name]=United&fields[countries]=code
|
|
724
|
-
// Returns: {
|
|
725
|
-
// data: [
|
|
726
|
-
// { id: '4', code: 'UK' },
|
|
727
|
-
// { id: '5', code: 'US' }
|
|
728
|
-
// ]
|
|
729
|
-
// }
|
|
730
|
-
|
|
731
|
-
console.log('Codes only:', inspect(codesOnly));
|
|
732
|
-
// Note: codesOnly contains { data, meta, links }
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
**Combining Sparse Fieldsets with Complex Searches:**
|
|
736
|
-
|
|
737
|
-
Sparse fieldsets work seamlessly with all search features, including our new multi-word search:
|
|
738
|
-
|
|
739
|
-
```javascript
|
|
740
|
-
// Define a countries resource with multi-word search
|
|
741
|
-
await api.addResource('countries', {
|
|
742
|
-
schema: {
|
|
743
|
-
name: { type: 'string', required: true },
|
|
744
|
-
code: { type: 'string', unique: true },
|
|
745
|
-
population: { type: 'integer' },
|
|
746
|
-
continent: { type: 'string' }
|
|
747
|
-
},
|
|
748
|
-
searchSchema: {
|
|
749
|
-
search: {
|
|
750
|
-
type: 'string',
|
|
751
|
-
oneOf: ['name', 'continent'],
|
|
752
|
-
filterOperator: 'like',
|
|
753
|
-
splitBy: ' ',
|
|
754
|
-
matchAll: true
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
// Add countries with more fields
|
|
760
|
-
await api.resources.countries.post({
|
|
761
|
-
name: 'South Africa',
|
|
762
|
-
code: 'ZA',
|
|
763
|
-
population: 59308690,
|
|
764
|
-
continent: 'Africa'
|
|
765
|
-
});
|
|
766
|
-
await api.resources.countries.post({
|
|
767
|
-
name: 'South Korea',
|
|
768
|
-
code: 'KR',
|
|
769
|
-
population: 51269185,
|
|
770
|
-
continent: 'Asia'
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
// Search for "south africa" but return only name and population
|
|
774
|
-
const sparseSearch = await api.resources.countries.query({
|
|
775
|
-
queryParams: {
|
|
776
|
-
filters: { search: 'south africa' },
|
|
777
|
-
fields: { countries: 'name,population' }
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
// HTTP: GET /api/countries?filter[search]=south%20africa&fields[countries]=name,population
|
|
781
|
-
// Returns: {
|
|
782
|
-
// data: [{ id: '1', name: 'South Africa', population: 59308690 }]
|
|
783
|
-
// }
|
|
784
|
-
|
|
785
|
-
console.log('Sparse search result:', inspect(sparseSearch));
|
|
786
|
-
// Note: sparseSearch contains { data, meta, links }
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
**Important Notes:**
|
|
790
|
-
|
|
791
|
-
1. **The `id` field is always included** - This is required by the JSON:API specification
|
|
792
|
-
2. **Field names must match schema** - Requesting non-existent fields will be ignored
|
|
793
|
-
3. **Improves performance** - Especially important for large records or when fetching many records
|
|
794
|
-
4. **Works with relationships** - When we cover relationships, you'll see how to apply sparse fieldsets to related resources too
|
|
795
|
-
|
|
796
|
-
**HTTP API Usage:**
|
|
797
|
-
|
|
798
|
-
When using the HTTP API, sparse fieldsets are specified as comma-separated values:
|
|
799
|
-
|
|
800
|
-
```
|
|
801
|
-
GET /api/countries?fields[countries]=name,code
|
|
802
|
-
GET /api/countries/1?fields[countries]=name
|
|
803
|
-
GET /api/countries?filter[search]=united+states&fields[countries]=code
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
---
|
|
807
|
-
|
|
808
|
-
[Previous: 2.1 The starting point](./GUIDE_2_1_The_Starting_Point.md) | [Back to Guide](./README.md) | [Next: 2.3 `belongsTo` Relationships](./GUIDE_2_3_BelongsTo_Relationships.md)
|