json-rest-api 1.0.14 → 1.0.16

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