json-rest-api 1.0.29 → 2.1.0

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