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,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
- })