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.
- package/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
package/tests/socketio.test.js
CHANGED
|
@@ -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
|
+
})
|