@jcbuisson/express-x 1.3.7 → 1.4.1
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/package.json +1 -2
- package/src/index.mjs +0 -2
- package/src/server.mjs +49 -17
- package/test/index.test.js +10 -41
- package/src/client.mjs +0 -95
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jcbuisson/express-x",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
},
|
|
19
19
|
"keywords": [],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@jcbuisson/express-x-client": "^1.0.10",
|
|
22
21
|
"@prisma/client": "^4.10.1",
|
|
23
22
|
"axios": "^1.4.0",
|
|
24
23
|
"bcryptjs": "^2.4.3",
|
package/src/index.mjs
CHANGED
package/src/server.mjs
CHANGED
|
@@ -14,10 +14,17 @@ export function expressX(prisma, options = {}) {
|
|
|
14
14
|
if (options.ws === undefined) options.ws = { ws_prefix: "expressx" }
|
|
15
15
|
|
|
16
16
|
const services = {}
|
|
17
|
-
const connections = {}
|
|
18
17
|
|
|
18
|
+
app.connections = {}
|
|
19
19
|
let lastConnectionId = 1
|
|
20
20
|
|
|
21
|
+
app.printCnx = (label) => {
|
|
22
|
+
console.log(label)
|
|
23
|
+
for (const id in app.connections) {
|
|
24
|
+
console.log(`CNX ${id}, data ${JSON.stringify(app.connections[id].data)}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
// logging function - a winston logger must be configured first
|
|
22
29
|
app.log = (severity, message) => {
|
|
23
30
|
const logger = app.get('logger')
|
|
@@ -62,18 +69,21 @@ export function expressX(prisma, options = {}) {
|
|
|
62
69
|
const beforeMethodHooks = service?.hooks?.before && service.hooks.before[methodName] || []
|
|
63
70
|
const beforeAllHooks = service?.hooks?.before?.all || []
|
|
64
71
|
for (const hook of [...beforeMethodHooks, ...beforeAllHooks]) {
|
|
65
|
-
context = await hook(
|
|
72
|
+
// context = await hook(context)
|
|
73
|
+
await hook(context)
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
// call method
|
|
69
77
|
const result = await method(...context.args)
|
|
70
|
-
|
|
78
|
+
// put result into context
|
|
79
|
+
context.result = result
|
|
71
80
|
|
|
72
81
|
// call 'after' hooks
|
|
73
82
|
const afterMethodHooks = service?.hooks?.after && service.hooks.after[methodName] || []
|
|
74
83
|
const afterAllHooks = service?.hooks?.after?.all || []
|
|
75
84
|
for (const hook of [...afterMethodHooks, ...afterAllHooks]) {
|
|
76
|
-
context = await hook(
|
|
85
|
+
// context = await hook(context)
|
|
86
|
+
await hook(context)
|
|
77
87
|
}
|
|
78
88
|
return result
|
|
79
89
|
}
|
|
@@ -117,10 +127,11 @@ export function expressX(prisma, options = {}) {
|
|
|
117
127
|
async function addHttpRest(path, service) {
|
|
118
128
|
const context = {
|
|
119
129
|
app,
|
|
120
|
-
|
|
130
|
+
transport: 'http',
|
|
131
|
+
params: { name: service.name }
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
// introspect schema
|
|
134
|
+
// introspect schema and return a map: field name => prisma type
|
|
124
135
|
async function getTypesMap() {
|
|
125
136
|
const dmmf = await service.prisma._getDmmf()
|
|
126
137
|
const fieldDescriptions = dmmf.modelMap[service.name].fields
|
|
@@ -133,7 +144,7 @@ export function expressX(prisma, options = {}) {
|
|
|
133
144
|
|
|
134
145
|
app.post(path, async (req, res) => {
|
|
135
146
|
app.log('verbose', `http request POST ${req.url}`)
|
|
136
|
-
context.
|
|
147
|
+
context.params.req = req
|
|
137
148
|
try {
|
|
138
149
|
const value = await service.__create(context, { data: req.body })
|
|
139
150
|
publish(service, 'create', value)
|
|
@@ -146,7 +157,7 @@ export function expressX(prisma, options = {}) {
|
|
|
146
157
|
|
|
147
158
|
app.get(path, async (req, res) => {
|
|
148
159
|
app.log('verbose', `http request GET ${req.url}`)
|
|
149
|
-
context.
|
|
160
|
+
context.params.req = req
|
|
150
161
|
const query = { ...req.query }
|
|
151
162
|
try {
|
|
152
163
|
// the values in `req.query` are all strings, but Prisma need proper types
|
|
@@ -182,7 +193,7 @@ export function expressX(prisma, options = {}) {
|
|
|
182
193
|
|
|
183
194
|
app.get(`${path}/:id`, async (req, res) => {
|
|
184
195
|
app.log('verbose', `http request GET ${req.url}`)
|
|
185
|
-
context.
|
|
196
|
+
context.params.req = req
|
|
186
197
|
try {
|
|
187
198
|
const value = await service.__findUnique(context, {
|
|
188
199
|
where: {
|
|
@@ -199,7 +210,7 @@ export function expressX(prisma, options = {}) {
|
|
|
199
210
|
|
|
200
211
|
app.patch(`${path}/:id`, async (req, res) => {
|
|
201
212
|
app.log('verbose', `http request PATCH ${req.url}`)
|
|
202
|
-
context.
|
|
213
|
+
context.params.req = req
|
|
203
214
|
try {
|
|
204
215
|
const value = await service.__update(context, {
|
|
205
216
|
where: {
|
|
@@ -217,7 +228,7 @@ export function expressX(prisma, options = {}) {
|
|
|
217
228
|
|
|
218
229
|
app.delete(`${path}/:id`, async (req, res) => {
|
|
219
230
|
app.log('verbose', `http request DELETE ${req.url}`)
|
|
220
|
-
context.
|
|
231
|
+
context.params.req = req
|
|
221
232
|
try {
|
|
222
233
|
const value = await service.__delete(context, {
|
|
223
234
|
where: {
|
|
@@ -252,10 +263,13 @@ export function expressX(prisma, options = {}) {
|
|
|
252
263
|
id: lastConnectionId++,
|
|
253
264
|
socket,
|
|
254
265
|
channelNames: new Set(),
|
|
266
|
+
data: {
|
|
267
|
+
a: 123
|
|
268
|
+
},
|
|
255
269
|
}
|
|
256
270
|
// store connection in cache
|
|
257
|
-
connections[connection.id] = connection
|
|
258
|
-
app.log('verbose', `
|
|
271
|
+
app.connections[connection.id] = connection
|
|
272
|
+
app.log('verbose', `Connection ids: ${Object.keys(app.connections)}`)
|
|
259
273
|
|
|
260
274
|
// emit 'connection' event for app (expressjs extends EventEmitter)
|
|
261
275
|
app.emit('connection', connection)
|
|
@@ -265,7 +279,23 @@ export function expressX(prisma, options = {}) {
|
|
|
265
279
|
|
|
266
280
|
socket.on('disconnect', () => {
|
|
267
281
|
app.log('verbose', `Client disconnected ${connection.id}`)
|
|
268
|
-
|
|
282
|
+
|
|
283
|
+
// remove connection record after 1mn (leaves time in case of connection transfer)
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
app.log('verbose', `Delete connection ${connection.id}`)
|
|
286
|
+
delete app.connections[connection.id]
|
|
287
|
+
}, 60 * 1000)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
// handle connection data transfer caused by a disconnection/reconnection (page reload, network issue, etc.)
|
|
292
|
+
socket.on('cnx-transfer', async ({ from, to }) => {
|
|
293
|
+
app.log('verbose', `cnx-transfer from ${from} to ${to}`)
|
|
294
|
+
if (!app.connections[from]) return
|
|
295
|
+
app.connections[to] = app.connections[from]
|
|
296
|
+
app.connections[to].socket = socket
|
|
297
|
+
delete app.connections[from]
|
|
298
|
+
// app.printCnx('AFTER TRANSFER')
|
|
269
299
|
})
|
|
270
300
|
|
|
271
301
|
|
|
@@ -282,11 +312,13 @@ export function expressX(prisma, options = {}) {
|
|
|
282
312
|
if (serviceMethod) {
|
|
283
313
|
const context = {
|
|
284
314
|
app,
|
|
285
|
-
|
|
315
|
+
transport: 'ws',
|
|
316
|
+
params: { connection, name, action, args },
|
|
286
317
|
}
|
|
287
318
|
|
|
288
319
|
try {
|
|
289
320
|
const result = await serviceMethod(context, ...args)
|
|
321
|
+
app.log('verbose', `client-response ${uid} ${JSON.stringify(result)}`)
|
|
290
322
|
socket.emit('client-response', {
|
|
291
323
|
uid,
|
|
292
324
|
result,
|
|
@@ -294,7 +326,7 @@ export function expressX(prisma, options = {}) {
|
|
|
294
326
|
// pub/sub: send event on associated channels
|
|
295
327
|
publish(service, action, result)
|
|
296
328
|
} catch(err) {
|
|
297
|
-
app.log('error', err)
|
|
329
|
+
app.log('error', err.toString())
|
|
298
330
|
io.emit('client-response', {
|
|
299
331
|
uid,
|
|
300
332
|
error: err.toString(),
|
|
@@ -330,7 +362,7 @@ export function expressX(prisma, options = {}) {
|
|
|
330
362
|
app.log('verbose', `publish channels ${service.name} ${action} ${channelNames}`)
|
|
331
363
|
for (const channelName of channelNames) {
|
|
332
364
|
app.log('verbose', `service-event ${service.name} ${action} ${channelName}`)
|
|
333
|
-
const connectionList = Object.values(connections).filter(cnx => cnx.channelNames.has(channelName))
|
|
365
|
+
const connectionList = Object.values(app.connections).filter(cnx => cnx.channelNames.has(channelName))
|
|
334
366
|
for (const connection of connectionList) {
|
|
335
367
|
app.log('verbose', `emit to ${connection.id} ${service.name} ${action} ${result}`)
|
|
336
368
|
connection.socket.emit('service-event', {
|
package/test/index.test.js
CHANGED
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
import bodyParser from 'body-parser'
|
|
3
3
|
import axios from 'axios'
|
|
4
4
|
import io from 'socket.io-client'
|
|
5
|
+
import { PrismaClient } from '@prisma/client'
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
// import { assert } from 'chai'
|
|
8
8
|
import { describe, it, before, after, beforeEach, afterEach } from 'node:test'
|
|
9
9
|
import { strict as assert } from 'node:assert'
|
|
10
10
|
|
|
11
|
-
import { expressX
|
|
11
|
+
import { expressX } from '../src/index.mjs'
|
|
12
12
|
|
|
13
|
+
const prisma = new PrismaClient({
|
|
14
|
+
datasources: {
|
|
15
|
+
db: {
|
|
16
|
+
url: "file:./dev.db",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
13
20
|
|
|
14
21
|
// `app` is a regular express application, enhanced with services and real-time features
|
|
15
|
-
const app = expressX()
|
|
22
|
+
const app = expressX(prisma)
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
|
|
@@ -137,41 +144,3 @@ describe('HTTP/REST API', () => {
|
|
|
137
144
|
app.server.close()
|
|
138
145
|
})
|
|
139
146
|
})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// test compatibility with client API
|
|
143
|
-
describe('Client API', () => {
|
|
144
|
-
|
|
145
|
-
let clientApp, socket
|
|
146
|
-
|
|
147
|
-
before(async () => {
|
|
148
|
-
await new Promise((resolve) => {
|
|
149
|
-
app.server.listen(8008, () => {
|
|
150
|
-
console.log(`App listening at http://localhost:8008`)
|
|
151
|
-
resolve()
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
socket = io('http://localhost:8008', { transports: ["websocket"] })
|
|
156
|
-
clientApp = expressXClient(socket)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
after(async () => {
|
|
160
|
-
app.server.close()
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it("can create a user", async () => {
|
|
164
|
-
const user = await clientApp.service('User').create({
|
|
165
|
-
data: {
|
|
166
|
-
name: "chris",
|
|
167
|
-
email: 'chris@mail.fr'
|
|
168
|
-
},
|
|
169
|
-
})
|
|
170
|
-
assert(user.name === 'chris')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
after(async () => {
|
|
174
|
-
await socket.close()
|
|
175
|
-
await app.server.close()
|
|
176
|
-
})
|
|
177
|
-
})
|
package/src/client.mjs
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { v4 } from 'uuid'
|
|
3
|
-
|
|
4
|
-
export function expressXClient(socket, options={}) {
|
|
5
|
-
if (options.debug === undefined) options.debug = false
|
|
6
|
-
|
|
7
|
-
const waitingPromisesByUid = {}
|
|
8
|
-
const action2service2handlers = {}
|
|
9
|
-
let onConnectionCallback = null
|
|
10
|
-
let onDisconnectionCallback = null
|
|
11
|
-
|
|
12
|
-
const setConnectionCallback = (callback) => {
|
|
13
|
-
onConnectionCallback = callback
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const setDisconnectionCallback = (callback) => {
|
|
17
|
-
onDisconnectionCallback = callback
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// on connection
|
|
21
|
-
socket.on("connected", async (connectionId) => {
|
|
22
|
-
if (options.debug) console.log('connected', connectionId)
|
|
23
|
-
if (onConnectionCallback) onConnectionCallback(connectionId)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
// on receiving response from service request
|
|
27
|
-
socket.on('client-response', ({ uid, error, result }) => {
|
|
28
|
-
if (options.debug) console.log('client-response', uid, error, result)
|
|
29
|
-
if (!waitingPromisesByUid[uid]) return // may not exist because a timeout removed it
|
|
30
|
-
const [resolve, reject] = waitingPromisesByUid[uid]
|
|
31
|
-
if (error) {
|
|
32
|
-
reject(error)
|
|
33
|
-
} else {
|
|
34
|
-
resolve(result)
|
|
35
|
-
}
|
|
36
|
-
delete waitingPromisesByUid[uid]
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// on receiving events from pub/sub
|
|
40
|
-
socket.on('service-event', ({ name, action, result }) => {
|
|
41
|
-
if (!action2service2handlers[action]) action2service2handlers[action] = {}
|
|
42
|
-
const serviceHandlers = action2service2handlers[action]
|
|
43
|
-
const handler = serviceHandlers[name]
|
|
44
|
-
if (handler) handler(result)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
async function serviceMethodRequest(name, action, ...args) {
|
|
48
|
-
const uid = v4()
|
|
49
|
-
// create a promise which will resolve or reject by an event 'client-response'
|
|
50
|
-
const promise = new Promise((resolve, reject) => {
|
|
51
|
-
waitingPromisesByUid[uid] = [resolve, reject]
|
|
52
|
-
// a 5s timeout may also reject the promise
|
|
53
|
-
setTimeout(() => {
|
|
54
|
-
delete waitingPromisesByUid[uid]
|
|
55
|
-
reject(`Error: timeout on service '${name}', action '${action}', args: ${args}`)
|
|
56
|
-
}, 5000)
|
|
57
|
-
})
|
|
58
|
-
// send request to server through websocket
|
|
59
|
-
socket.emit('client-request', {
|
|
60
|
-
uid,
|
|
61
|
-
name,
|
|
62
|
-
action,
|
|
63
|
-
args,
|
|
64
|
-
})
|
|
65
|
-
return promise
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function service(name) {
|
|
69
|
-
const service = {
|
|
70
|
-
// associate a handler to a pub/sub event for this service
|
|
71
|
-
on: (action, handler) => {
|
|
72
|
-
if (!action2service2handlers[action]) action2service2handlers[action] = {}
|
|
73
|
-
const serviceHandlers = action2service2handlers[action]
|
|
74
|
-
serviceHandlers[name] = handler
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
// use a Proxy to allow for any method name for a service
|
|
78
|
-
const handler = {
|
|
79
|
-
get(service, action) {
|
|
80
|
-
if (!(action in service)) {
|
|
81
|
-
// newly used property `action`: define it as a service method request function
|
|
82
|
-
service[action] = (...args) => serviceMethodRequest(name, action, ...args)
|
|
83
|
-
}
|
|
84
|
-
return service[action]
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return new Proxy(service, handler)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
setConnectionCallback,
|
|
92
|
-
setDisconnectionCallback,
|
|
93
|
-
service,
|
|
94
|
-
}
|
|
95
|
-
}
|