@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x",
3
- "version": "1.3.7",
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
@@ -1,11 +1,9 @@
1
1
 
2
2
  import { expressX } from './server.mjs'
3
- import { expressXClient } from './client.mjs'
4
3
  import { hashPassword, protect, setSessionJWT, } from './common-hooks.mjs'
5
4
 
6
5
  export {
7
6
  expressX,
8
- expressXClient,
9
7
 
10
8
  hashPassword,
11
9
  protect,
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({ ...context, args })
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
- app.log('debug', 'result ' + JSON.stringify(result))
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({ ...context, result })
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
- http: { name: service.name }
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.http.req = req
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.http.req = req
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.http.req = req
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.http.req = req
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.http.req = req
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', `active connections ${Object.keys(connections)}`)
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
- delete connections[connection.id]
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
- ws: { connection, name, action, args },
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', {
@@ -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, expressXClient } from '../src/index.mjs'
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
- }