json-rest-api 1.0.29 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Query projections"
|
|
3
|
+
chapter: 15
|
|
4
|
+
chapter_label: "15"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 15. Query projections
|
|
8
|
+
|
|
9
|
+
Query projections are SQL-backed, output-only query fields provided by the optional `QueryProjectionsPlugin`. They are different from normal computed fields.
|
|
10
|
+
|
|
11
|
+
If you want to build your own plugin on the same query-field seam, see [Writing Plugins](29-writing-plugins.md).
|
|
12
|
+
|
|
13
|
+
- **Computed fields** run after the row is fetched, in JavaScript.
|
|
14
|
+
- **Projected fields** are selected inside the query, so they can participate in `SELECT`, `ORDER BY`, and cursor pagination.
|
|
15
|
+
|
|
16
|
+
Use query projection fields when a derived value must behave like a real list field.
|
|
17
|
+
|
|
18
|
+
Typical cases:
|
|
19
|
+
- sorting by a derived label such as `full_name`
|
|
20
|
+
- cursor pagination over a derived value
|
|
21
|
+
- returning a derived SQL value without storing it
|
|
22
|
+
|
|
23
|
+
Do **not** use query projection fields for normal response decoration. Regular computed fields are simpler and remain the right default for that.
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
The six executable blocks below form one scenario. Add them to the
|
|
28
|
+
[starting script](03-running-example.md) after REST/storage installation,
|
|
29
|
+
before starting the server, on a fresh database. Other code blocks are
|
|
30
|
+
configuration fragments.
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { QueryProjectionsPlugin } from 'json-rest-api'
|
|
34
|
+
await api.use(QueryProjectionsPlugin)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Defining a query field
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
await api.addResource('authors', {
|
|
41
|
+
schema: {
|
|
42
|
+
id: { type: 'id' },
|
|
43
|
+
first_name: { type: 'string', required: true },
|
|
44
|
+
last_name: { type: 'string', required: true }
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
queryFields: {
|
|
48
|
+
full_name: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
sortable: true,
|
|
51
|
+
select: ({ knex, column }) => knex.raw(
|
|
52
|
+
knex.client.config.client === 'mysql2'
|
|
53
|
+
? "trim(concat(coalesce(??, ''), ' ', coalesce(??, '')))"
|
|
54
|
+
: "trim(coalesce(??, '') || ' ' || coalesce(??, ''))",
|
|
55
|
+
[column('first_name'), column('last_name')]
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
sortableFields: ['id', 'first_name', 'last_name'],
|
|
61
|
+
defaultSort: ['full_name']
|
|
62
|
+
})
|
|
63
|
+
await api.addResource('books', {
|
|
64
|
+
schema: {
|
|
65
|
+
title: { type: 'string', required: true },
|
|
66
|
+
author_id: { type: 'id', belongsTo: 'authors', as: 'author', nullable: true }
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
await api.resources.authors.createKnexTable()
|
|
70
|
+
await api.resources.books.createKnexTable()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The expression uses `concat` for MySQL and `||` for SQLite/PostgreSQL. `column`
|
|
74
|
+
resolves logical fields to the selected storage representation.
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
const jane = await api.resources.authors.post({ format: 'plain', data: { first_name: 'Jane', last_name: 'Doe' } })
|
|
78
|
+
await api.resources.authors.post({ format: 'plain', data: { first_name: 'John', last_name: 'Adams' } })
|
|
79
|
+
await api.resources.authors.post({ format: 'plain', data: { first_name: 'Jane', last_name: 'Smith' } })
|
|
80
|
+
const book = await api.resources.books.post({ format: 'plain', data: { title: 'Projection example', author: jane.id } })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`select()` is called at query time. It can return:
|
|
84
|
+
|
|
85
|
+
- a `knex.raw(...)` expression
|
|
86
|
+
- a Knex reference
|
|
87
|
+
- a Knex query/subquery expression
|
|
88
|
+
|
|
89
|
+
Return the expression directly from a synchronous callback. Knex raw values,
|
|
90
|
+
references and query builders are thenable: returning them from an `async`
|
|
91
|
+
callback or awaiting them can execute SQL instead of supplying an expression.
|
|
92
|
+
For example, use `select: ({ knex }) => knex.raw('?', [1])`, without `async`.
|
|
93
|
+
|
|
94
|
+
Definitions are compiled into `scope.vars.schemaInfo.queryFields` during schema
|
|
95
|
+
enrichment. Projection names cannot collide with stored/computed fields,
|
|
96
|
+
relationship names or logical ID names. Canonical field additions repeat these
|
|
97
|
+
checks before publishing a replacement schema. SQL expressions remain local to
|
|
98
|
+
each query; compiled definitions do not cache request-dependent SQL.
|
|
99
|
+
|
|
100
|
+
Getters and computed fields can declare projection names as dependencies. The
|
|
101
|
+
projection is selected before JavaScript callbacks run, including through a
|
|
102
|
+
chain of dependencies in a sparse fieldset. A projection needed only as an input
|
|
103
|
+
is removed from output unless selected and visible. Install the declaration
|
|
104
|
+
plugin before resource registration so dependencies can be validated.
|
|
105
|
+
|
|
106
|
+
The callback receives:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
{
|
|
110
|
+
knex, // current knex/db handle
|
|
111
|
+
db, // same as knex
|
|
112
|
+
context, // request context
|
|
113
|
+
scopeName,
|
|
114
|
+
tableName,
|
|
115
|
+
fieldName,
|
|
116
|
+
schemaInfo,
|
|
117
|
+
adapter, // storage adapter
|
|
118
|
+
column, // helper: logical field -> qualified storage column
|
|
119
|
+
ref // helper: logical field -> knex ref
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Behavior
|
|
124
|
+
|
|
125
|
+
Query projection fields are:
|
|
126
|
+
|
|
127
|
+
- returned by default in `get()` and `query()`
|
|
128
|
+
- available in sparse fieldsets
|
|
129
|
+
- usable in sorting and cursor pagination
|
|
130
|
+
- available on included resources
|
|
131
|
+
- ignored on writes
|
|
132
|
+
|
|
133
|
+
They are **not** stored columns, and they are **not** part of write validation.
|
|
134
|
+
|
|
135
|
+
A failed `select()` callback or rejected promise rejects the operation. On reads,
|
|
136
|
+
typed API errors retain their identity; unexpected errors are wrapped with the
|
|
137
|
+
resource/field context and the original thrown value as `cause`, including
|
|
138
|
+
non-Error values. Included projection failures also reject GET/query instead of
|
|
139
|
+
returning a partial response. During a full write response, the failure occurs
|
|
140
|
+
before an owned transaction commits; the public `RestApiWriteError` retains the
|
|
141
|
+
failure as its cause and reports the transaction outcome. A borrowed transaction
|
|
142
|
+
remains its owner's responsibility.
|
|
143
|
+
|
|
144
|
+
## Sparse fieldsets
|
|
145
|
+
|
|
146
|
+
Query projection fields can be requested explicitly:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
const sparse = await api.resources.authors.get({
|
|
150
|
+
id: jane.id, format: 'jsonapi',
|
|
151
|
+
queryParams: {
|
|
152
|
+
fields: {
|
|
153
|
+
authors: 'first_name,full_name'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If a query field is needed only for sorting, it is still selected internally, but it is removed from the response unless the sparse fieldset asked for it.
|
|
160
|
+
|
|
161
|
+
## Sorting and cursor pagination
|
|
162
|
+
|
|
163
|
+
Query projection fields are useful when a derived value must drive list behavior:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const firstPage = await api.resources.authors.query({
|
|
167
|
+
format: 'jsonapi',
|
|
168
|
+
queryParams: { sort: ['full_name'], page: { size: 2 } }
|
|
169
|
+
})
|
|
170
|
+
const nextPage = await api.resources.authors.query({
|
|
171
|
+
format: 'jsonapi',
|
|
172
|
+
queryParams: { sort: ['full_name'], page: { size: 2, after: firstPage.meta.pagination.cursor.next } }
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`json-rest-api` automatically appends `id` as a stable tie-breaker for cursor ordering. That prevents duplicate or skipped rows when multiple records share the same projected value.
|
|
177
|
+
|
|
178
|
+
Projection cursors retain the SQL expression's value before output formatting
|
|
179
|
+
and bind it directly on the next request. This also preserves precise timestamps
|
|
180
|
+
beyond the built-in millisecond write limit:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
queryFields: {
|
|
184
|
+
preciseTimestamp: {
|
|
185
|
+
type: 'dateTime',
|
|
186
|
+
temporalPrecision: 6,
|
|
187
|
+
sortable: true,
|
|
188
|
+
select: ({ knex, column }) => knex.raw('??', [column('recordedAt')])
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Projections cannot declare `storage`, `setter`, or `runSetterAfter`; compilation
|
|
194
|
+
rejects those options. Use the stored field's serializer when writing that field.
|
|
195
|
+
For projection output formatting, use `getter` and optional `runGetterAfter`.
|
|
196
|
+
The shared dependency order awaits projection getters before dependent getters
|
|
197
|
+
or computed fields, and sparse reads fetch their declared dependencies. Hidden
|
|
198
|
+
dependencies remain hidden in the response. Getter failures propagate as read
|
|
199
|
+
errors. Neither getters nor response formatting change SQL ordering or cursors.
|
|
200
|
+
Always follow returned cursors/links without rebuilding them from attributes.
|
|
201
|
+
See [temporal migration requirements](33-migrating-to-v2.md#temporal-formatting-and-native-databases)
|
|
202
|
+
for UTC bindings, fractional precision and existing AnyAPI data.
|
|
203
|
+
|
|
204
|
+
Structured (`object`/`array`) projections are output fields and cannot be marked
|
|
205
|
+
sortable. Select a scalar JSON key in a separate typed projection when it should
|
|
206
|
+
drive ordering or cursors. PostgreSQL structured selections use `to_jsonb` to
|
|
207
|
+
support `DISTINCT` parent queries; this does not alter application tables.
|
|
208
|
+
See [querying structured attributes](33-migrating-to-v2.md#querying-structured-attributes)
|
|
209
|
+
for bound SQL examples and custom-filter migration.
|
|
210
|
+
|
|
211
|
+
Hidden projections cannot be sortable or appear in resource/include sort
|
|
212
|
+
configuration: cursors contain the selected SQL value. Use `normallyHidden`
|
|
213
|
+
for a publicly readable projection that should be omitted by default but remain
|
|
214
|
+
sortable. A `hidden` projection can still supply a getter/computed dependency.
|
|
215
|
+
See [hidden sort migration](33-migrating-to-v2.md#hidden-sort-fields).
|
|
216
|
+
|
|
217
|
+
## Included resources
|
|
218
|
+
|
|
219
|
+
Without a target fieldset, includes select all visible projections and the
|
|
220
|
+
dependencies of getters and computed fields. This applies to to-one, to-many
|
|
221
|
+
and polymorphic includes in both storage modes, even before the target's first
|
|
222
|
+
direct read. A sparse fieldset returns only the requested fields; dependencies
|
|
223
|
+
selected solely for a callback are removed from the response.
|
|
224
|
+
|
|
225
|
+
Query projection fields can be selected for included resources too:
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
const included = await api.resources.books.get({
|
|
229
|
+
id: book.id, format: 'jsonapi',
|
|
230
|
+
queryParams: {
|
|
231
|
+
include: ['author'],
|
|
232
|
+
fields: {
|
|
233
|
+
books: 'title,author',
|
|
234
|
+
authors: 'full_name'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Write behavior
|
|
241
|
+
|
|
242
|
+
Query projection fields are output-only. If a client sends them in `POST`, `PUT`, or `PATCH`, they are ignored.
|
|
243
|
+
|
|
244
|
+
The configured logger receives a bounded warning with the operation, resource,
|
|
245
|
+
validation phase and total ignored-field count. It does not include submitted
|
|
246
|
+
attribute values. A failure to write this advisory warning does not reject the
|
|
247
|
+
operation.
|
|
248
|
+
|
|
249
|
+
That keeps the boundary clean:
|
|
250
|
+
|
|
251
|
+
- persisted fields own storage
|
|
252
|
+
- computed fields own response-layer enrichment
|
|
253
|
+
- query projection fields own query-layer derivation
|
|
254
|
+
|
|
255
|
+
## Limits
|
|
256
|
+
|
|
257
|
+
Projected fields are an advanced query feature. They are intentionally **backend-sensitive**.
|
|
258
|
+
|
|
259
|
+
Important limits:
|
|
260
|
+
|
|
261
|
+
- projection SQL can be dialect-specific
|
|
262
|
+
- this is not a portability layer across every SQL engine
|
|
263
|
+
- query projection fields are not auto-searchable
|
|
264
|
+
- if you want to sort by one, mark it `sortable: true`
|
|
265
|
+
|
|
266
|
+
For display-only derived values, prefer normal computed fields from [Field Transformations](12-field-transformations.md).
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: "Autofiltering"
|
|
3
|
+
chapter: 16
|
|
4
|
+
chapter_label: "16"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 16. Autofiltering
|
|
2
8
|
|
|
3
9
|
The `AutoFilterPlugin` provides declarative dataset scoping for `json-rest-api`.
|
|
4
10
|
|
|
@@ -31,17 +37,75 @@ The plugin is generic. It does not assume:
|
|
|
31
37
|
|
|
32
38
|
Your application decides where scope values come from. The plugin only consumes configured resolver functions.
|
|
33
39
|
|
|
40
|
+
## Runnable workspace example
|
|
41
|
+
|
|
42
|
+
Insert these three blocks into the [starting script](03-running-example.md)
|
|
43
|
+
after installing storage and before starting the server, on a fresh database.
|
|
44
|
+
They also work with canonical storage after its normal initialization. The
|
|
45
|
+
remaining examples below are configuration alternatives and fragments.
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import { AutoFilterPlugin } from 'json-rest-api'
|
|
49
|
+
|
|
50
|
+
await api.use(AutoFilterPlugin, {
|
|
51
|
+
resolvers: { workspace: ({ context }) => context.session?.workspaceId },
|
|
52
|
+
presets: { workspace: { filters: [{ field: 'workspace_id', resolver: 'workspace' }] } }
|
|
53
|
+
})
|
|
54
|
+
await api.addResource('projects', {
|
|
55
|
+
schema: {
|
|
56
|
+
name: { type: 'string', required: true },
|
|
57
|
+
workspace_id: { type: 'string', required: true }
|
|
58
|
+
},
|
|
59
|
+
autofilter: 'workspace'
|
|
60
|
+
})
|
|
61
|
+
await api.resources.projects.createKnexTable()
|
|
62
|
+
const workspaceContext = workspaceId => ({ session: { workspaceId } })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Supply trusted application context as the second argument. Here the workspace
|
|
66
|
+
values are illustrative; a server should derive them from its authenticated
|
|
67
|
+
request context.
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
const acmeProject = await api.resources.projects.post({
|
|
71
|
+
data: { name: 'Roadmap' }, format: 'plain'
|
|
72
|
+
}, workspaceContext('acme'))
|
|
73
|
+
await api.resources.projects.post({
|
|
74
|
+
data: { name: 'Other workspace' }, format: 'plain'
|
|
75
|
+
}, workspaceContext('other'))
|
|
76
|
+
const acmePage = await api.resources.projects.query({
|
|
77
|
+
format: 'jsonapi', queryParams: { page: { number: 1, size: 10 } }
|
|
78
|
+
}, workspaceContext('acme'))
|
|
79
|
+
console.log(acmeProject.workspace_id, acmePage.data, acmePage.meta.pagination.total)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The created record has `workspace_id: 'acme'`. The page contains only Roadmap
|
|
83
|
+
and its total is 1, even though another workspace has a stored project.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
const replacedProject = await api.resources.projects.put({
|
|
87
|
+
id: acmeProject.id, data: { name: 'Updated roadmap' }, format: 'plain'
|
|
88
|
+
}, workspaceContext('acme'))
|
|
89
|
+
const otherPage = await api.resources.projects.query({ format: 'plain' }, workspaceContext('other'))
|
|
90
|
+
console.log(replacedProject.workspace_id, otherPage.data.map(project => project.name))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
PUT supplies the complete writable record, with the scoped workspace field
|
|
94
|
+
injected by the plugin. The record remains in acme, and the other workspace
|
|
95
|
+
still sees only its own project. Explicitly submitting a different workspace
|
|
96
|
+
value rejects; omitting required application scope context also rejects.
|
|
97
|
+
|
|
34
98
|
## Installation
|
|
35
99
|
|
|
36
100
|
```js
|
|
37
|
-
import {
|
|
101
|
+
import { JsonRestApi } from 'json-rest-api'
|
|
38
102
|
import {
|
|
39
103
|
RestApiPlugin,
|
|
40
104
|
RestApiKnexPlugin,
|
|
41
105
|
AutoFilterPlugin
|
|
42
106
|
} from 'json-rest-api'
|
|
43
107
|
|
|
44
|
-
const api = new
|
|
108
|
+
const api = new JsonRestApi({ name: 'scoped-api' })
|
|
45
109
|
|
|
46
110
|
await api.use(RestApiPlugin)
|
|
47
111
|
await api.use(RestApiKnexPlugin, { knex })
|
|
@@ -84,7 +148,7 @@ await api.use(AutoFilterPlugin, {
|
|
|
84
148
|
- `RestApiKnexPlugin` for resources backed by normal database tables
|
|
85
149
|
- `RestApiAnyapiKnexPlugin` for logical resources backed by AnyAPI canonical storage
|
|
86
150
|
|
|
87
|
-
|
|
151
|
+
A `JsonRestApi` instance has one storage engine. Install either storage plugin, then install `AutoFilterPlugin`. If an application keeps core resources in normal tables and user-defined resources in AnyAPI, create one `JsonRestApi` instance for each storage engine. Each instance owns its resources and autofilter configuration.
|
|
88
152
|
|
|
89
153
|
Autofilter definitions always use logical schema field names. The selected storage plugin translates those fields to normal table columns or AnyAPI canonical slots.
|
|
90
154
|
|
|
@@ -93,14 +157,14 @@ Autofilter definitions always use logical schema field names. The selected stora
|
|
|
93
157
|
AnyAPI's `tenantId` is fixed when `RestApiAnyapiKnexPlugin` is installed. It identifies the metadata and canonical-storage namespace; it is not a per-request workspace value. For one AnyAPI instance serving multiple application workspaces, use a stable internal `tenantId` and persist `workspace_id` on every workspace-scoped resource:
|
|
94
158
|
|
|
95
159
|
```js
|
|
96
|
-
import {
|
|
160
|
+
import { JsonRestApi } from 'json-rest-api'
|
|
97
161
|
import {
|
|
98
162
|
RestApiPlugin,
|
|
99
163
|
RestApiAnyapiKnexPlugin,
|
|
100
164
|
AutoFilterPlugin
|
|
101
165
|
} from 'json-rest-api'
|
|
102
166
|
|
|
103
|
-
const api = new
|
|
167
|
+
const api = new JsonRestApi({ name: 'form-data-api' })
|
|
104
168
|
|
|
105
169
|
await api.use(RestApiPlugin)
|
|
106
170
|
await api.use(RestApiAnyapiKnexPlugin, {
|
|
@@ -222,7 +286,7 @@ For a `workspace_user` resource:
|
|
|
222
286
|
|
|
223
287
|
```js
|
|
224
288
|
await api.resources.projects.query(
|
|
225
|
-
{
|
|
289
|
+
{ format: 'jsonapi' },
|
|
226
290
|
{
|
|
227
291
|
session: { workspaceId: 'acme' },
|
|
228
292
|
subject: { id: 101 }
|
|
@@ -252,7 +316,7 @@ Example:
|
|
|
252
316
|
|
|
253
317
|
```js
|
|
254
318
|
await api.resources.projects.post({
|
|
255
|
-
|
|
319
|
+
document: {
|
|
256
320
|
data: {
|
|
257
321
|
type: 'projects',
|
|
258
322
|
attributes: {
|
|
@@ -260,7 +324,7 @@ await api.resources.projects.post({
|
|
|
260
324
|
}
|
|
261
325
|
}
|
|
262
326
|
},
|
|
263
|
-
|
|
327
|
+
format: 'jsonapi'
|
|
264
328
|
}, {
|
|
265
329
|
session: { workspaceId: 'acme' },
|
|
266
330
|
subject: { id: 101 }
|
|
@@ -297,7 +361,7 @@ So this will fail if the referenced project is outside scope:
|
|
|
297
361
|
|
|
298
362
|
```js
|
|
299
363
|
await api.resources.tasks.post({
|
|
300
|
-
|
|
364
|
+
document: {
|
|
301
365
|
data: {
|
|
302
366
|
type: 'tasks',
|
|
303
367
|
attributes: { title: 'Cross-scope task' },
|
|
@@ -308,12 +372,10 @@ await api.resources.tasks.post({
|
|
|
308
372
|
}
|
|
309
373
|
}
|
|
310
374
|
},
|
|
311
|
-
|
|
375
|
+
format: 'jsonapi'
|
|
312
376
|
}, {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
userId: 101
|
|
316
|
-
}
|
|
377
|
+
session: { workspaceId: 'workspace-a' },
|
|
378
|
+
subject: { id: 101 }
|
|
317
379
|
})
|
|
318
380
|
```
|
|
319
381
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: "Row policies"
|
|
3
|
+
chapter: 17
|
|
4
|
+
chapter_label: "17"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 17. Row policies
|
|
2
8
|
|
|
3
9
|
`RowPolicyPlugin` is the server-side visibility seam for resources whose readable rows depend on more than a fixed equality filter.
|
|
4
10
|
|
|
@@ -18,7 +24,7 @@ This is unsafe and paginates the wrong dataset:
|
|
|
18
24
|
```js
|
|
19
25
|
const page = await api.resources.organisation_units.query({
|
|
20
26
|
queryParams: { page: { number: 1, size: 20 } },
|
|
21
|
-
|
|
27
|
+
format: 'jsonapi'
|
|
22
28
|
}, context)
|
|
23
29
|
|
|
24
30
|
page.data = page.data.filter((unit) => canSeeUnit(context, unit))
|
|
@@ -66,7 +72,7 @@ The page size and count now describe the visible dataset.
|
|
|
66
72
|
| Requirement | Use |
|
|
67
73
|
| --- | --- |
|
|
68
74
|
| The caller asks to filter by a public search field | Normal `queryParams.filters` / `searchSchema` |
|
|
69
|
-
| Every row must match one or more context-derived persisted fields, and those fields should be stamped on writes | [`AutoFilterPlugin`](
|
|
75
|
+
| Every row must match one or more context-derived persisted fields, and those fields should be stamped on writes | [`AutoFilterPlugin`](16-autofiltering.md) |
|
|
70
76
|
| Visibility needs `OR`, `EXISTS`, joins, hierarchy traversal, or domain-composed SQL | `RowPolicyPlugin` |
|
|
71
77
|
| The caller may perform an action such as update, approve, or delete | Permission hooks, usually in addition to a row policy |
|
|
72
78
|
| A response-only field must be calculated | Computed fields or query projections |
|
|
@@ -77,46 +83,14 @@ The page size and count now describe the visible dataset.
|
|
|
77
83
|
|
|
78
84
|
Install the REST plugin, one supported Knex storage plugin, and then the row-policy plugin. Install `RowPolicyPlugin` before adding resources that declare `rowPolicy`.
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
import {
|
|
85
|
-
RestApiPlugin,
|
|
86
|
-
RestApiKnexPlugin,
|
|
87
|
-
RowPolicyPlugin
|
|
88
|
-
} from 'json-rest-api'
|
|
89
|
-
|
|
90
|
-
const api = new Api({ name: 'application-api' })
|
|
91
|
-
|
|
92
|
-
await api.use(RestApiPlugin)
|
|
93
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
94
|
-
await api.use(RowPolicyPlugin, {
|
|
95
|
-
policies: {
|
|
96
|
-
workspaceMember: ({ query, context, column, value }) => {
|
|
97
|
-
const workspaceId = context.session?.workspaceId
|
|
98
|
-
if (!workspaceId) return false
|
|
99
|
-
|
|
100
|
-
query.where(
|
|
101
|
-
column('workspace_id'),
|
|
102
|
-
value('workspace_id', workspaceId)
|
|
103
|
-
)
|
|
104
|
-
return true
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### AnyAPI canonical storage
|
|
86
|
+
The following example starts with an `api` that already has `RestApiPlugin` and
|
|
87
|
+
one of `RestApiKnexPlugin` or `RestApiAnyapiKnexPlugin` installed, as described in
|
|
88
|
+
[initial setup](01-getting-started.md). Use a fresh database for this example.
|
|
89
|
+
The policy code is the same for both storage modes:
|
|
111
90
|
|
|
112
|
-
|
|
91
|
+
```javascript
|
|
92
|
+
import { RowPolicyPlugin } from 'json-rest-api'
|
|
113
93
|
|
|
114
|
-
```js
|
|
115
|
-
await api.use(RestApiPlugin)
|
|
116
|
-
await api.use(RestApiAnyapiKnexPlugin, {
|
|
117
|
-
knex,
|
|
118
|
-
tenantId: 'application'
|
|
119
|
-
})
|
|
120
94
|
await api.use(RowPolicyPlugin, {
|
|
121
95
|
policies: {
|
|
122
96
|
workspaceMember: ({ query, context, column, value }) => {
|
|
@@ -133,7 +107,8 @@ await api.use(RowPolicyPlugin, {
|
|
|
133
107
|
})
|
|
134
108
|
```
|
|
135
109
|
|
|
136
|
-
`column()` and `value()` translate logical schema fields and values through the
|
|
110
|
+
`column()` and `value()` translate logical schema fields and values through the
|
|
111
|
+
active storage adapter, including canonical slots.
|
|
137
112
|
|
|
138
113
|
## Resource configuration
|
|
139
114
|
|
|
@@ -141,7 +116,7 @@ await api.use(RowPolicyPlugin, {
|
|
|
141
116
|
|
|
142
117
|
Registered policies are preferred for shared or package-owned behavior:
|
|
143
118
|
|
|
144
|
-
```
|
|
119
|
+
```javascript
|
|
145
120
|
await api.addResource('documents', {
|
|
146
121
|
schema: {
|
|
147
122
|
id: { type: 'id' },
|
|
@@ -151,10 +126,44 @@ await api.addResource('documents', {
|
|
|
151
126
|
rowPolicy: 'workspaceMember',
|
|
152
127
|
tableName: 'documents'
|
|
153
128
|
})
|
|
129
|
+
await api.resources.documents.createKnexTable()
|
|
154
130
|
```
|
|
155
131
|
|
|
156
132
|
The string must match a function in `RowPolicyPlugin`'s `policies` option. Unknown names fail while the resource is being added.
|
|
157
133
|
|
|
134
|
+
Create two documents in one workspace and one in another. Supply trusted
|
|
135
|
+
application context separately from the call parameters:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const acmeContext = { session: { workspaceId: 'acme' } }
|
|
139
|
+
const otherContext = { session: { workspaceId: 'other' } }
|
|
140
|
+
for (const title of ['Alpha', 'Beta']) {
|
|
141
|
+
await api.resources.documents.post({
|
|
142
|
+
data: { title, workspace_id: 'acme' }, returning: 'none'
|
|
143
|
+
}, acmeContext)
|
|
144
|
+
}
|
|
145
|
+
await api.resources.documents.post({
|
|
146
|
+
data: { title: 'Other workspace', workspace_id: 'other' }, returning: 'none'
|
|
147
|
+
}, otherContext)
|
|
148
|
+
|
|
149
|
+
const acmePage = await api.resources.documents.query({
|
|
150
|
+
format: 'jsonapi',
|
|
151
|
+
queryParams: { sort: 'title', page: { number: 1, size: 1 } }
|
|
152
|
+
}, acmeContext)
|
|
153
|
+
const otherPage = await api.resources.documents.query({}, otherContext)
|
|
154
|
+
const deniedPage = await api.resources.documents.query({})
|
|
155
|
+
|
|
156
|
+
console.log(acmePage.data[0].attributes.title) // Alpha
|
|
157
|
+
console.log(acmePage.meta.pagination.total) // 2, before the page limit
|
|
158
|
+
console.log(otherPage.data.map(document => document.title)) // ['Other workspace']
|
|
159
|
+
console.log(deniedPage.data) // []: no workspace in the caller context
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
These inserts explicitly supply `workspace_id`. This policy does not stamp or
|
|
163
|
+
validate ownership on writes; use autofilter and write permissions for that
|
|
164
|
+
contract. The example demonstrates read visibility, counts and missing-context
|
|
165
|
+
denial.
|
|
166
|
+
|
|
158
167
|
### Inline policy
|
|
159
168
|
|
|
160
169
|
An inline function is supported for a policy that belongs to one resource:
|
|
@@ -279,8 +288,9 @@ Policies are reused for the resource selections that define externally visible d
|
|
|
279
288
|
| `count` | A separate count query, including native offset-pagination counts and standalone count helpers. |
|
|
280
289
|
| `single` | Minimal lookup used by `get`, and by existing-record preflights for `put`, `patch`, and `delete`. |
|
|
281
290
|
| `include` | A target resource loaded into JSON:API `included` data. |
|
|
291
|
+
| `search-join` | A related resource used by a cross-resource or polymorphic search, reference filter, or reference sort, including intermediate search path segments. |
|
|
282
292
|
| `relationship-identifiers` | Child identifiers attached to a relationship, including has-many and AnyAPI many-to-many links. |
|
|
283
|
-
| `relationship-validation` | A target
|
|
293
|
+
| `relationship-validation` | A batch of up to 100 distinct target IDs referenced by a write payload, or a scoped lookup retaining database ID equality. |
|
|
284
294
|
| `relationship-parent` | The parent of a relationship route such as `getRelated`. |
|
|
285
295
|
| `unspecified` | A custom call to the lower-level query-filtering seam did not provide a purpose. |
|
|
286
296
|
|
|
@@ -288,12 +298,74 @@ The important invariant is not the label. It is that the predicate is attached b
|
|
|
288
298
|
|
|
289
299
|
Treat `queryPurpose` as diagnostic context, not as permission to weaken a policy. A policy should normally add the same visibility predicate for every purpose. If an advanced policy uses a purpose-specific SQL shape, each branch must enforce equivalent visibility and must be tested independently; never skip a predicate for counts, includes, identifiers, or relationship validation.
|
|
290
300
|
|
|
301
|
+
Relationship identifier visibility is selected in batches of at most 100 unique
|
|
302
|
+
IDs per target resource type. Target query permissions and filtering hooks run
|
|
303
|
+
for each batch, with the caller's authorization and transaction context. Always
|
|
304
|
+
attach the complete predicate to the supplied builder; a request-level
|
|
305
|
+
"already applied" flag must not skip later queries. Keep ordering, pagination
|
|
306
|
+
and include limits in their operation/resource settings. Visibility hooks add
|
|
307
|
+
row predicates and retain the query's selected ID and mandatory constraints.
|
|
308
|
+
Their invocation count is not a per-request or per-record contract.
|
|
309
|
+
|
|
310
|
+
Regular and polymorphic belongs-to includes also load at most 100 target IDs
|
|
311
|
+
per query, with target permission and filtering hooks applied to every batch
|
|
312
|
+
under `queryPurpose: 'include'`. SQL projection callbacks run for each query.
|
|
313
|
+
Nested collection includes run after all target batches have been collected,
|
|
314
|
+
so a global collection limit is not restarted for each batch.
|
|
315
|
+
|
|
316
|
+
Relationship validation uses the same batch bound. Its query context has no
|
|
317
|
+
individual `id` and no parent client filters. Retain the selected ID and scope
|
|
318
|
+
predicates; filtering supplies an ID subquery used to fetch complete minimal
|
|
319
|
+
records. Read permission hooks then receive each target's ID and minimal record,
|
|
320
|
+
once per distinct identifier per relationship. Link writers do not run target
|
|
321
|
+
response GET hooks. See the [migration steps](33-migrating-to-v2.md#relationship-target-validation).
|
|
322
|
+
|
|
323
|
+
Large collection and relationship-metadata reads keep their selection in one
|
|
324
|
+
SQL query. PostgreSQL uses an array predicate and SQLite uses a JSON1 ID
|
|
325
|
+
subquery; their policies and projections still run before the original sort
|
|
326
|
+
and limit. Apply predicates using the supplied query/context rather than
|
|
327
|
+
assuming the internal ID constraint is always a Knex `whereIn` value array.
|
|
328
|
+
|
|
329
|
+
The identifier selection uses `DISTINCT` so predicate joins do not multiply its
|
|
330
|
+
returned ID rows. The library retains the input linkage order and occurrences
|
|
331
|
+
after visibility filtering. A permission or filter failure in a later batch
|
|
332
|
+
rejects the read rather than returning a partial set. No visibility results are
|
|
333
|
+
shared between calls, resource types or tenant APIs.
|
|
334
|
+
|
|
291
335
|
### Collections and counts
|
|
292
336
|
|
|
293
337
|
Offset pagination applies the same mandatory policy to the data query and its count. This remains true when the caller supplies no `filters` object.
|
|
294
338
|
|
|
295
339
|
Cursor predicates are added after the row policy, so the cursor moves through visible rows rather than through a page that will later be filtered.
|
|
296
340
|
|
|
341
|
+
Related search rows are filtered inside each joined selection. A hidden related
|
|
342
|
+
name cannot match a search or contribute to its count. A visible primary record
|
|
343
|
+
can still match its own attributes in an OR search when its related row is hidden
|
|
344
|
+
or absent. These selections check the target's `query` permission and use the
|
|
345
|
+
caller's authorization and transaction context, without inheriting the primary
|
|
346
|
+
resource's client filters. Explicit permission or query-hook failures reject the
|
|
347
|
+
query. Keep mandatory policy predicates active for `search-join` too.
|
|
348
|
+
|
|
349
|
+
Built-in filters on a belongs-to foreign key, its relationship alias, or a
|
|
350
|
+
polymorphic ID/type pair also require a visible target. A hidden or missing
|
|
351
|
+
reference behaves as null when evaluating the declared filter operator. This
|
|
352
|
+
applies to `actualField`, `oneOf` and joined target fields as well as direct
|
|
353
|
+
filters. Comparisons retain the stored column's type and collation. Custom
|
|
354
|
+
`applyFilter` callbacks still own the visibility of any related SQL they write;
|
|
355
|
+
the library cannot infer relationships from arbitrary custom predicates.
|
|
356
|
+
|
|
357
|
+
Built-in sorting on these reference fields and their aliases uses the same
|
|
358
|
+
visibility boundary, including `defaultSort`. Hidden and absent references
|
|
359
|
+
share a null sort value; later fields and the primary ID resolve ties. Ordering,
|
|
360
|
+
cursor comparisons and cursor values all use the same visible value, so a
|
|
361
|
+
hidden target's ID cannot appear in pagination metadata or links. Visible values
|
|
362
|
+
retain their storage column's type and collation. Target lookup predicates use
|
|
363
|
+
`search-join`, the current caller and the caller's transaction.
|
|
364
|
+
|
|
365
|
+
For HTTP null filters, send `filter[field][json]=null`; plain
|
|
366
|
+
`filter[field]=null` is the literal string. Generated links preserve typed nulls
|
|
367
|
+
and arrays using this explicit JSON form.
|
|
368
|
+
|
|
297
369
|
### Single records and writes
|
|
298
370
|
|
|
299
371
|
An existing row outside the policy behaves as not found for `get`, `patch`, and `delete`. Existing-record `put` authorization uses the same scoped minimal lookup.
|
|
@@ -310,6 +382,27 @@ The target resource's policy is used when the framework loads:
|
|
|
310
382
|
- per-parent windowed include queries
|
|
311
383
|
- a relationship target during write validation
|
|
312
384
|
|
|
385
|
+
These secondary reads also check the target resource's `query` action permission.
|
|
386
|
+
A permission denial rejects the read, whereas a row policy filters individual
|
|
387
|
+
rows. Default relationship identifiers are subject to the same permission check
|
|
388
|
+
as requested includes. The target permission context retains caller auth and
|
|
389
|
+
transaction, with target scope/schema and no inherited parent ID or filters.
|
|
390
|
+
|
|
391
|
+
Regular-storage many-to-many reads also apply the pivot resource's permission and
|
|
392
|
+
policy to membership before standard/windowed include limits and linkage. AnyAPI
|
|
393
|
+
uses tenant-scoped canonical links for these associations; a declared pivot
|
|
394
|
+
resource's row policy does not apply to canonical links.
|
|
395
|
+
|
|
396
|
+
Both storage backends apply include ordering and limits after target visibility.
|
|
397
|
+
`include: { strategy: 'window', limit: 2, orderBy: ['name'] }` selects up to two
|
|
398
|
+
visible children for each parent. The default/`standard` strategy applies a global
|
|
399
|
+
limit to the included target set; a shared many-to-many child counts once and
|
|
400
|
+
remains linked to every selected parent it belongs to. Duplicate physical links
|
|
401
|
+
do not consume extra slots. Sparse fields and SQL query projections retain the
|
|
402
|
+
configured strategy. Reverse polymorphic collections follow the same rules.
|
|
403
|
+
Included linkage lists the selected subset; use the relationship/related endpoint
|
|
404
|
+
to read or paginate the full visible membership.
|
|
405
|
+
|
|
313
406
|
Filtering the parent resource does not automatically define child visibility. Put `rowPolicy` on every child resource that has its own visibility rule.
|
|
314
407
|
|
|
315
408
|
Relationship routes preserve the caller's trusted context when they invoke the target resource. Filters, sorting, and pagination supplied for a related-resource request are applied to that target query; they are deliberately withheld from the preliminary parent-visibility lookup. This keeps a target-only filter from being misinterpreted as a parent field while still enforcing the parent's policy.
|