json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. 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.