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,860 +0,0 @@
1
- import { describe, it, before, after, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import knexLib from 'knex'
4
- import { io as ioClient } from 'socket.io-client'
5
- import { SignJWT } from 'jose'
6
- import {
7
- validateJsonApiStructure,
8
- cleanTables,
9
- createJsonApiDocument,
10
- assertResourceAttributes,
11
- createRelationship,
12
- resourceIdentifier
13
- } from './helpers/test-utils.js'
14
- import { createWebSocketApi } from './fixtures/api-configs.js'
15
-
16
- // Create JWT token using jose
17
- async function createToken (payload = {}, secret = 'test-secret-key') {
18
- const encoder = new TextEncoder()
19
- const key = encoder.encode(secret)
20
-
21
- const jwt = new SignJWT({
22
- sub: '123',
23
- email: 'test@example.com',
24
- roles: ['user'],
25
- ...payload,
26
- jti: `test-${Date.now()}`
27
- })
28
- .setProtectedHeader({ alg: 'HS256' })
29
- .setIssuedAt()
30
- .setExpirationTime('1h')
31
-
32
- return await jwt.sign(key)
33
- }
34
-
35
- // Create Knex instance for tests
36
- const knex = knexLib({
37
- client: 'better-sqlite3',
38
- connection: {
39
- filename: ':memory:'
40
- },
41
- useNullAsDefault: true
42
- })
43
-
44
- // API instance that persists across ALL tests
45
- let api
46
- let server
47
-
48
- // Helper function to wait for socket event with timeout
49
- function waitForSocketEvent (socket, eventName, timeout = 1000) {
50
- return new Promise((resolve, reject) => {
51
- const timer = setTimeout(() => {
52
- reject(new Error(`Timeout waiting for ${eventName}`))
53
- }, timeout)
54
-
55
- socket.once(eventName, (data) => {
56
- clearTimeout(timer)
57
- resolve(data)
58
- })
59
- })
60
- }
61
-
62
- describe('WebSocket/Socket.IO Plugin', () => {
63
- // IMPORTANT: before() runs ONCE for the entire test suite
64
- before(async () => {
65
- // Create API instance ONCE with WebSocket support
66
- const result = await createWebSocketApi(knex)
67
- api = result.api
68
- server = result.server
69
- })
70
-
71
- // IMPORTANT: after() cleans up resources
72
- after(async () => {
73
- // Skip cleanup if nothing was initialized
74
- if (!api || !server) {
75
- return
76
- }
77
-
78
- // Close Socket.IO server first
79
- if (api.vars.socketIO) {
80
- // Disconnect all connected sockets
81
- await api.vars.socketIO.disconnectSockets()
82
-
83
- // Close the Socket.IO server
84
- await new Promise((resolve) => {
85
- api.vars.socketIO.close(() => {
86
- resolve()
87
- })
88
- })
89
- }
90
-
91
- // Close Redis clients if they exist
92
- if (api.vars.socketIORedisClients) {
93
- await api.vars.socketIORedisClients.pubClient.quit()
94
- await api.vars.socketIORedisClients.subClient.quit()
95
- }
96
-
97
- // Close HTTP server
98
- if (server) {
99
- await new Promise((resolve) => {
100
- server.close(resolve)
101
- })
102
- }
103
-
104
- // Clean up JWT plugin resources
105
- if (api.helpers?.auth?.cleanup) {
106
- api.helpers.auth.cleanup()
107
- }
108
-
109
- // Always destroy knex connection to allow tests to exit
110
- await knex.destroy()
111
- })
112
-
113
- // IMPORTANT: beforeEach() cleans data but does NOT recreate API
114
- beforeEach(async () => {
115
- // Clean all tables
116
- await cleanTables(knex, [
117
- 'basic_countries',
118
- 'basic_publishers',
119
- 'basic_authors',
120
- 'basic_books',
121
- 'basic_book_authors'
122
- ])
123
- })
124
-
125
- describe('Basic Subscription and Notifications', () => {
126
- it('should receive minimal notifications for subscribed resources', async () => {
127
- // Generate a real JWT token for testing
128
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
129
-
130
- const socket = ioClient(`http://localhost:${server.address().port}`, {
131
- path: '/api/socket.io',
132
- auth: { token }
133
- })
134
-
135
- try {
136
- // Wait for connection
137
- await new Promise((resolve, reject) => {
138
- socket.on('connect', resolve)
139
- socket.on('connect_error', reject)
140
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
141
- })
142
-
143
- // Subscribe to posts with status filter
144
- const subResponse = await new Promise((resolve) => {
145
- socket.emit('subscribe', {
146
- resource: 'books',
147
- filters: { title: 'Test Book' } // Exact match
148
- }, resolve)
149
- })
150
-
151
- assert(subResponse.success, 'Subscription should succeed')
152
- assert(subResponse.data.subscriptionId, 'Should return subscription ID')
153
-
154
- // Listen for updates
155
- const updatePromise = waitForSocketEvent(socket, 'subscription.update')
156
-
157
- // Create a country first (required for books)
158
- const countryDoc = createJsonApiDocument('countries', {
159
- name: 'Test Country',
160
- code: 'TC'
161
- })
162
- const countryResult = await api.resources.countries.post({
163
- inputRecord: countryDoc,
164
- simplified: false
165
- })
166
-
167
- // Create a book using API
168
- const bookDoc = createJsonApiDocument('books',
169
- { title: 'Test Book' },
170
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
171
- )
172
-
173
- const createResult = await api.resources.books.post({
174
- inputRecord: bookDoc,
175
- simplified: false
176
- })
177
-
178
- // Check notification
179
- const notification = await updatePromise
180
- assert.equal(notification.type, 'resource.postd')
181
- assert.equal(notification.resource, 'books')
182
- assert.equal(String(notification.id), String(createResult.data.id))
183
- assert.equal(notification.action, 'post')
184
- assert(!notification.data, 'Should not include data in notification')
185
- } finally {
186
- socket.close()
187
- }
188
- })
189
-
190
- it('should not receive notifications for non-matching filters', async () => {
191
- // Generate a real JWT token for testing
192
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
193
-
194
- const socket = ioClient(`http://localhost:${server.address().port}`, {
195
- path: '/api/socket.io',
196
- auth: { token }
197
- })
198
-
199
- try {
200
- // Wait for connection
201
- await new Promise((resolve, reject) => {
202
- socket.on('connect', () => {
203
- console.log('Socket connected successfully')
204
- resolve()
205
- })
206
- socket.on('connect_error', (error) => {
207
- console.error('Connection error:', error.message, error.type)
208
- reject(error)
209
- })
210
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
211
- })
212
-
213
- // Subscribe with specific filter
214
- await new Promise((resolve) => {
215
- socket.emit('subscribe', {
216
- resource: 'books',
217
- filters: { title: 'Specific Title' }
218
- }, resolve)
219
- })
220
-
221
- // Create a country first (required for books)
222
- const countryDoc = createJsonApiDocument('countries', {
223
- name: 'Test Country',
224
- code: 'TC'
225
- })
226
- const countryResult = await api.resources.countries.post({
227
- inputRecord: countryDoc,
228
- simplified: false
229
- })
230
-
231
- // Create a book that doesn't match filter
232
- const bookDoc = createJsonApiDocument('books',
233
- { title: 'Different Book' },
234
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
235
- )
236
-
237
- await api.resources.books.post({
238
- inputRecord: bookDoc,
239
- simplified: false
240
- })
241
-
242
- // Should not receive update
243
- await assert.rejects(
244
- waitForSocketEvent(socket, 'subscription.update', 500),
245
- { message: /Timeout/ }
246
- )
247
- } finally {
248
- socket.close()
249
- }
250
- })
251
- })
252
-
253
- describe('Filter Validation', () => {
254
- it('should validate filters against searchSchema', async () => {
255
- // Generate a real JWT token for testing
256
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
257
-
258
- const socket = ioClient(`http://localhost:${server.address().port}`, {
259
- path: '/api/socket.io',
260
- auth: { token }
261
- })
262
-
263
- try {
264
- // Wait for connection
265
- await new Promise((resolve, reject) => {
266
- socket.on('connect', resolve)
267
- socket.on('connect_error', reject)
268
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
269
- })
270
- // Try invalid filter
271
- const response = await new Promise((resolve) => {
272
- socket.emit('subscribe', {
273
- resource: 'books',
274
- filters: { invalid_field: 'value' }
275
- }, resolve)
276
- })
277
-
278
- assert(response.error, 'Should return error')
279
- assert.equal(response.error.code, 'INVALID_FILTERS')
280
- } finally {
281
- socket.close()
282
- }
283
- })
284
-
285
- it('should reject function filters without filterRecord', async () => {
286
- // Generate a real JWT token for testing
287
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
288
-
289
- const socket = ioClient(`http://localhost:${server.address().port}`, {
290
- path: '/api/socket.io',
291
- auth: { token }
292
- })
293
-
294
- try {
295
- // Wait for connection
296
- await new Promise((resolve, reject) => {
297
- socket.on('connect', resolve)
298
- socket.on('connect_error', reject)
299
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
300
- })
301
- // Create a country first
302
- const countryDoc = createJsonApiDocument('countries', {
303
- name: 'Test Country',
304
- code: 'TC'
305
- })
306
- const countryResult = await api.resources.countries.post({
307
- inputRecord: countryDoc,
308
- simplified: false
309
- })
310
-
311
- // Assuming we add a complex filter to the schema that uses filterOperator function
312
- // For now, test with country_id which is a simple filter
313
- const response = await new Promise((resolve) => {
314
- socket.emit('subscribe', {
315
- resource: 'books',
316
- filters: { country: countryResult.data.id }
317
- }, resolve)
318
- })
319
-
320
- // Should succeed for simple filters
321
- assert(response.success, 'Should succeed for simple filters')
322
- } finally {
323
- socket.close()
324
- }
325
- })
326
- })
327
-
328
- describe('Transaction Safety', () => {
329
- // These tests verify that broadcasts are properly deferred until after transaction commit
330
- // The implementation uses:
331
- // - WeakMap to store pending broadcasts per transaction
332
- // - afterCommit hook to broadcast after successful transactions
333
- // - afterRollback hook to clean up after failed transactions
334
- // - No mutation of transaction objects
335
-
336
- it('should not broadcast when operations fail', async () => {
337
- // This test verifies that broadcasts don't happen when operations fail
338
- // The library should rollback transactions and not send notifications
339
-
340
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
341
-
342
- const socket = ioClient(`http://localhost:${server.address().port}`, {
343
- path: '/api/socket.io',
344
- auth: { token }
345
- })
346
-
347
- try {
348
- // Wait for connection
349
- await new Promise((resolve, reject) => {
350
- socket.on('connect', resolve)
351
- socket.on('connect_error', reject)
352
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
353
- })
354
-
355
- // Subscribe to all books
356
- await new Promise((resolve) => {
357
- socket.emit('subscribe', { resource: 'books' }, resolve)
358
- })
359
-
360
- // Track notifications
361
- const notifications = []
362
- socket.on('subscription.update', (notification) => {
363
- notifications.push(notification)
364
- })
365
-
366
- // Try to create a book without required country relationship
367
- const bookDoc = createJsonApiDocument('books',
368
- { title: 'Invalid Book' }
369
- // Missing required country relationship
370
- )
371
-
372
- try {
373
- await api.resources.books.post({
374
- inputRecord: bookDoc,
375
- simplified: false
376
- })
377
- assert.fail('Should have thrown an error')
378
- } catch (error) {
379
- // Expected to fail due to missing required relationship
380
- }
381
-
382
- // Wait a bit to ensure no broadcast happens
383
- await new Promise(resolve => setTimeout(resolve, 100))
384
-
385
- // Should not have received any notification
386
- assert.equal(notifications.length, 0, 'Should not receive any notifications for failed operations')
387
- } finally {
388
- socket.close()
389
- }
390
- })
391
-
392
- it('should broadcast after successful operations', async () => {
393
- // This test verifies that broadcasts happen after successful operations
394
- // The library handles transactions internally and uses afterCommit hook
395
- // to ensure broadcasts happen only after successful commit
396
-
397
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
398
-
399
- const socket = ioClient(`http://localhost:${server.address().port}`, {
400
- path: '/api/socket.io',
401
- auth: { token }
402
- })
403
-
404
- try {
405
- // Wait for connection
406
- await new Promise((resolve, reject) => {
407
- socket.on('connect', resolve)
408
- socket.on('connect_error', reject)
409
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
410
- })
411
-
412
- // Subscribe to all books
413
- await new Promise((resolve) => {
414
- socket.emit('subscribe', { resource: 'books' }, resolve)
415
- })
416
-
417
- // Create a country first (required for books)
418
- const countryDoc = createJsonApiDocument('countries', {
419
- name: 'Test Country',
420
- code: 'TC'
421
- })
422
- const countryResult = await api.resources.countries.post({
423
- inputRecord: countryDoc,
424
- simplified: false
425
- })
426
-
427
- // Track notifications
428
- const notifications = []
429
- socket.on('subscription.update', (notification) => {
430
- notifications.push(notification)
431
- })
432
-
433
- // Create book - the library will handle transaction internally if configured
434
- const bookDoc = createJsonApiDocument('books',
435
- { title: 'Broadcast Test Book' },
436
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
437
- )
438
-
439
- const createResult = await api.resources.books.post({
440
- inputRecord: bookDoc,
441
- simplified: false
442
- })
443
-
444
- // Wait a bit to ensure broadcast happens (setImmediate)
445
- await new Promise(resolve => setTimeout(resolve, 50))
446
-
447
- // Should have received the notification
448
- assert.equal(notifications.length, 1, 'Should receive one notification')
449
- assert.equal(notifications[0].type, 'resource.postd')
450
- assert.equal(notifications[0].resource, 'books')
451
- assert.equal(String(notifications[0].id), String(createResult.data.id))
452
- } finally {
453
- socket.close()
454
- }
455
- })
456
-
457
- it('should broadcast deferred patch notifications from relationship PATCH', async () => {
458
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
459
-
460
- const socket = ioClient(`http://localhost:${server.address().port}`, {
461
- path: '/api/socket.io',
462
- auth: { token }
463
- })
464
-
465
- try {
466
- await new Promise((resolve, reject) => {
467
- socket.on('connect', resolve)
468
- socket.on('connect_error', reject)
469
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
470
- })
471
-
472
- const country = await api.resources.countries.post({
473
- inputRecord: createJsonApiDocument('countries', {
474
- name: 'Original Country',
475
- code: 'OC'
476
- }),
477
- simplified: false
478
- })
479
-
480
- const nextCountry = await api.resources.countries.post({
481
- inputRecord: createJsonApiDocument('countries', {
482
- name: 'Next Country',
483
- code: 'NC'
484
- }),
485
- simplified: false
486
- })
487
-
488
- const book = await api.resources.books.post({
489
- inputRecord: createJsonApiDocument(
490
- 'books',
491
- { title: 'Relationship Patch Broadcast' },
492
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
493
- ),
494
- simplified: false
495
- })
496
-
497
- await new Promise((resolve) => {
498
- socket.emit('subscribe', { resource: 'books' }, resolve)
499
- })
500
-
501
- const updatePromise = waitForSocketEvent(socket, 'subscription.update', 1000)
502
-
503
- await api.resources.books.patchRelationship({
504
- id: book.data.id,
505
- relationshipName: 'country',
506
- relationshipData: resourceIdentifier('countries', nextCountry.data.id)
507
- })
508
-
509
- const notification = await updatePromise
510
- assert.equal(notification.type, 'resource.patchd')
511
- assert.equal(notification.resource, 'books')
512
- assert.equal(String(notification.id), String(book.data.id))
513
- assert.equal(notification.action, 'patch')
514
- } finally {
515
- socket.close()
516
- }
517
- })
518
- })
519
-
520
- describe('Multiple Subscriptions', () => {
521
- it('should handle multiple subscriptions from same client', async () => {
522
- // Generate a real JWT token for testing
523
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
524
-
525
- const socket = ioClient(`http://localhost:${server.address().port}`, {
526
- path: '/api/socket.io',
527
- auth: { token }
528
- })
529
-
530
- try {
531
- // Wait for connection
532
- await new Promise((resolve, reject) => {
533
- socket.on('connect', resolve)
534
- socket.on('connect_error', reject)
535
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
536
- })
537
- // Subscribe to books and countries
538
- const bookSubResponse = await new Promise((resolve) => {
539
- socket.emit('subscribe', { resource: 'books' }, resolve)
540
- })
541
-
542
- const countrySubResponse = await new Promise((resolve) => {
543
- socket.emit('subscribe', { resource: 'countries' }, resolve)
544
- })
545
-
546
- assert(bookSubResponse.success)
547
- assert(countrySubResponse.success)
548
- assert.notEqual(bookSubResponse.data.subscriptionId, countrySubResponse.data.subscriptionId)
549
-
550
- // Create both resources
551
- const notifications = []
552
- socket.on('subscription.update', (notification) => {
553
- notifications.push(notification)
554
- })
555
-
556
- // Create a country first (required for books)
557
- const countryDoc = createJsonApiDocument('countries', { name: 'Multi Test Country', code: 'MT' })
558
- const countryResult = await api.resources.countries.post({ inputRecord: countryDoc, simplified: false })
559
-
560
- const bookDoc = createJsonApiDocument('books',
561
- { title: 'Multi Test Book' },
562
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
563
- )
564
- await api.resources.books.post({ inputRecord: bookDoc, simplified: false })
565
-
566
- const countryDoc2 = createJsonApiDocument('countries', { name: 'Multi Test Country 2', code: 'MT2' })
567
- await api.resources.countries.post({ inputRecord: countryDoc2, simplified: false })
568
-
569
- // Wait a bit for notifications
570
- await new Promise(resolve => setTimeout(resolve, 100))
571
-
572
- assert.equal(notifications.length, 3, 'Should receive 3 notifications')
573
- assert.equal(notifications.filter(n => n.resource === 'books').length, 1, 'Should receive 1 book notification')
574
- assert.equal(notifications.filter(n => n.resource === 'countries').length, 2, 'Should receive 2 country notifications')
575
- } finally {
576
- socket.close()
577
- }
578
- })
579
-
580
- it('should unsubscribe correctly', async () => {
581
- // Generate a real JWT token for testing
582
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
583
-
584
- const socket = ioClient(`http://localhost:${server.address().port}`, {
585
- path: '/api/socket.io',
586
- auth: { token }
587
- })
588
-
589
- try {
590
- // Wait for connection
591
- await new Promise((resolve, reject) => {
592
- socket.on('connect', resolve)
593
- socket.on('connect_error', reject)
594
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
595
- })
596
- // Subscribe
597
- const subResponse = await new Promise((resolve) => {
598
- socket.emit('subscribe', { resource: 'books' }, resolve)
599
- })
600
-
601
- const subscriptionId = subResponse.data.subscriptionId
602
-
603
- // Unsubscribe
604
- const unsubResponse = await new Promise((resolve) => {
605
- socket.emit('unsubscribe', { subscriptionId }, resolve)
606
- })
607
-
608
- assert(unsubResponse.success)
609
-
610
- // Create a country first (required for books)
611
- const countryDoc = createJsonApiDocument('countries', {
612
- name: 'Test Country',
613
- code: 'TC'
614
- })
615
- const countryResult = await api.resources.countries.post({
616
- inputRecord: countryDoc,
617
- simplified: false
618
- })
619
-
620
- // Create a book
621
- const bookDoc = createJsonApiDocument('books',
622
- { title: 'After Unsub Book' },
623
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
624
- )
625
- await api.resources.books.post({ inputRecord: bookDoc, simplified: false })
626
-
627
- // Should not receive notification
628
- await assert.rejects(
629
- waitForSocketEvent(socket, 'subscription.update', 500),
630
- { message: /Timeout/ }
631
- )
632
- } finally {
633
- socket.close()
634
- }
635
- })
636
- })
637
-
638
- describe('Update and Delete Notifications', () => {
639
- it('should receive notifications for updates', async () => {
640
- // Generate a real JWT token for testing
641
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
642
-
643
- const socket = ioClient(`http://localhost:${server.address().port}`, {
644
- path: '/api/socket.io',
645
- auth: { token }
646
- })
647
-
648
- try {
649
- // Wait for connection
650
- await new Promise((resolve, reject) => {
651
- socket.on('connect', resolve)
652
- socket.on('connect_error', reject)
653
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
654
- })
655
- // Create a country first (required for books)
656
- const countryDoc = createJsonApiDocument('countries', {
657
- name: 'Test Country',
658
- code: 'TC'
659
- })
660
- const countryResult = await api.resources.countries.post({
661
- inputRecord: countryDoc,
662
- simplified: false
663
- })
664
-
665
- // Create a book first
666
- const bookDoc = createJsonApiDocument('books',
667
- { title: 'Original Title' },
668
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
669
- )
670
- const createResult = await api.resources.books.post({
671
- inputRecord: bookDoc,
672
- simplified: false
673
- })
674
- const bookId = createResult.data.id
675
-
676
- // Subscribe to books
677
- await new Promise((resolve) => {
678
- socket.emit('subscribe', { resource: 'books' }, resolve)
679
- })
680
-
681
- // Update the book
682
- const updatePromise = waitForSocketEvent(socket, 'subscription.update')
683
-
684
- const patchDoc = {
685
- data: {
686
- type: 'books',
687
- id: String(bookId),
688
- attributes: { title: 'Updated Title' }
689
- }
690
- }
691
-
692
- await api.resources.books.patch({
693
- id: bookId,
694
- inputRecord: patchDoc,
695
- simplified: false
696
- })
697
-
698
- // Check notification
699
- const notification = await updatePromise
700
- assert.equal(notification.type, 'resource.patchd')
701
- assert.equal(notification.resource, 'books')
702
- assert.equal(String(notification.id), String(bookId))
703
- assert.equal(notification.action, 'patch')
704
- } finally {
705
- socket.close()
706
- }
707
- })
708
-
709
- it('should receive notifications for deletes', async () => {
710
- // Generate a real JWT token for testing
711
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
712
-
713
- const socket = ioClient(`http://localhost:${server.address().port}`, {
714
- path: '/api/socket.io',
715
- auth: { token }
716
- })
717
-
718
- try {
719
- // Wait for connection
720
- await new Promise((resolve, reject) => {
721
- socket.on('connect', resolve)
722
- socket.on('connect_error', reject)
723
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
724
- })
725
- // Create a country first (required for books)
726
- const countryDoc = createJsonApiDocument('countries', {
727
- name: 'Test Country',
728
- code: 'TC'
729
- })
730
- const countryResult = await api.resources.countries.post({
731
- inputRecord: countryDoc,
732
- simplified: false
733
- })
734
-
735
- // Create a book first
736
- const bookDoc = createJsonApiDocument('books',
737
- { title: 'To Delete' },
738
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
739
- )
740
- const createResult = await api.resources.books.post({
741
- inputRecord: bookDoc,
742
- simplified: false
743
- })
744
- const bookId = createResult.data.id
745
-
746
- // Subscribe to books
747
- await new Promise((resolve) => {
748
- socket.emit('subscribe', { resource: 'books' }, resolve)
749
- })
750
-
751
- // Delete the book
752
- const deletePromise = waitForSocketEvent(socket, 'subscription.update')
753
-
754
- await api.resources.books.delete({ id: bookId })
755
-
756
- // Check notification
757
- const notification = await deletePromise
758
- assert.equal(notification.type, 'resource.deleted')
759
- assert.equal(notification.resource, 'books')
760
- assert.equal(String(notification.id), String(bookId))
761
- assert.equal(notification.action, 'delete')
762
- } finally {
763
- socket.close()
764
- }
765
- })
766
- })
767
-
768
- describe('Relationship Filters', () => {
769
- it('should filter by relationship fields', async () => {
770
- // Generate a real JWT token for testing
771
- const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
772
-
773
- const socket = ioClient(`http://localhost:${server.address().port}`, {
774
- path: '/api/socket.io',
775
- auth: { token }
776
- })
777
-
778
- try {
779
- // Wait for connection
780
- await new Promise((resolve, reject) => {
781
- socket.on('connect', resolve)
782
- socket.on('connect_error', reject)
783
- setTimeout(() => reject(new Error('Connection timeout')), 5000)
784
- })
785
- // Create test data
786
- const countryDoc = createJsonApiDocument('countries', { name: 'Filter Country', code: 'FC' })
787
- const countryResult = await api.resources.countries.post({
788
- inputRecord: countryDoc,
789
- simplified: false
790
- })
791
-
792
- const publisherDoc = createJsonApiDocument('publishers',
793
- { name: 'Filter Publisher' },
794
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
795
- )
796
- const publisherResult = await api.resources.publishers.post({
797
- inputRecord: publisherDoc,
798
- simplified: false
799
- })
800
-
801
- // Subscribe to books filtered by publisher
802
- await new Promise((resolve) => {
803
- socket.emit('subscribe', {
804
- resource: 'books',
805
- filters: { publisher: publisherResult.data.id }
806
- }, resolve)
807
- })
808
-
809
- // Create book with matching publisher
810
- const matchingBookPromise = waitForSocketEvent(socket, 'subscription.update')
811
-
812
- const matchingBookDoc = createJsonApiDocument('books',
813
- { title: 'Matching Book' },
814
- {
815
- country: createRelationship(resourceIdentifier('countries', countryResult.data.id)),
816
- publisher: createRelationship(resourceIdentifier('publishers', publisherResult.data.id))
817
- }
818
- )
819
- await api.resources.books.post({
820
- inputRecord: matchingBookDoc,
821
- simplified: false
822
- })
823
-
824
- // Should receive notification
825
- const notification = await matchingBookPromise
826
- assert.equal(notification.resource, 'books')
827
-
828
- // Create book with different publisher
829
- const otherPublisherDoc = createJsonApiDocument('publishers',
830
- { name: 'Other Publisher' },
831
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
832
- )
833
- const otherPublisherResult = await api.resources.publishers.post({
834
- inputRecord: otherPublisherDoc,
835
- simplified: false
836
- })
837
-
838
- const nonMatchingBookDoc = createJsonApiDocument('books',
839
- { title: 'Non-Matching Book' },
840
- {
841
- country: createRelationship(resourceIdentifier('countries', countryResult.data.id)),
842
- publisher: createRelationship(resourceIdentifier('publishers', otherPublisherResult.data.id))
843
- }
844
- )
845
- await api.resources.books.post({
846
- inputRecord: nonMatchingBookDoc,
847
- simplified: false
848
- })
849
-
850
- // Should not receive notification for non-matching book
851
- await assert.rejects(
852
- waitForSocketEvent(socket, 'subscription.update', 500),
853
- { message: /Timeout/ }
854
- )
855
- } finally {
856
- socket.close()
857
- }
858
- })
859
- })
860
- })