json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Socket.IO integration"
|
|
3
|
+
chapter: 28
|
|
4
|
+
chapter_label: "28"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 28. Socket.IO integration
|
|
8
|
+
|
|
9
|
+
`SocketIOPlugin` sends `subscription.update` messages after resource changes.
|
|
10
|
+
Messages identify a changed resource; clients fetch records through the resource
|
|
11
|
+
API using their own fields, includes and authorization context.
|
|
12
|
+
|
|
13
|
+
## Runnable public example
|
|
14
|
+
|
|
15
|
+
This example deliberately exposes a public countries resource. It uses a local
|
|
16
|
+
in-memory database and accepts anonymous subscriptions.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { JsonRestApi } from 'json-rest-api'
|
|
20
|
+
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin, SocketIOPlugin } from 'json-rest-api'
|
|
21
|
+
import { createServer } from 'node:http'
|
|
22
|
+
import express from 'express'
|
|
23
|
+
import knex from 'knex'
|
|
24
|
+
|
|
25
|
+
const db = knex({
|
|
26
|
+
client: 'better-sqlite3',
|
|
27
|
+
connection: { filename: ':memory:' },
|
|
28
|
+
useNullAsDefault: true
|
|
29
|
+
})
|
|
30
|
+
const api = new JsonRestApi({ name: 'socket-example' })
|
|
31
|
+
const app = express()
|
|
32
|
+
await api.use(RestApiPlugin)
|
|
33
|
+
await api.use(RestApiKnexPlugin, { knex: db })
|
|
34
|
+
await api.use(ExpressPlugin, { mountPath: '/api' })
|
|
35
|
+
await api.use(SocketIOPlugin, { subscriptions: { maxPerSocket: 20 } })
|
|
36
|
+
await api.addResource('countries', {
|
|
37
|
+
schema: { id: { type: 'id' }, name: { type: 'string', required: true, search: true } }
|
|
38
|
+
})
|
|
39
|
+
await api.resources.countries.createKnexTable()
|
|
40
|
+
api.http.express.mount(app)
|
|
41
|
+
const server = createServer(app)
|
|
42
|
+
await api.startSocketServer(server)
|
|
43
|
+
server.listen(3000)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Install `socket.io` in the server application and `socket.io-client` in the
|
|
47
|
+
client. Close the Socket.IO server and database during application shutdown:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
await new Promise(resolve => api.io.close(resolve))
|
|
51
|
+
await db.destroy()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Socket.IO closes the HTTP server passed to `startSocketServer`. Closing that
|
|
55
|
+
server also initiates graceful shutdown of the Redis clients created by the
|
|
56
|
+
plugin and clears `api.io` and its internal server/client references. Database
|
|
57
|
+
ownership remains with the application; ordinary resource calls still work
|
|
58
|
+
after Socket.IO shuts down. Do not separately quit those plugin-created Redis
|
|
59
|
+
clients after closing Socket.IO.
|
|
60
|
+
|
|
61
|
+
Only one Socket.IO startup may be active per API instance. Overlapping or
|
|
62
|
+
duplicate starts reject instead of replacing the active server. With Redis
|
|
63
|
+
enabled, both connections must succeed before Socket.IO attaches to the HTTP
|
|
64
|
+
server. Failed Redis startup closes partial connections, preserves the caller's
|
|
65
|
+
HTTP listeners and authentication configuration, and permits a subsequent retry.
|
|
66
|
+
Redis's configured reconnection policy is retained; set
|
|
67
|
+
`redis.socket.reconnectStrategy: false` when startup should fail on its first
|
|
68
|
+
connection error rather than retry.
|
|
69
|
+
|
|
70
|
+
The Socket.IO path defaults to the HTTP connector mount path plus `/socket.io`,
|
|
71
|
+
or `/socket.io` without a connector. Override it with
|
|
72
|
+
`startSocketServer(server, { path: '/events/socket.io' })`. HTTP resource routes
|
|
73
|
+
continue to use their configured API prefix.
|
|
74
|
+
|
|
75
|
+
## Subscribe and fetch
|
|
76
|
+
|
|
77
|
+
After the connection is established:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { io } from 'socket.io-client'
|
|
81
|
+
|
|
82
|
+
const socket = io('http://localhost:3000', { path: '/api/socket.io' })
|
|
83
|
+
socket.on('connect', async () => {
|
|
84
|
+
const result = await socket.timeout(5000).emitWithAck('subscribe', {
|
|
85
|
+
resource: 'countries',
|
|
86
|
+
subscriptionId: 'country-list',
|
|
87
|
+
filters: { name: 'Australia' }
|
|
88
|
+
})
|
|
89
|
+
if (result.error) console.error(result.error)
|
|
90
|
+
})
|
|
91
|
+
socket.on('subscription.update', change => {
|
|
92
|
+
// Invalidate the affected view, then fetch through your normal API client.
|
|
93
|
+
console.log(change.resource, change.id, change.action)
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Subscription input accepts these fields:
|
|
98
|
+
|
|
99
|
+
| Field | Meaning |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `resource` | Required declared resource name |
|
|
102
|
+
| `subscriptionId` | Optional nonempty string; omitted IDs are generated UUIDs |
|
|
103
|
+
| `filters` | Optional object validated with the resource's search schema |
|
|
104
|
+
|
|
105
|
+
An acknowledgement is either `{ success: true, data }` or `{ error: { code,
|
|
106
|
+
message } }`. Successful data contains `subscriptionId`, `resource`, normalized
|
|
107
|
+
`filters` and `status: 'active'`. Without an acknowledgement callback, subscribe
|
|
108
|
+
emits `subscription.created` or `subscription.error` instead.
|
|
109
|
+
|
|
110
|
+
`include` and `fields` are not subscription options: notifications never return
|
|
111
|
+
full records. Keep those options in the client's subsequent GET/query. Unknown
|
|
112
|
+
options receive `UNSUPPORTED_OPTION`; malformed filter containers receive
|
|
113
|
+
`INVALID_FILTERS` instead of becoming unfiltered subscriptions.
|
|
114
|
+
|
|
115
|
+
IDs must be unique among the socket's active subscriptions. Reusing one returns
|
|
116
|
+
`SUBSCRIPTION_EXISTS`; unsubscribe before replacing it. The default limit is
|
|
117
|
+
100 active subscriptions per socket, controlled by `subscriptions.maxPerSocket`.
|
|
118
|
+
Concurrent async admission hooks cannot exceed the configured limit.
|
|
119
|
+
|
|
120
|
+
To remove a subscription:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
await socket.timeout(5000).emitWithAck('unsubscribe', { subscriptionId: 'country-list' })
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The connection stays subscribed to a resource while it has other subscriptions
|
|
127
|
+
for that resource. Every matching subscription receives its own update, with its
|
|
128
|
+
own subscription ID.
|
|
129
|
+
|
|
130
|
+
Unsubscribe removes the logical subscription immediately and waits for any
|
|
131
|
+
required room departure before acknowledging success. Room joins and departures
|
|
132
|
+
are ordered per socket, so a concurrent new subscription retains its membership.
|
|
133
|
+
If the adapter rejects departure, the acknowledgement contains `UNSUBSCRIBE_ERROR`;
|
|
134
|
+
the logical subscription remains removed and cannot receive notifications. The
|
|
135
|
+
failure does not imply that adapter room cleanup succeeded. A later subscription
|
|
136
|
+
can join normally; disconnect also ends the socket's room membership.
|
|
137
|
+
|
|
138
|
+
After reconnecting, submit the desired subscriptions again:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const result = await socket.timeout(5000).emitWithAck('restore-subscriptions', {
|
|
142
|
+
subscriptions: [{ resource: 'countries', subscriptionId: 'country-list' }]
|
|
143
|
+
})
|
|
144
|
+
// result.restored contains successful IDs; result.failed contains per-entry errors.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Restoration uses the same validation, permission, uniqueness and limit checks as
|
|
148
|
+
subscribe. It does not replay changes missed during disconnection.
|
|
149
|
+
|
|
150
|
+
## Notification shape
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"type": "resource.updated",
|
|
155
|
+
"resource": "countries",
|
|
156
|
+
"id": "42",
|
|
157
|
+
"action": "patch",
|
|
158
|
+
"subscriptionId": "country-list",
|
|
159
|
+
"meta": { "timestamp": "2026-09-09T00:00:00.000Z" }
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Change | `type` | `action` |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| POST or PUT-create | `resource.created` | `post` or `put` |
|
|
166
|
+
| PATCH or PUT replacement | `resource.updated` | `patch` or `put` |
|
|
167
|
+
| Relationship POST/PATCH/DELETE | `resource.updated` | `patch` |
|
|
168
|
+
| Resource DELETE | `resource.deleted` | `delete` |
|
|
169
|
+
|
|
170
|
+
IDs are strings. Deletion also includes `deletedRecord: { id }`, without deleted
|
|
171
|
+
attributes. Reverse relationship changes notify the parent and the changed
|
|
172
|
+
child resources. A relationship PATCH's nested resource PATCH provides its
|
|
173
|
+
parent update; it is not emitted a second time by the outer relationship hook.
|
|
174
|
+
|
|
175
|
+
Eligibility uses the resource's ordinary SQL query before and after each write,
|
|
176
|
+
inside that write's transaction. The changed ID is constrained internally; it
|
|
177
|
+
need not be a public search field, and a client's ID filter cannot replace it.
|
|
178
|
+
A record entering or leaving the query result invalidates that subscription.
|
|
179
|
+
Deletion uses the before-write result. Custom `applyFilter(query, value)` search
|
|
180
|
+
fields work directly; remove any Socket.IO-only `filterRecord` callback. There
|
|
181
|
+
is no separate JavaScript predicate to maintain.
|
|
182
|
+
|
|
183
|
+
These queries run normal query permissions, hooks, filters, row policies and
|
|
184
|
+
autofilters. They use subscriber context, never the writer's context. Query
|
|
185
|
+
failures are logged and count as nonmatches; a successful match before or after
|
|
186
|
+
is sufficient. Applications should account for these additional queries and
|
|
187
|
+
ensure query hooks can run inside the write transaction.
|
|
188
|
+
|
|
189
|
+
## Authentication and permissions
|
|
190
|
+
|
|
191
|
+
For an application with authenticated users, install the plugin with a
|
|
192
|
+
server-owned authentication function:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
await api.use(SocketIOPlugin, {
|
|
196
|
+
auth: {
|
|
197
|
+
requireAuth: true,
|
|
198
|
+
authenticate: async ({ socket }) => {
|
|
199
|
+
const identity = await verifyToken(socket.handshake.auth?.token)
|
|
200
|
+
return { userId: identity.id, roles: identity.roles, workspaceId: identity.workspaceId, groups: identity.groups }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`verifyToken` is application code. A failed authentication rejects the connection.
|
|
207
|
+
Handshake fields do not become trusted identity unless the application explicitly
|
|
208
|
+
enables `allowClientProvidedAuth`; the default is false. `anonymousContext` can
|
|
209
|
+
supply a deliberate public context. `onAuthenticationFailed` can observe a
|
|
210
|
+
failure; an error from that observer does not authorize the connection.
|
|
211
|
+
|
|
212
|
+
Resource query permission is checked when subscribing and again before each
|
|
213
|
+
notification. A failing notification permission check suppresses that message
|
|
214
|
+
and is logged. It does not undo the committed write. Authentication is performed
|
|
215
|
+
when connecting, so applications that revoke sessions must also invalidate or
|
|
216
|
+
disconnect their active sockets as appropriate.
|
|
217
|
+
|
|
218
|
+
A `subscriptionFilters` hook can add filters and set trusted query context.
|
|
219
|
+
Populate the same application context your row policies and autofilters need.
|
|
220
|
+
Hook arguments use the resource runtime envelope:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
await api.customize({ hooks: {
|
|
224
|
+
subscriptionFilters: {
|
|
225
|
+
functionName: 'limit-subscriptions',
|
|
226
|
+
handler: ({ context }) => {
|
|
227
|
+
const { subscription, auth } = context
|
|
228
|
+
subscription.context = {
|
|
229
|
+
scopeValues: { workspaceId: auth.workspaceId },
|
|
230
|
+
visibility: { groups: auth.groups }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} })
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The context property defaults to `{}` and must be an object. It is supplied to
|
|
238
|
+
both admission and notification query permission checks; `auth` always comes
|
|
239
|
+
from the stored subscription identity. Use JSON-serializable context for Redis
|
|
240
|
+
adapter transport. Clients cannot supply `context` in subscribe/restore messages.
|
|
241
|
+
The resulting filters are validated again, and the hook cannot change the
|
|
242
|
+
resource or subscription ID. Replacing a subscription with the same public ID
|
|
243
|
+
creates a new generation: it receives subsequent changes only, not messages
|
|
244
|
+
queued for the removed subscription.
|
|
245
|
+
|
|
246
|
+
Admission failures are logged with `operation: 'subscribe'`, `phase: 'admission'`
|
|
247
|
+
and the resolved `scopeName`, including individual failures while restoring
|
|
248
|
+
subscriptions. After resolving a resource, the diagnostic formatter redacts
|
|
249
|
+
structured fields marked `hidden` or `normallyHidden` in that resource's schema.
|
|
250
|
+
Unknown resources have no resource-specific field policy. Diagnostic formatting
|
|
251
|
+
does not mutate the original error, and a failed log sink does not replace the
|
|
252
|
+
subscription rejection. This does not redact secrets embedded in arbitrary
|
|
253
|
+
error-message text; avoid putting secret values in messages.
|
|
254
|
+
|
|
255
|
+
## Transactions, Redis and verification limits
|
|
256
|
+
|
|
257
|
+
For individual resource and relationship operations, notifications wait for
|
|
258
|
+
commit of the library-owned transaction. Writes that roll back,
|
|
259
|
+
including finish-hook failures after events were queued, emit no successful
|
|
260
|
+
change. A rejected call after commit can still have emitted notifications. These paths are tested on both SQLite storage modes over WebSocket and
|
|
261
|
+
HTTP polling, including relationship changes and child rollback.
|
|
262
|
+
|
|
263
|
+
Library writes now require the handle from `api.transaction` when joining an
|
|
264
|
+
outer unit. Resource and atomic-bulk child completion hooks run after confirmed
|
|
265
|
+
owner completion; non-atomic bulk retains one outcome per entry. Rolled-back
|
|
266
|
+
units do not flush queued notifications. Explicit callbacks are tested with
|
|
267
|
+
repeated changes to the same record, callback/caught-write/caught-SQL failures,
|
|
268
|
+
and rejected completion hooks on both storage modes and all three databases.
|
|
269
|
+
Direct raw transaction completion is no longer a supported library-write
|
|
270
|
+
notification path.
|
|
271
|
+
|
|
272
|
+
Notifications are per-operation invalidations. A sequential create, update and
|
|
273
|
+
delete in one committed callback emits those three notices in order, even though
|
|
274
|
+
the final row is absent. Several updates to one row are not coalesced. No notice
|
|
275
|
+
is emitted while the callback remains pending. The helper awaits completion
|
|
276
|
+
hooks; after a confirmed commit, a completion error cannot undo stored changes
|
|
277
|
+
or notifications already emitted. A later operation's completion chain can flush
|
|
278
|
+
the transaction's queue if an earlier chain failed before delivery. This does
|
|
279
|
+
not guarantee delivery when every applicable chain is interrupted or the
|
|
280
|
+
connection fails.
|
|
281
|
+
|
|
282
|
+
Once queue draining begins, failure while delivering one queued notice does not
|
|
283
|
+
prevent attempts for later notices. The call rejects with the first delivery
|
|
284
|
+
failure and a committed outcome; it does not roll back stored data. The context
|
|
285
|
+
of the operation draining the queue retains each failed attempt as
|
|
286
|
+
`{ phase: 'socketioBroadcast', broadcastIndex, error }`, where `broadcastIndex`
|
|
287
|
+
is zero-based in that transaction's captured queue. Keep that operation context
|
|
288
|
+
when you need these diagnostics, just as for file cleanup diagnostics.
|
|
289
|
+
|
|
290
|
+
The queue is consumed once. Failed notices are not automatically replayed by
|
|
291
|
+
later completion chains. One notice can fail after delivery to some recipients;
|
|
292
|
+
the plugin cannot establish who received it or undo those deliveries. Clients
|
|
293
|
+
must be able to refetch authoritative data. Continuing with later notices is a
|
|
294
|
+
best-effort attempt, not a delivery acknowledgement or durable retry mechanism.
|
|
295
|
+
|
|
296
|
+
With overlapping writes, notification order follows their capture in the
|
|
297
|
+
Socket.IO finish hook, which can differ from call-start order. Completion hook
|
|
298
|
+
chains follow transaction enlistment order (reverse order on rollback). Await
|
|
299
|
+
writes sequentially when their ordering matters to the application.
|
|
300
|
+
|
|
301
|
+
Socket.IO preserves message ordering but does not provide durable delivery by
|
|
302
|
+
default. Clients can miss notifications while disconnected; refetch on reconnect.
|
|
303
|
+
See [Socket.IO delivery guarantees](https://socket.io/docs/v4/delivery-guarantees/).
|
|
304
|
+
|
|
305
|
+
Optional Redis setup accepts the options used by Redis's `createClient`, for
|
|
306
|
+
example `startSocketServer(server, { redis: { url: 'redis://localhost:6379' } })`.
|
|
307
|
+
Install `redis` and `@socket.io/redis-adapter` when using it. Subscription state
|
|
308
|
+
survives the JSON encoding used by the adapter's `fetchSockets` responses.
|
|
309
|
+
The [real Redis tests](../contributing.md#real-databases-and-redis) exercise two Socket.IO servers
|
|
310
|
+
sharing SQLite, in both storage modes, over polling and WebSocket. They cover
|
|
311
|
+
committed CRUD and relationship notifications, row visibility, rollback and
|
|
312
|
+
replacement of queued subscriptions. Further cases drop each publisher/subscriber
|
|
313
|
+
connection on each server and verify delivery after reconnection. Startup tests
|
|
314
|
+
cover bad credentials, missing sockets, exhausted retries, partial connection
|
|
315
|
+
failure, preserved authentication and overlapping starts. Shutdown tests cover
|
|
316
|
+
Socket.IO close, HTTP close and close during Redis reconnection.
|
|
317
|
+
|
|
318
|
+
These tests do not establish durable delivery during an outage, Redis Cluster
|
|
319
|
+
or Sentinel failover, or bounded cleanup when a network connection remains open
|
|
320
|
+
but never answers. Redis shutdown drains queued commands; connection and retry
|
|
321
|
+
timeouts remain part of the supplied Redis configuration.
|
|
322
|
+
|
|
323
|
+
Socket.IO transport CORS is configured separately through
|
|
324
|
+
`startSocketServer(server, { cors: ... })` or `transport.cors` in plugin options.
|
|
325
|
+
It does not replace the HTTP [CORS plugin](24-cors.md) or authentication.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Writing plugins"
|
|
3
|
+
chapter: 29
|
|
4
|
+
chapter_label: "29"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 29. Writing plugins
|
|
8
|
+
|
|
9
|
+
`JsonRestApi` installs plugins and runs resource methods through a small local
|
|
10
|
+
runtime. This guide covers the public extension surface for plugin authors.
|
|
11
|
+
|
|
12
|
+
Use this guide when you want to:
|
|
13
|
+
|
|
14
|
+
- add resource-level behavior across many resources
|
|
15
|
+
- add new resource methods
|
|
16
|
+
- compile resource metadata into reusable runtime state
|
|
17
|
+
- hook into query or write processing
|
|
18
|
+
|
|
19
|
+
For the internal execution path, see [architecture](../architecture.md).
|
|
20
|
+
|
|
21
|
+
## Minimal plugin shape
|
|
22
|
+
|
|
23
|
+
These three blocks form a runnable example. Add them to the
|
|
24
|
+
[starting script](03-running-example.md) after installing storage,
|
|
25
|
+
before starting the server, on a fresh database. Later snippets are fragments
|
|
26
|
+
for a plugin's `install` function.
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
const MyPlugin = {
|
|
30
|
+
name: 'my-plugin',
|
|
31
|
+
dependencies: ['rest-api'],
|
|
32
|
+
install ({ addHook, addResourceMethod }) {
|
|
33
|
+
addHook('beforeSchemaValidate', 'trim-example-name', {}, ({ context }) => {
|
|
34
|
+
const attributes = context.inputRecord?.data?.attributes
|
|
35
|
+
if (typeof attributes?.name === 'string') attributes.name = attributes.name.trim()
|
|
36
|
+
})
|
|
37
|
+
addHook('resource:added', 'compile-my-plugin', {}, ({ context, scopes }) => {
|
|
38
|
+
const scope = scopes[context.scopeName]
|
|
39
|
+
scope.vars.myPlugin = { enabled: context.scopeOptions.exampleFlag === true }
|
|
40
|
+
})
|
|
41
|
+
addResourceMethod('describeExample', async ({ scopeName, scope }) => ({
|
|
42
|
+
scopeName, enabled: scope.vars.myPlugin.enabled
|
|
43
|
+
}))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await api.use(MyPlugin)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Install registration hooks before registering the resources they configure.
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
await api.addResource('authors', {
|
|
53
|
+
schema: { name: { type: 'string', required: true } },
|
|
54
|
+
exampleFlag: true
|
|
55
|
+
})
|
|
56
|
+
await api.addResource('publishers', {
|
|
57
|
+
schema: { name: { type: 'string', required: true } }
|
|
58
|
+
})
|
|
59
|
+
await api.resources.authors.createKnexTable()
|
|
60
|
+
await api.resources.publishers.createKnexTable()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The custom method is available on each resource, with resource-specific state.
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
const authorDescription = await api.resources.authors.describeExample()
|
|
67
|
+
const publisherDescription = await api.resources.publishers.describeExample()
|
|
68
|
+
const createdAuthor = await api.resources.authors.post({
|
|
69
|
+
format: 'plain', data: { name: ' Ada ' }
|
|
70
|
+
})
|
|
71
|
+
console.log(authorDescription, publisherDescription, createdAuthor)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The first result is `{ scopeName: 'authors', enabled: true }`; the second is
|
|
75
|
+
`{ scopeName: 'publishers', enabled: false }`. The created author's `name` is
|
|
76
|
+
`'Ada'`: the hook trims the declared attribute before attribute validation and
|
|
77
|
+
storage.
|
|
78
|
+
Export `MyPlugin` as a named
|
|
79
|
+
export when moving the declaration into its own module.
|
|
80
|
+
|
|
81
|
+
The usual pattern is:
|
|
82
|
+
|
|
83
|
+
1. compile resource metadata during `resource:added`
|
|
84
|
+
2. store normalized state in `scope.vars`
|
|
85
|
+
3. use hooks or resource methods to apply behavior at runtime
|
|
86
|
+
|
|
87
|
+
## Registration and hook ordering
|
|
88
|
+
|
|
89
|
+
A plugin has a unique `name`, an `install` function and optional `dependencies`.
|
|
90
|
+
Dependencies must already be installed. Each string names a required plugin;
|
|
91
|
+
an array names alternatives, for example
|
|
92
|
+
`dependencies: ['rest-api', ['rest-api-knex', 'rest-api-anyapi-knex']]`.
|
|
93
|
+
Duplicate plugin installation rejects. Await setup calls sequentially and discard
|
|
94
|
+
an instance if installation fails; installation is not transactional.
|
|
95
|
+
|
|
96
|
+
The installation argument provides `addApiMethod(name, handler)`,
|
|
97
|
+
`addResourceMethod(name, handler)` and `addHook(event, name, placement, handler)`.
|
|
98
|
+
An API method name cannot replace an existing API member. Registering a shared
|
|
99
|
+
resource method replaces the shared definition; resource-local overrides remain.
|
|
100
|
+
Methods and hooks receive the common runtime envelope. Internal names `scope`,
|
|
101
|
+
`scopeName` and `scopes` refer to resources; the public collection is `api.resources`.
|
|
102
|
+
|
|
103
|
+
Use `{}` to append a hook, or specify one named `beforeFunction` or `afterFunction`
|
|
104
|
+
anchor in the same event. A missing anchor rejects registration. Numeric order
|
|
105
|
+
and per-plugin ordering options are unsupported. Handlers run sequentially over
|
|
106
|
+
a snapshot of the list. Returning `false` stops that list; throw an error to reject
|
|
107
|
+
an operation. The dispatcher propagates thrown values unchanged.
|
|
108
|
+
|
|
109
|
+
Resource-local hooks use the same placement contract and run only for that
|
|
110
|
+
resource. Register global `resource:added` hooks before adding resources; a
|
|
111
|
+
resource's local hooks are installed after its setup event completes.
|
|
112
|
+
|
|
113
|
+
## The stable extension points
|
|
114
|
+
|
|
115
|
+
### `resource:added`
|
|
116
|
+
|
|
117
|
+
Use this to inspect `scopeOptions`, validate configuration, and compile resource-specific runtime state into `scope.vars`.
|
|
118
|
+
|
|
119
|
+
Typical uses:
|
|
120
|
+
|
|
121
|
+
- initialize resource-specific extension state
|
|
122
|
+
- validate non-schema resource options
|
|
123
|
+
|
|
124
|
+
Declare fields during `schema:enrich` (or `computedSchema:enrich` for computed
|
|
125
|
+
fields). Use `schema:compiled` for work that needs the compiled schema, such
|
|
126
|
+
as autofilter configuration; do not add fields after compilation.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
addHook('resource:added', 'compile-example', {}, ({ context, scopes }) => {
|
|
132
|
+
const scope = scopes[context.scopeName]
|
|
133
|
+
const options = context.scopeOptions || {}
|
|
134
|
+
|
|
135
|
+
scope.vars.example = {
|
|
136
|
+
flag: options.exampleFlag === true
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `beforeSchemaValidate`
|
|
142
|
+
|
|
143
|
+
Use this to normalize attribute values before attribute schema validation runs.
|
|
144
|
+
Request document validation and relationship processing have already begun.
|
|
145
|
+
Use the earlier `beforeProcessing` stage if an extension must change the input
|
|
146
|
+
structure before those checks, and validate any untrusted shape it reads.
|
|
147
|
+
|
|
148
|
+
Typical uses:
|
|
149
|
+
|
|
150
|
+
- inject derived input values
|
|
151
|
+
- apply attribute-level business checks
|
|
152
|
+
|
|
153
|
+
This hook cannot make an otherwise rejected request document valid retroactively.
|
|
154
|
+
|
|
155
|
+
The complete example above trims a declared string attribute here. It does not
|
|
156
|
+
attempt to remove an unknown field after request validation has rejected it.
|
|
157
|
+
For stage order and available context, use the
|
|
158
|
+
[write lifecycle reference](13-hooks-and-lifecycle.md#resource-write-order).
|
|
159
|
+
|
|
160
|
+
### `knexQueryFiltering`
|
|
161
|
+
|
|
162
|
+
Use this to add query constraints to the generated Knex query.
|
|
163
|
+
|
|
164
|
+
Typical uses:
|
|
165
|
+
|
|
166
|
+
- automatic scoping
|
|
167
|
+
- cross-table filters
|
|
168
|
+
- custom public filter semantics
|
|
169
|
+
|
|
170
|
+
The hook receives the current builder through `context.knexQuery.query`,
|
|
171
|
+
along with its `scopeName` and `tableName`. It also runs for nested queries.
|
|
172
|
+
Raw column names are unsafe to assume: ordinary resources can map logical fields
|
|
173
|
+
to different columns, and canonical resources use storage slots and aliases.
|
|
174
|
+
|
|
175
|
+
For equality scoping and write stamping, use
|
|
176
|
+
[AutoFilterPlugin](16-autofiltering.md). For mandatory resource visibility,
|
|
177
|
+
use [RowPolicyPlugin](17-row-policies.md), whose policy context supplies
|
|
178
|
+
logical column/value translation and query-purpose metadata. For a public
|
|
179
|
+
client-selected filter, use the
|
|
180
|
+
[custom search filter contract](04-creating-and-querying.md).
|
|
181
|
+
These mechanisms avoid duplicating storage translation in application hooks.
|
|
182
|
+
|
|
183
|
+
A filtering hook does not turn an ad hoc SQL alias into a declared field;
|
|
184
|
+
use the query-field contract below for selected or sortable derived values.
|
|
185
|
+
|
|
186
|
+
### `addResourceMethod`
|
|
187
|
+
|
|
188
|
+
Use this when a plugin needs a reusable method on every resource or selected resources.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
addResourceMethod('introspect', async ({ vars }) => {
|
|
194
|
+
return {
|
|
195
|
+
tableName: vars.schemaInfo?.tableName,
|
|
196
|
+
fields: Object.keys(vars.schemaInfo?.schemaStructure || {})
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## The query-field seam
|
|
202
|
+
|
|
203
|
+
`json-rest-api` now supports a small, explicit seam for **query-only read fields**.
|
|
204
|
+
|
|
205
|
+
This is the seam used by `QueryProjectionsPlugin`, and it is the recommended pattern for plugins that need derived SQL-backed fields.
|
|
206
|
+
|
|
207
|
+
### What a plugin should provide
|
|
208
|
+
|
|
209
|
+
Declare query fields during the existing schema-enrichment stage:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
addHook('schema:enrich', 'declare-uppercase-name', {}, ({ context }) => {
|
|
213
|
+
if (context.scopeName !== 'authors') return
|
|
214
|
+
context.queryFields.uppercase_name = {
|
|
215
|
+
type: 'string',
|
|
216
|
+
sortable: true,
|
|
217
|
+
select: ({ knex, column }) => knex.raw('upper(??)', [column('first_name')])
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Install the hook before registering resources. Core normalizes the final map
|
|
223
|
+
and rejects names that collide with attributes, computed fields, relationships
|
|
224
|
+
or logical IDs. If another declaration hook replaces the map, order this hook
|
|
225
|
+
after that one. The published definitions are available at
|
|
226
|
+
`scope.vars.schemaInfo.queryFields`; do not mutate a separate `vars.queryFields`
|
|
227
|
+
map. Canonical `addKnexFields` recompiles declarations and dependencies before
|
|
228
|
+
publication. SQL callbacks are still evaluated with each query's context.
|
|
229
|
+
|
|
230
|
+
### What core does with compiled query fields
|
|
231
|
+
|
|
232
|
+
Core will:
|
|
233
|
+
|
|
234
|
+
- include visible query fields by default in `get()` and `query()`
|
|
235
|
+
- allow them in sparse fieldsets
|
|
236
|
+
- allow them in sorting when the plugin marks them sortable
|
|
237
|
+
- use them in cursor pagination with a stable `id` tie-breaker
|
|
238
|
+
- carry them through included-resource selection
|
|
239
|
+
- only expose declared query-field aliases on the AnyAPI path
|
|
240
|
+
|
|
241
|
+
### What this seam is for
|
|
242
|
+
|
|
243
|
+
Use it for:
|
|
244
|
+
|
|
245
|
+
- SQL-backed projections
|
|
246
|
+
- aggregate list fields
|
|
247
|
+
- joined display labels
|
|
248
|
+
- derived fields that must participate in `ORDER BY`
|
|
249
|
+
|
|
250
|
+
Do **not** use it for:
|
|
251
|
+
|
|
252
|
+
- writable fields
|
|
253
|
+
- storage/schema ownership
|
|
254
|
+
- migrations or introspection
|
|
255
|
+
- normal response-only computed fields
|
|
256
|
+
|
|
257
|
+
For the concrete projection example, see [Query Projections](15-query-projections.md).
|
|
258
|
+
|
|
259
|
+
## Recommended plugin pattern
|
|
260
|
+
|
|
261
|
+
When you add a new plugin feature, prefer this flow:
|
|
262
|
+
|
|
263
|
+
1. read plugin options at install time
|
|
264
|
+
2. declare schema metadata during its enrichment stage; use `resource:added` for other registration work
|
|
265
|
+
3. store only normalized runtime state in `scope.vars`
|
|
266
|
+
4. use hooks or resource methods to apply behavior
|
|
267
|
+
|
|
268
|
+
That keeps the plugin declarative and avoids re-parsing configuration during requests.
|
|
269
|
+
|
|
270
|
+
## Boundaries to keep clean
|
|
271
|
+
|
|
272
|
+
- Keep **query-layer** extensions out of `schema`.
|
|
273
|
+
- Choose input hooks according to the [write lifecycle](13-hooks-and-lifecycle.md#resource-write-order).
|
|
274
|
+
- Keep **filtering** in `knexQueryFiltering`.
|
|
275
|
+
- Keep **response-only** enrichment in `enrichAttributes`.
|
|
276
|
+
- Do not rely on arbitrary internal helper filenames as public API.
|
|
277
|
+
|
|
278
|
+
If a feature requires deeper integration than the public seams provide, treat that as a core extension discussion rather than a plugin hack.
|