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.
Files changed (292) 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 +744 -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} +74 -30
  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 -177
  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 +48 -317
  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 +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -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 +179 -194
  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 +100 -290
  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 +93 -38
  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 +128 -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 -2485
  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 -1077
  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 -395
  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 -148
  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 +36 -68
  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 +37 -81
  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 -1247
  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 -148
  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 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  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-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,1040 +0,0 @@
1
- # WebSocket Real-time Updates with Socket.IO
2
-
3
- ## Table of Contents
4
-
5
- 1. [Overview](#overview)
6
- 2. [Server Setup](#server-setup)
7
- 3. [Core Concepts](#core-concepts)
8
- 4. [The Filter System](#the-filter-system)
9
- 5. [Security Architecture](#security-architecture)
10
- 6. [Advanced Features](#advanced-features)
11
- 7. [Client Usage](#client-usage)
12
- 8. [Performance Considerations](#performance-considerations)
13
- 9. [Troubleshooting](#troubleshooting)
14
-
15
- ## Overview
16
-
17
- The Socket.IO plugin provides real-time notifications when resources change in your json-rest-api application. It implements a **notification-only pattern** - instead of broadcasting full data (which could leak sensitive information), it only sends minimal notifications about what changed. Clients then fetch the updated data through the regular REST API, ensuring all permissions and transformations are properly applied.
18
-
19
- ### Key Benefits
20
-
21
- - **Security First**: No data leaks possible - notifications contain only resource type and ID
22
- - **Performance**: One broadcast per change, not N database queries for N subscribers
23
- - **Consistency**: Uses the same searchSchema as REST API for filtering
24
- - **Transaction Safe**: Only broadcasts after database commits succeed
25
- - **Scalable**: Supports Redis adapter for multi-server deployments
26
-
27
- ## Server Setup
28
-
29
- ### Installation
30
-
31
- The Socket.IO plugin is included in json-rest-api core plugins. To use it, you need to:
32
-
33
- 1. Install Socket.IO. Redis packages are only needed when you configure the Redis adapter:
34
- ```bash
35
- npm install socket.io
36
- npm install @socket.io/redis-adapter redis
37
- ```
38
-
39
- 2. Use the plugin and start the Socket.IO server:
40
-
41
- ```javascript
42
- import { Api } from 'hooked-api';
43
- import { RestApiPlugin, RestApiKnexPlugin, SocketIOPlugin } from 'json-rest-api';
44
-
45
- // Create your API instance
46
- const api = new Api({
47
- name: 'my-api',
48
- });
49
-
50
- // Add required plugins
51
- await api.use(RestApiPlugin);
52
- await api.use(RestApiKnexPlugin, { knex: knexInstance });
53
- await api.use(SocketIOPlugin, {
54
- auth: {
55
- requireAuth: true,
56
- authenticate: async ({ socket }) => {
57
- const token = socket.handshake.auth?.token;
58
- return verifyToken(token); // App-defined token verification
59
- }
60
- }
61
- });
62
-
63
- // Start your HTTP server
64
- const server = app.listen(3000).on('error', (err) => {
65
- console.error('Failed to start server:', err);
66
- process.exit(1)
67
- });
68
-
69
- // Start Socket.IO server
70
- const io = await api.startSocketServer(server, {
71
- path: '/socket.io', // Socket.IO path (default: '/socket.io')
72
- cors: { // CORS configuration
73
- origin: '*', // Configure for your security needs
74
- methods: ['GET', 'POST']
75
- },
76
- redis: { // Optional: Redis adapter for scaling
77
- host: 'localhost',
78
- port: 6379
79
- }
80
- });
81
- ```
82
-
83
- ### Configuration Options
84
-
85
- The `startSocketServer` method accepts these options:
86
-
87
- | Option | Type | Default | Description |
88
- |--------|------|---------|-------------|
89
- | `path` | string | '/socket.io' | URL path for Socket.IO endpoint |
90
- | `cors` | object | `{ origin: '*', methods: ['GET', 'POST'] }` | CORS configuration |
91
- | `redis` | object | null | Redis configuration for multi-server setup |
92
-
93
- ### How It Works
94
-
95
- 1. **REST API Integration**: The plugin hooks into the REST API's `finish` event
96
- 2. **Transaction Awareness**: Waits for database commits before broadcasting
97
- 3. **Filter Matching**: Uses `context.minimalRecord` to check subscription filters
98
- 4. **Notification Broadcasting**: Sends minimal notifications to matching subscribers
99
-
100
- ## Core Concepts
101
-
102
- ### Notification-Only Pattern
103
-
104
- Traditional WebSocket implementations often broadcast full data:
105
-
106
- ```javascript
107
- // ❌ INSECURE: Broadcasting full data
108
- io.emit('user.updated', {
109
- id: 123,
110
- name: 'John Doe',
111
- email: 'john@example.com',
112
- ssn: '123-45-6789', // LEAKED to all subscribers!
113
- salary: 150000, // LEAKED to all subscribers!
114
- medical_notes: '...' // LEAKED to all subscribers!
115
- });
116
- ```
117
-
118
- Our implementation broadcasts only notifications:
119
-
120
- ```javascript
121
- // ✅ SECURE: Notification only
122
- socket.emit('subscription.update', {
123
- type: 'resource.updated',
124
- resource: 'users',
125
- id: '123',
126
- action: 'update',
127
- subscriptionId: 'users-12345-abc',
128
- meta: { timestamp: '2024-01-15T10:00:00Z' }
129
- });
130
- ```
131
-
132
- Clients then fetch data through REST API with proper permissions:
133
-
134
- ```javascript
135
- // Client fetches with their permissions applied
136
- const response = await fetch('/api/users/123', {
137
- headers: { Authorization: `Bearer ${token}` }
138
- });
139
- // Server applies all permission checks, field hiding, etc.
140
- ```
141
-
142
- ### searchSchema Integration
143
-
144
- The plugin reuses your existing searchSchema definitions for filtering subscriptions. This ensures consistency between REST API queries and WebSocket subscriptions:
145
-
146
- ```javascript
147
- // Define your resource with searchSchema
148
- await api.addResource('posts', {
149
- schema: {
150
- title: { type: 'string', required: true },
151
- content: { type: 'string' },
152
- status: { type: 'string', defaultTo: 'draft' },
153
- author_id: { type: 'id', required: true },
154
- published_at: { type: 'dateTime', nullable: true },
155
- view_count: { type: 'number', defaultTo: 0 }
156
- },
157
-
158
- searchSchema: {
159
- // These filters work for both REST and WebSocket
160
- status: { type: 'string', filterOperator: '=' },
161
- author_id: { type: 'id', filterOperator: '=' },
162
- published_at: { type: 'dateTime', filterOperator: '>=' },
163
- view_count: { type: 'number', filterOperator: '>' }
164
- }
165
- });
166
-
167
- // REST API query
168
- GET /api/posts?filter[status]=published&filter[view_count]=100
169
-
170
- // WebSocket subscription - SAME filters!
171
- socket.emit('subscribe', {
172
- resource: 'posts',
173
- filters: {
174
- status: 'published',
175
- view_count: 100
176
- }
177
- });
178
- ```
179
-
180
- ### Transaction Safety
181
-
182
- The plugin ensures broadcasts only happen after successful database commits:
183
-
184
- ```javascript
185
- // In a transaction
186
- const trx = await knex.transaction();
187
- try {
188
- // Create a post
189
- const post = await api.resources.posts.post({
190
- inputRecord: { /* ... */ },
191
- transaction: trx
192
- });
193
-
194
- // At this point, NO broadcast has been sent
195
-
196
- await trx.commit();
197
- // NOW the broadcast is sent
198
- } catch (error) {
199
- await trx.rollback();
200
- // No broadcast is ever sent
201
- }
202
- ```
203
-
204
- ## The Filter System
205
-
206
- ### Simple Operator Filters
207
-
208
- Filters using simple operators (`=`, `>`, `>=`, `<`, `<=`, `!=`, `like`, `in`, `between`) work automatically for both REST and WebSocket:
209
-
210
- ```javascript
211
- searchSchema: {
212
- // Equality
213
- status: { type: 'string', filterOperator: '=' },
214
-
215
- // Comparison
216
- price: { type: 'number', filterOperator: '>=' },
217
- stock: { type: 'number', filterOperator: '>' },
218
-
219
- // Pattern matching
220
- title: { type: 'string', filterOperator: 'like' },
221
-
222
- // Multiple values
223
- category_id: { type: 'array', filterOperator: 'in' },
224
-
225
- // Range
226
- created_at: { type: 'date', filterOperator: 'between' }
227
- }
228
-
229
- // These work for both REST and WebSocket
230
- socket.emit('subscribe', {
231
- resource: 'products',
232
- filters: {
233
- status: 'active',
234
- price: 99.99,
235
- title: 'phone',
236
- category_id: [1, 2, 3],
237
- created_at: ['2024-01-01', '2024-12-31']
238
- }
239
- });
240
- ```
241
-
242
- ### Complex Filters with filterRecord
243
-
244
- When `filterOperator` is a function (for complex SQL queries), you must provide `filterRecord` for WebSocket support:
245
-
246
- ```javascript
247
- searchSchema: {
248
- // Complex multi-field search
249
- search: {
250
- type: 'string',
251
-
252
- // For REST API - builds SQL query
253
- filterOperator: function(query, value, { tableName }) {
254
- query.where(function() {
255
- this.where(`${tableName}.title`, 'like', `%${value}%`)
256
- .orWhere(`${tableName}.description`, 'like', `%${value}%`)
257
- .orWhere(`${tableName}.tags`, 'like', `%${value}%`);
258
- });
259
- },
260
-
261
- // For WebSocket - evaluates single record (REQUIRED!)
262
- filterRecord: function(record, value) {
263
- const search = value.toLowerCase();
264
- const title = (record.title || '').toLowerCase();
265
- const desc = (record.description || '').toLowerCase();
266
- const tags = (record.tags || []).join(' ').toLowerCase();
267
-
268
- return title.includes(search) ||
269
- desc.includes(search) ||
270
- tags.includes(search);
271
- }
272
- },
273
-
274
- // Location-based search
275
- near_location: {
276
- type: 'object',
277
-
278
- // REST: Haversine formula in SQL
279
- filterOperator: function(query, value, { tableName }) {
280
- const { lat, lng, radius = 10 } = value;
281
- query.whereRaw(`
282
- (6371 * acos(
283
- cos(radians(?)) * cos(radians(${tableName}.latitude)) *
284
- cos(radians(${tableName}.longitude) - radians(?)) +
285
- sin(radians(?)) * sin(radians(${tableName}.latitude))
286
- )) <= ?
287
- `, [lat, lng, lat, radius]);
288
- },
289
-
290
- // WebSocket: JavaScript distance calculation
291
- filterRecord: function(record, value) {
292
- const { lat, lng, radius = 10 } = value;
293
- const distance = calculateDistance(
294
- lat, lng,
295
- record.latitude, record.longitude
296
- );
297
- return distance <= radius;
298
- }
299
- },
300
-
301
- // Custom business logic
302
- available_for_user: {
303
- type: 'object',
304
-
305
- // REST: Complex JOIN with user permissions
306
- filterOperator: function(query, value, { tableName }) {
307
- const { user_id, include_private } = value;
308
- query.where(`${tableName}.owner_id`, user_id);
309
- if (!include_private) {
310
- query.orWhere(`${tableName}.is_public`, true);
311
- }
312
- },
313
-
314
- // WebSocket: Same logic in JavaScript
315
- filterRecord: function(record, value) {
316
- const { user_id, include_private } = value;
317
- if (record.owner_id === user_id) return true;
318
- if (record.is_public) return true;
319
- return include_private && record.shared_with?.includes(user_id);
320
- }
321
- }
322
- }
323
- ```
324
-
325
- ### Filter Validation
326
-
327
- All filters are validated against searchSchema before subscription:
328
-
329
- ```javascript
330
- // This subscription
331
- socket.emit('subscribe', {
332
- resource: 'posts',
333
- filters: {
334
- status: 'published', // ✅ Valid: defined in searchSchema
335
- invalid_field: 'value' // ❌ Error: not in searchSchema
336
- }
337
- });
338
-
339
- // Returns error:
340
- {
341
- error: {
342
- code: 'INVALID_FILTERS',
343
- message: 'Invalid filter values',
344
- details: {
345
- invalid_field: {
346
- code: 'UNKNOWN_FIELD',
347
- message: 'Field not defined in searchSchema'
348
- }
349
- }
350
- }
351
- }
352
- ```
353
-
354
- ## Security Architecture
355
-
356
- ### Authentication
357
-
358
- All connections must be authenticated using JWT tokens:
359
-
360
- ```javascript
361
- // Client must provide valid JWT
362
- const socket = io('http://localhost:3000', {
363
- auth: {
364
- token: 'eyJhbGciOiJIUzI1NiIs...' // Your JWT token
365
- }
366
- });
367
-
368
- // Without valid token, connection is rejected
369
- socket.on('connect_error', (error) => {
370
- console.error('Authentication failed:', error.message);
371
- });
372
- ```
373
-
374
- ### Permission Checking
375
-
376
- Subscriptions require 'query' permission on the resource:
377
-
378
- ```javascript
379
- // In your scope definition
380
- await api.addResource('secret-documents', {
381
- // ... schema ...
382
-
383
- checkPermissions: async ({ method, auth }) => {
384
- if (method === 'query') {
385
- // Check if user can query/subscribe to this resource
386
- return auth.roles?.includes('admin');
387
- }
388
- // ... other permission checks
389
- }
390
- });
391
- ```
392
-
393
- ### Filter Injection with Hooks
394
-
395
- Use the `subscriptionFilters` hook to enforce security policies:
396
-
397
- ```javascript
398
- // Multi-tenancy plugin example
399
- export const MultiTenancyPlugin = {
400
- name: 'multi-tenancy',
401
-
402
- install({ addHook }) {
403
- // This hook runs for EVERY subscription
404
- addHook('subscriptionFilters', 'workspace-isolation', {},
405
- async ({ subscription, auth }) => {
406
- // Force workspace isolation
407
- if (!auth.workspace_id) {
408
- throw new Error('User must belong to a workspace');
409
- }
410
-
411
- // Always add workspace filter
412
- subscription.filters.workspace_id = auth.workspace_id;
413
-
414
- // Prevent bypassing workspace isolation
415
- if (subscription.filters.workspace_id &&
416
- subscription.filters.workspace_id !== auth.workspace_id) {
417
- throw new Error('Cannot subscribe to other workspaces');
418
- }
419
- }
420
- );
421
- }
422
- };
423
-
424
- // Now ALL subscriptions automatically include workspace filter
425
- socket.emit('subscribe', {
426
- resource: 'projects',
427
- filters: { status: 'active' }
428
- });
429
- // Server automatically adds: filters.workspace_id = user's workspace
430
- ```
431
-
432
- ### Data Isolation Example
433
-
434
- Here's a complete example showing how data isolation works:
435
-
436
- ```javascript
437
- // User Roles Plugin
438
- export const UserRolesPlugin = {
439
- name: 'user-roles',
440
-
441
- install({ addHook }) {
442
- // Filter subscriptions based on user role
443
- addHook('subscriptionFilters', 'role-based-filters', {},
444
- async ({ subscription, auth }) => {
445
- const { resource, filters } = subscription;
446
-
447
- // Regular users can only see their own data
448
- if (!auth.roles?.includes('admin')) {
449
- switch (resource) {
450
- case 'orders':
451
- subscription.filters.customer_id = auth.user_id;
452
- break;
453
-
454
- case 'invoices':
455
- subscription.filters.user_id = auth.user_id;
456
- break;
457
-
458
- case 'messages':
459
- // Can see messages where they're sender or recipient
460
- subscription.filters.$or = [
461
- { sender_id: auth.user_id },
462
- { recipient_id: auth.user_id }
463
- ];
464
- break;
465
-
466
- case 'admin-logs':
467
- throw new Error('Access denied to admin resources');
468
- }
469
- }
470
- }
471
- );
472
- }
473
- };
474
- ```
475
-
476
- ## Advanced Features
477
-
478
- ### Redis Adapter for Scaling
479
-
480
- When running multiple servers, use Redis adapter for proper broadcasting:
481
-
482
- ```javascript
483
- // Server configuration
484
- const io = await api.startSocketServer(server, {
485
- redis: {
486
- host: process.env.REDIS_HOST || 'localhost',
487
- port: process.env.REDIS_PORT || 6379,
488
- password: process.env.REDIS_PASSWORD,
489
- db: 0
490
- }
491
- });
492
-
493
- // Now broadcasts work across all servers
494
- // Server A: Record updated → broadcast
495
- // Server B: Receives broadcast → notifies its connected clients
496
- ```
497
-
498
- ### Subscription Management
499
-
500
- Each socket can have multiple subscriptions with different filters:
501
-
502
- ```javascript
503
- // Subscribe to different filtered views
504
- const sub1 = await subscribeToResource(socket, {
505
- resource: 'orders',
506
- filters: { status: 'pending' }
507
- });
508
-
509
- const sub2 = await subscribeToResource(socket, {
510
- resource: 'orders',
511
- filters: { status: 'processing', priority: 'high' }
512
- });
513
-
514
- const sub3 = await subscribeToResource(socket, {
515
- resource: 'products',
516
- filters: { category_id: 5, in_stock: true }
517
- });
518
-
519
- // Unsubscribe from specific subscription
520
- socket.emit('unsubscribe', {
521
- subscriptionId: sub1.subscriptionId
522
- });
523
- ```
524
-
525
- ### Include and Fields Storage
526
-
527
- While notifications don't include data, subscriptions can store include/fields preferences:
528
-
529
- ```javascript
530
- // Subscribe with preferred includes and fields
531
- socket.emit('subscribe', {
532
- resource: 'posts',
533
- filters: { status: 'published' },
534
- include: ['author', 'comments.user'],
535
- fields: {
536
- posts: ['title', 'summary', 'published_at'],
537
- users: ['name', 'avatar'],
538
- comments: ['body', 'created_at']
539
- }
540
- });
541
-
542
- // Client can use these when fetching
543
- socket.on('subscription.update', async (notification) => {
544
- // Use the stored preferences for fetching
545
- const url = `/api/posts/${notification.id}?` +
546
- 'include=author,comments.user&' +
547
- 'fields[posts]=title,summary,published_at&' +
548
- 'fields[users]=name,avatar';
549
-
550
- const response = await fetch(url);
551
- });
552
- ```
553
-
554
- ### Reconnection Support
555
-
556
- Restore subscriptions after reconnection:
557
-
558
- ```javascript
559
- // Store active subscriptions
560
- const activeSubscriptions = new Map();
561
-
562
- socket.on('subscription.created', (response) => {
563
- activeSubscriptions.set(response.subscriptionId, response);
564
- });
565
-
566
- // On reconnect, restore all subscriptions
567
- socket.on('connect', async () => {
568
- if (activeSubscriptions.size > 0) {
569
- const { restored, failed } = await restoreSubscriptions(
570
- socket,
571
- Array.from(activeSubscriptions.values())
572
- );
573
-
574
- console.log(`Restored ${restored.length} subscriptions`);
575
- if (failed.length > 0) {
576
- console.error(`Failed to restore ${failed.length} subscriptions`);
577
- }
578
- }
579
- });
580
-
581
- async function restoreSubscriptions(socket, subscriptions) {
582
- return new Promise((resolve) => {
583
- socket.emit('restore-subscriptions',
584
- { subscriptions },
585
- resolve
586
- );
587
- });
588
- }
589
- ```
590
-
591
- ### Error Handling
592
-
593
- The plugin provides detailed error information:
594
-
595
- ```javascript
596
- socket.on('subscription.error', (error) => {
597
- switch (error.code) {
598
- case 'RESOURCE_NOT_FOUND':
599
- console.error(`Resource type '${error.resource}' doesn't exist`);
600
- break;
601
-
602
- case 'PERMISSION_DENIED':
603
- console.error('You lack permission to subscribe to this resource');
604
- break;
605
-
606
- case 'INVALID_FILTERS':
607
- console.error('Filter validation failed:', error.details);
608
- break;
609
-
610
- case 'UNSUPPORTED_FILTER':
611
- console.error(`Filter requires 'filterRecord' for WebSocket support`);
612
- break;
613
-
614
- case 'FILTERING_NOT_ENABLED':
615
- console.error('Resource does not have searchSchema defined');
616
- break;
617
- }
618
- });
619
- ```
620
-
621
- ## Client Usage
622
-
623
- ### Basic Setup
624
-
625
- ```javascript
626
- import { io } from 'socket.io-client';
627
-
628
- // Connect with authentication
629
- const socket = io('http://localhost:3000', {
630
- auth: {
631
- token: localStorage.getItem('jwt_token')
632
- }
633
- });
634
-
635
- // Handle connection events
636
- socket.on('connect', () => {
637
- console.log('Connected to WebSocket server');
638
- });
639
-
640
- socket.on('connect_error', (error) => {
641
- console.error('Connection failed:', error.message);
642
- });
643
-
644
- socket.on('disconnect', (reason) => {
645
- console.log('Disconnected:', reason);
646
- });
647
- ```
648
-
649
- ### Subscribing to Resources
650
-
651
- ```javascript
652
- // Helper function for subscribing
653
- async function subscribeToResource(socket, options) {
654
- return new Promise((resolve, reject) => {
655
- socket.emit('subscribe', options, (response) => {
656
- if (response.error) {
657
- reject(response.error);
658
- } else {
659
- resolve(response.data);
660
- }
661
- });
662
- });
663
- }
664
-
665
- // Subscribe to filtered resources
666
- try {
667
- const subscription = await subscribeToResource(socket, {
668
- resource: 'posts',
669
- filters: {
670
- status: 'published',
671
- category_id: 5
672
- }
673
- });
674
-
675
- console.log('Subscribed:', subscription.subscriptionId);
676
- } catch (error) {
677
- console.error('Subscription failed:', error);
678
- }
679
- ```
680
-
681
- ### Handling Updates
682
-
683
- ```javascript
684
- // Set up update handler
685
- socket.on('subscription.update', async (notification) => {
686
- console.log('Resource updated:', notification);
687
- // {
688
- // type: 'resource.updated',
689
- // resource: 'posts',
690
- // id: '123',
691
- // action: 'update',
692
- // subscriptionId: 'posts-1234567-abc',
693
- // meta: { timestamp: '2024-01-15T10:00:00Z' }
694
- // }
695
-
696
- // Handle different actions
697
- switch (notification.action) {
698
- case 'post':
699
- await handleNewResource(notification);
700
- break;
701
-
702
- case 'update':
703
- case 'patch':
704
- await handleUpdatedResource(notification);
705
- break;
706
-
707
- case 'delete':
708
- await handleDeletedResource(notification);
709
- break;
710
- }
711
- });
712
-
713
- // Fetch updated data when needed
714
- async function handleUpdatedResource(notification) {
715
- // Check if user is viewing this resource
716
- if (isCurrentlyViewing(notification.resource, notification.id)) {
717
- // Fetch immediately
718
- const response = await fetch(
719
- `/api/${notification.resource}/${notification.id}`,
720
- { headers: { Authorization: `Bearer ${token}` } }
721
- );
722
-
723
- if (response.ok) {
724
- const data = await response.json();
725
- updateUI(data);
726
- }
727
- } else {
728
- // Just invalidate cache
729
- cacheManager.invalidate(notification.resource, notification.id);
730
- }
731
- }
732
- ```
733
-
734
- ### Complete Client Example
735
-
736
- ```javascript
737
- class RealtimeResourceManager {
738
- constructor(apiUrl, token) {
739
- this.apiUrl = apiUrl;
740
- this.token = token;
741
- this.subscriptions = new Map();
742
- this.cache = new Map();
743
-
744
- this.socket = io(apiUrl, {
745
- auth: { token }
746
- });
747
-
748
- this.setupEventHandlers();
749
- }
750
-
751
- setupEventHandlers() {
752
- this.socket.on('connect', () => {
753
- console.log('WebSocket connected');
754
- this.restoreSubscriptions();
755
- });
756
-
757
- this.socket.on('subscription.update', (notification) => {
758
- this.handleUpdate(notification);
759
- });
760
-
761
- this.socket.on('subscription.created', (response) => {
762
- this.subscriptions.set(response.subscriptionId, response);
763
- });
764
- }
765
-
766
- async subscribe(resource, filters = {}, options = {}) {
767
- return new Promise((resolve, reject) => {
768
- this.socket.emit('subscribe', {
769
- resource,
770
- filters,
771
- ...options
772
- }, (response) => {
773
- if (response.error) {
774
- reject(response.error);
775
- } else {
776
- resolve(response.data);
777
- }
778
- });
779
- });
780
- }
781
-
782
- async handleUpdate(notification) {
783
- const { resource, id, action } = notification;
784
-
785
- // Invalidate cache
786
- const cacheKey = `${resource}:${id}`;
787
- this.cache.delete(cacheKey);
788
-
789
- // Emit custom event for UI updates
790
- this.emit('resource:updated', {
791
- resource,
792
- id,
793
- action,
794
- notification
795
- });
796
- }
797
-
798
- async fetchResource(resource, id, options = {}) {
799
- const cacheKey = `${resource}:${id}`;
800
-
801
- // Check cache first
802
- if (this.cache.has(cacheKey)) {
803
- return this.cache.get(cacheKey);
804
- }
805
-
806
- // Fetch from API
807
- const queryString = new URLSearchParams(options).toString();
808
- const url = `${this.apiUrl}/${resource}/${id}${queryString ? '?' + queryString : ''}`;
809
-
810
- const response = await fetch(url, {
811
- headers: {
812
- Authorization: `Bearer ${this.token}`
813
- }
814
- });
815
-
816
- if (!response.ok) {
817
- throw new Error(`Failed to fetch ${resource}/${id}`);
818
- }
819
-
820
- const data = await response.json();
821
-
822
- // Cache the result
823
- this.cache.set(cacheKey, data);
824
-
825
- return data;
826
- }
827
-
828
- async restoreSubscriptions() {
829
- if (this.subscriptions.size === 0) return;
830
-
831
- const subscriptions = Array.from(this.subscriptions.values());
832
-
833
- return new Promise((resolve) => {
834
- this.socket.emit('restore-subscriptions',
835
- { subscriptions },
836
- (response) => {
837
- if (response.error) {
838
- console.error('Failed to restore subscriptions:', response.error);
839
- } else {
840
- console.log(`Restored ${response.restored.length} subscriptions`);
841
- }
842
- resolve(response);
843
- }
844
- );
845
- });
846
- }
847
- }
848
-
849
- // Usage
850
- const realtime = new RealtimeResourceManager(
851
- 'http://localhost:3000',
852
- localStorage.getItem('jwt_token')
853
- );
854
-
855
- // Subscribe to posts
856
- await realtime.subscribe('posts', {
857
- status: 'published',
858
- author_id: currentUser.id
859
- });
860
-
861
- // React to updates
862
- realtime.on('resource:updated', async ({ resource, id, action }) => {
863
- if (resource === 'posts' && isViewingPost(id)) {
864
- const post = await realtime.fetchResource('posts', id, {
865
- include: 'author,comments'
866
- });
867
- updatePostUI(post);
868
- }
869
- });
870
- ```
871
-
872
- ## Performance Considerations
873
-
874
- ### Subscription Limits
875
-
876
- Each socket is limited to 100 subscriptions to prevent memory exhaustion:
877
-
878
- ```javascript
879
- // After 100 subscriptions, new ones are rejected
880
- socket.emit('subscribe', { resource: 'posts' }, (response) => {
881
- if (response.error?.code === 'SUBSCRIPTION_LIMIT_EXCEEDED') {
882
- console.error('Too many active subscriptions');
883
- }
884
- });
885
- ```
886
-
887
- ### Filter Efficiency
888
-
889
- - **Simple operators** (`=`, `>`, etc.) are very fast - just property comparisons
890
- - **Complex filters** with `filterRecord` functions should be kept lightweight
891
- - **Avoid expensive operations** in filterRecord (no async calls, minimal computation)
892
-
893
- ### Broadcast Optimization
894
-
895
- The plugin optimizes broadcasts by:
896
-
897
- 1. **Single broadcast per change** - Not N broadcasts for N subscribers
898
- 2. **Room-based delivery** - Socket.IO efficiently handles room broadcasts
899
- 3. **Minimal payload** - Notifications are tiny (< 200 bytes)
900
- 4. **In-memory filtering** - Uses context.minimalRecord, no database queries
901
-
902
- ### Client-Side Optimization
903
-
904
- Optimize your client implementation:
905
-
906
- ```javascript
907
- // Batch fetch requests
908
- const pendingFetches = new Set();
909
-
910
- socket.on('subscription.update', (notification) => {
911
- pendingFetches.add(`${notification.resource}:${notification.id}`);
912
- });
913
-
914
- // Fetch in batches every 100ms
915
- setInterval(async () => {
916
- if (pendingFetches.size === 0) return;
917
-
918
- const toFetch = Array.from(pendingFetches);
919
- pendingFetches.clear();
920
-
921
- // Batch fetch multiple resources
922
- const results = await Promise.all(
923
- toFetch.map(key => {
924
- const [resource, id] = key.split(':');
925
- return fetchResource(resource, id);
926
- })
927
- );
928
-
929
- // Update UI with all results
930
- updateBatchUI(results);
931
- }, 100);
932
- ```
933
-
934
- ## Troubleshooting
935
-
936
- ### Common Issues
937
-
938
- **1. Subscriptions not receiving updates**
939
-
940
- Check:
941
- - Are filters too restrictive?
942
- - Does the record pass `context.minimalRecord` filtering?
943
- - Are you in a transaction that hasn't committed?
944
-
945
- **2. "UNSUPPORTED_FILTER" errors**
946
-
947
- If using custom filterOperator functions, you must provide filterRecord:
948
-
949
- ```javascript
950
- // ❌ This will error for WebSocket
951
- searchSchema: {
952
- complex_search: {
953
- type: 'string',
954
- filterOperator: function(query, value) { /* SQL */ }
955
- }
956
- }
957
-
958
- // ✅ This works for both REST and WebSocket
959
- searchSchema: {
960
- complex_search: {
961
- type: 'string',
962
- filterOperator: function(query, value) { /* SQL */ },
963
- filterRecord: function(record, value) { /* JavaScript */ }
964
- }
965
- }
966
- ```
967
-
968
- **3. Authentication failures**
969
-
970
- Ensure:
971
- - JWT token is valid and not expired
972
- - Token is sent in auth.token, not headers
973
- - JWT plugin is configured correctly
974
-
975
- **4. Redis connection issues**
976
-
977
- If using Redis adapter:
978
- - Check Redis server is running
979
- - Verify connection credentials
980
- - Ensure all servers use same Redis instance
981
-
982
- ### Debug Logging
983
-
984
- Enable debug logging to troubleshoot:
985
-
986
- ```javascript
987
- // Server-side
988
- const api = new Api({
989
- name: 'my-api',
990
- logging: { level: 'debug' }
991
- });
992
-
993
- // Client-side
994
- localStorage.debug = 'socket.io-client:*';
995
- ```
996
-
997
- ### Testing WebSocket Functionality
998
-
999
- ```javascript
1000
- // Test helper for WebSocket subscriptions
1001
- async function testWebSocketSubscription() {
1002
- const socket = io('http://localhost:3000', {
1003
- auth: { token: testToken }
1004
- });
1005
-
1006
- return new Promise((resolve, reject) => {
1007
- socket.on('connect', async () => {
1008
- console.log('✓ Connected to WebSocket');
1009
-
1010
- // Test subscription
1011
- socket.emit('subscribe', {
1012
- resource: 'posts',
1013
- filters: { status: 'published' }
1014
- }, (response) => {
1015
- if (response.error) {
1016
- console.error('✗ Subscription failed:', response.error);
1017
- reject(response.error);
1018
- } else {
1019
- console.log('✓ Subscription successful:', response.data);
1020
-
1021
- // Wait for an update
1022
- socket.once('subscription.update', (notification) => {
1023
- console.log('✓ Received update:', notification);
1024
- socket.close();
1025
- resolve(notification);
1026
- });
1027
-
1028
- // Trigger an update
1029
- createTestPost();
1030
- }
1031
- });
1032
- });
1033
-
1034
- socket.on('connect_error', (error) => {
1035
- console.error('✗ Connection failed:', error.message);
1036
- reject(error);
1037
- });
1038
- });
1039
- }
1040
- ```