@jcbuisson/express-x 1.8.4 → 2.0.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/README.md +15 -43
- package/package.json +1 -2
- package/src/common-hooks.mjs +11 -6
- package/src/index.mjs +2 -11
- package/src/server.mjs +147 -375
- package/src/context.mjs +0 -62
package/README.md
CHANGED
|
@@ -33,23 +33,17 @@ backed in a [Prisma](https://www.prisma.io/) database
|
|
|
33
33
|
|
|
34
34
|
```js
|
|
35
35
|
// app.js
|
|
36
|
-
import bodyParser from 'body-parser'
|
|
37
36
|
import { expressXServer } from '@jcbuisson/express-x'
|
|
38
37
|
|
|
39
38
|
// `app` is a regular express application, enhanced with service and real-time features
|
|
40
39
|
const app = expressX()
|
|
41
40
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
app.createDatabaseService('Post')
|
|
45
|
-
|
|
46
|
-
// add body parsers for http requests
|
|
47
|
-
app.use(bodyParser.json())
|
|
48
|
-
app.use(bodyParser.urlencoded({ extended: false }))
|
|
41
|
+
// configure prisma client from schema
|
|
42
|
+
const prisma = new PrismaClient()
|
|
49
43
|
|
|
50
|
-
//
|
|
51
|
-
app.
|
|
52
|
-
app.
|
|
44
|
+
// create two CRUD database services. They provide Prisma methods: `create`, 'createMany', 'find', 'findMany', 'upsert', etc.
|
|
45
|
+
app.createService('User', prisma.User)
|
|
46
|
+
app.createService('Post', prisma.Post)
|
|
53
47
|
|
|
54
48
|
app.server.listen(8000, () => console.log(`App listening at http://localhost:8000`))
|
|
55
49
|
```
|
|
@@ -88,40 +82,18 @@ datasource db {
|
|
|
88
82
|
|
|
89
83
|
Then create the database:
|
|
90
84
|
```bash
|
|
91
|
-
npx prisma
|
|
85
|
+
npx prisma db push
|
|
92
86
|
```
|
|
93
87
|
|
|
94
88
|
The sqlite database file is created at `prisma/dev.db`
|
|
95
89
|
|
|
96
90
|
|
|
97
|
-
## Run the application
|
|
91
|
+
## Run the application on the server side
|
|
98
92
|
|
|
99
93
|
```bash
|
|
100
94
|
node app.js
|
|
101
95
|
```
|
|
102
96
|
|
|
103
|
-
It prints the following lines in the console:
|
|
104
|
-
```bash
|
|
105
|
-
created service 'user' over entity 'User'
|
|
106
|
-
created service 'post' over entity 'Post'
|
|
107
|
-
added HTTP endpoints for service 'user' at path '/api/user'
|
|
108
|
-
added HTTP endpoints for service 'post' at path '/api/post'
|
|
109
|
-
App listening at http://localhost:8000
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
## Enjoy your HTTP REST API
|
|
114
|
-
|
|
115
|
-
Now you can try the HTTP endpoints `/api/user` and `/api/post`; open a new console and run HTTP requests:
|
|
116
|
-
```bash
|
|
117
|
-
curl -X POST -H 'Content-Type: application/json' -d '{"name":"JC"}' http://localhost:8000/api/user
|
|
118
|
-
# --> {"id":1,"name":"JC"}
|
|
119
|
-
curl http://localhost:8000/api/user
|
|
120
|
-
# --> [{"id":1,"name":"JC"}]
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
With a few lines of code, we got a complete REST API over the database tables. But there is more!
|
|
124
|
-
|
|
125
97
|
|
|
126
98
|
## Use it with a websocket client
|
|
127
99
|
|
|
@@ -162,9 +134,9 @@ async function main() {
|
|
|
162
134
|
main()
|
|
163
135
|
```
|
|
164
136
|
|
|
165
|
-
For simplicity we use a node client, but you
|
|
137
|
+
For simplicity we use a node client, but you would write something similar with your favorite front-end framework.
|
|
166
138
|
|
|
167
|
-
You can use
|
|
139
|
+
You can use the exact same statements on services on the client side as you would on the server side, such as: `app.service('User').create(...)`.
|
|
168
140
|
Of course the `app` object here on the client is quite different that the `app` object on the server; you can find explanations [here]().
|
|
169
141
|
|
|
170
142
|
Now run the client script:
|
|
@@ -218,11 +190,11 @@ import { PrismaClient } from '@prisma/client'
|
|
|
218
190
|
const app = expressX()
|
|
219
191
|
|
|
220
192
|
// configure prisma client from schema
|
|
221
|
-
|
|
193
|
+
const prisma = new PrismaClient()
|
|
222
194
|
|
|
223
195
|
// create two CRUD database services. They provide Prisma methods: `create`, 'createMany', 'find', 'findMany', 'upsert', etc.
|
|
224
|
-
app.
|
|
225
|
-
app.
|
|
196
|
+
app.createService('User', prisma.User)
|
|
197
|
+
app.createService('Post', prisma.Post)
|
|
226
198
|
|
|
227
199
|
// publish
|
|
228
200
|
app.service('User').publish(async (post, context) => {
|
|
@@ -233,9 +205,9 @@ app.service('Post').publish(async (post, context) => {
|
|
|
233
205
|
})
|
|
234
206
|
|
|
235
207
|
// subscribe
|
|
236
|
-
app.on('connection', (
|
|
237
|
-
console.log('connection',
|
|
238
|
-
app.joinChannel('anonymous',
|
|
208
|
+
app.on('connection', (socket) => {
|
|
209
|
+
console.log('connection', socket.id)
|
|
210
|
+
app.joinChannel('anonymous', socket)
|
|
239
211
|
})
|
|
240
212
|
|
|
241
213
|
app.server.listen(8000, () => console.log(`App listening at http://localhost:8000`))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jcbuisson/express-x",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
},
|
|
18
18
|
"keywords": [],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@prisma/client": "^4.10.1",
|
|
21
20
|
"bcryptjs": "^2.4.3",
|
|
22
21
|
"config": "^3.3.9",
|
|
23
22
|
"express": "^4.18.2",
|
package/src/common-hooks.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
import bcrypt from 'bcryptjs'
|
|
3
3
|
|
|
4
|
-
import { getConnectionDataItem, resetConnection } from './context.mjs'
|
|
5
4
|
|
|
5
|
+
export const addTimestamp = (field) => async (context) => {
|
|
6
|
+
context.result[field] = (new Date()).toISOString()
|
|
7
|
+
return context
|
|
8
|
+
}
|
|
6
9
|
|
|
7
10
|
/*
|
|
8
11
|
* Hash password of user record
|
|
@@ -33,20 +36,22 @@ export function protect(field) {
|
|
|
33
36
|
|
|
34
37
|
/*
|
|
35
38
|
* Does nothing for calls which are not client-side with websocket transport
|
|
36
|
-
* Check if the 'expireAt' key in
|
|
37
|
-
* If it is met, throw an error (which will be sent back to the calling server or client) and reset
|
|
39
|
+
* Check if the 'expireAt' key in socket.data is met
|
|
40
|
+
* If it is met, throw an error (which will be sent back to the calling server or client) and reset socket.data
|
|
38
41
|
* If not, do nothing. If needed, an application-level hook may automatically extend the expiration data at each service call
|
|
39
42
|
*/
|
|
40
43
|
export const isNotExpired = async (context) => {
|
|
44
|
+
// return context
|
|
41
45
|
if (context.caller !== 'client' || context.transport !== 'ws') return
|
|
42
46
|
|
|
43
|
-
const expireAt =
|
|
47
|
+
const expireAt = context.socket.data.expireAt
|
|
44
48
|
if (expireAt) {
|
|
45
49
|
const expireAtDate = new Date(expireAt)
|
|
46
50
|
const now = new Date()
|
|
47
51
|
if (now > expireAtDate) {
|
|
48
|
-
// expiration date is met: clear
|
|
49
|
-
|
|
52
|
+
// expiration date is met: clear socket.data & throw exception
|
|
53
|
+
const { clientIP } = socket.data
|
|
54
|
+
socket.data = { clientIP }
|
|
50
55
|
throw new Error('session-expired')
|
|
51
56
|
}
|
|
52
57
|
} else {
|
package/src/index.mjs
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
import { expressX } from './server.mjs'
|
|
3
|
-
import { hashPassword, protect, isNotExpired } from './common-hooks.mjs'
|
|
4
|
-
import { getContextConnection, resetConnection, getConnectionDataItem, setConnectionDataItem, removeConnectionDataItem, sendServiceEventToClient } from './context.mjs'
|
|
3
|
+
import { addTimestamp, hashPassword, protect, isNotExpired } from './common-hooks.mjs'
|
|
5
4
|
|
|
6
5
|
export {
|
|
7
6
|
expressX,
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
resetConnection,
|
|
11
|
-
|
|
12
|
-
getConnectionDataItem,
|
|
13
|
-
setConnectionDataItem,
|
|
14
|
-
removeConnectionDataItem,
|
|
15
|
-
|
|
16
|
-
sendServiceEventToClient,
|
|
17
|
-
|
|
8
|
+
addTimestamp,
|
|
18
9
|
hashPassword,
|
|
19
10
|
protect,
|
|
20
11
|
isNotExpired,
|
package/src/server.mjs
CHANGED
|
@@ -1,97 +1,146 @@
|
|
|
1
|
+
import express from "express"
|
|
2
|
+
import { createServer } from "http"
|
|
3
|
+
import { Server } from "socket.io"
|
|
1
4
|
|
|
2
|
-
import http from 'http'
|
|
3
|
-
import { Server } from 'socket.io'
|
|
4
|
-
import express from 'express'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
* Enhance `app` express application with services and real-time features
|
|
8
|
-
*/
|
|
6
|
+
export function expressX(config) {
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function expressX(prisma, config) {
|
|
8
|
+
const services = {}
|
|
9
|
+
let appHooks = []
|
|
13
10
|
|
|
14
11
|
const app = express()
|
|
12
|
+
const httpServer = createServer(app)
|
|
15
13
|
|
|
16
14
|
// so that config can be accessed anywhere with app.get('config')
|
|
17
15
|
app.set('config', config)
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
const io = new Server(httpServer, {
|
|
18
|
+
path: config?.WS_PATH || '/socket.io/',
|
|
19
|
+
connectionStateRecovery: {
|
|
20
|
+
// the backup duration of the sessions and the packets
|
|
21
|
+
maxDisconnectionDuration: 2 * 60 * 1000,
|
|
22
|
+
// whether to skip middlewares upon successful recovery
|
|
23
|
+
skipMiddlewares: true,
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
return connection
|
|
27
|
+
// logging function - a winston logger must be configured first
|
|
28
|
+
app.log = (severity, message) => {
|
|
29
|
+
const logger = app.get('logger')
|
|
30
|
+
if (logger) logger.log(severity, message); else console.log(`[${severity}]`, message)
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
io.on('connection', async function(socket) {
|
|
34
|
+
if (socket.recovered) {
|
|
35
|
+
// recovery was successful: socket.id, socket.rooms and socket.data were restored
|
|
36
|
+
console.log('reconnection!!!', socket.id)
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
clientIP: connection.clientIP,
|
|
45
|
-
channelNames: connection.channelNames,
|
|
46
|
-
data: connection.data,
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
}
|
|
38
|
+
} else {
|
|
39
|
+
// new or unrecoverable session
|
|
40
|
+
console.log("connection", socket.id)
|
|
41
|
+
}
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} catch(err) {
|
|
55
|
-
// in case it would no longer exist
|
|
43
|
+
const clientIP = socket.handshake.address
|
|
44
|
+
socket.data = {
|
|
45
|
+
clientIP,
|
|
56
46
|
}
|
|
57
|
-
|
|
47
|
+
// app.log('verbose', `Client connected ${connectionId} from IP ${clientIP}`)
|
|
48
|
+
app.log('verbose', `Client connected ${socket.id} from IP ${clientIP}`)
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
50
|
+
// emit 'connection' event for app (expressjs extends EventEmitter)
|
|
51
|
+
app.emit('connection', socket)
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
// send 'connected' event to client
|
|
54
|
+
// socket.emit('connected', connectionId)
|
|
55
|
+
socket.emit('connected', socket.id)
|
|
66
56
|
|
|
57
|
+
socket.on('disconnect', () => {
|
|
58
|
+
// app.log('verbose', `Client disconnected ${connectionId}`)
|
|
59
|
+
app.log('verbose', `Client disconnected ${socket.id}`)
|
|
60
|
+
})
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
/*
|
|
63
|
+
* Handle websocket client request
|
|
64
|
+
* Emit in return a 'client-response' message
|
|
65
|
+
*/
|
|
66
|
+
socket.on('client-request', async ({ uid, name, action, args }) => {
|
|
67
|
+
const trimmedArgs = args ? JSON.stringify(args).slice(0, 300) : ''
|
|
68
|
+
app.log('verbose', `client-request ${uid} ${name} ${action} ${trimmedArgs}`)
|
|
69
|
+
if (name in services) {
|
|
70
|
+
const service = services[name]
|
|
71
|
+
try {
|
|
72
|
+
const serviceMethod = service['__' + action]
|
|
73
|
+
if (serviceMethod) {
|
|
74
|
+
const context = {
|
|
75
|
+
app,
|
|
76
|
+
caller: 'client',
|
|
77
|
+
transport: 'ws',
|
|
78
|
+
socket,
|
|
79
|
+
// connectionId,
|
|
80
|
+
serviceName: name,
|
|
81
|
+
methodName: action,
|
|
82
|
+
args,
|
|
83
|
+
}
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
try {
|
|
86
|
+
// call method with context
|
|
87
|
+
const result = await serviceMethod(context, ...args)
|
|
88
|
+
|
|
89
|
+
const trimmedResult = result ? JSON.stringify(result).slice(0, 300) : ''
|
|
90
|
+
app.log('verbose', `client-response ${uid} ${trimmedResult}`)
|
|
91
|
+
socket.emit('client-response', {
|
|
92
|
+
uid,
|
|
93
|
+
result,
|
|
94
|
+
})
|
|
95
|
+
} catch(err) {
|
|
96
|
+
console.log('!!!!!!error', err.code, err)
|
|
97
|
+
app.log('verbose', err.stack)
|
|
98
|
+
socket.emit('client-response', {
|
|
99
|
+
uid,
|
|
100
|
+
error: {
|
|
101
|
+
code: err.code || 'unknown-error',
|
|
102
|
+
message: err.stack,
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
socket.emit('client-response', {
|
|
108
|
+
uid,
|
|
109
|
+
error: {
|
|
110
|
+
code: 'missing-method',
|
|
111
|
+
message: `there is no method named '${action}' for service '${name}'`,
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
} catch(err) {
|
|
116
|
+
console.log('err', err)
|
|
117
|
+
app.log('verbose', err.stack)
|
|
118
|
+
socket.emit('client-response', {
|
|
119
|
+
uid,
|
|
120
|
+
error: {
|
|
121
|
+
code: err.code || 'unknown-error',
|
|
122
|
+
message: err.stack,
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
socket.emit('client-response', {
|
|
128
|
+
uid,
|
|
129
|
+
error: {
|
|
130
|
+
code: 'missing-service',
|
|
131
|
+
message: `there is no service named '${name}'`,
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
})
|
|
89
138
|
|
|
90
139
|
/*
|
|
91
140
|
* create a service `name` with given `methods`
|
|
92
141
|
*/
|
|
93
142
|
function createService(name, methods) {
|
|
94
|
-
const service = {
|
|
143
|
+
const service = { name }
|
|
95
144
|
|
|
96
145
|
for (const methodName in methods) {
|
|
97
146
|
const method = methods[methodName]
|
|
@@ -115,7 +164,7 @@ export function expressX(prisma, config) {
|
|
|
115
164
|
}
|
|
116
165
|
|
|
117
166
|
// call method
|
|
118
|
-
const result = await method(...
|
|
167
|
+
const result = await method(...args)
|
|
119
168
|
// put result into context
|
|
120
169
|
context.result = result
|
|
121
170
|
|
|
@@ -127,29 +176,22 @@ export function expressX(prisma, config) {
|
|
|
127
176
|
await hook(context)
|
|
128
177
|
}
|
|
129
178
|
|
|
130
|
-
// publish event
|
|
131
|
-
if (
|
|
179
|
+
// publish 'service-event' event associated to service/method
|
|
180
|
+
if (service.publishFunction) {
|
|
181
|
+
// collect channel names to socket is member of
|
|
132
182
|
const channelNames = await service.publishFunction(context)
|
|
133
|
-
app.log('verbose', `publish channels ${service.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return channelNames.includes(channelName)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
for (const connection of connectionList) {
|
|
143
|
-
const trimmedResult = result ? JSON.stringify(result).slice(0, 300) : ''
|
|
144
|
-
app.log('verbose', `emit to ${connection.id} ${service._name} ${methodName} ${trimmedResult}`)
|
|
145
|
-
const socket = getSocket(connection.id)
|
|
146
|
-
// emit service event
|
|
147
|
-
socket && socket.emit('service-event', {
|
|
148
|
-
name: service._name,
|
|
149
|
-
action: methodName,
|
|
150
|
-
result,
|
|
151
|
-
})
|
|
183
|
+
app.log('verbose', `publish channels ${service.name} ${methodName} ${channelNames}`)
|
|
184
|
+
// send event on all these channels
|
|
185
|
+
if (channelNames.length > 0) {
|
|
186
|
+
let sender = io.to(channelNames[0])
|
|
187
|
+
for (let i = 1; i < channelNames.length; i++) {
|
|
188
|
+
sender = sender.to(channelNames[i])
|
|
152
189
|
}
|
|
190
|
+
sender.emit('service-event', {
|
|
191
|
+
name: service.name,
|
|
192
|
+
action: methodName,
|
|
193
|
+
result,
|
|
194
|
+
})
|
|
153
195
|
}
|
|
154
196
|
}
|
|
155
197
|
|
|
@@ -185,13 +227,6 @@ export function expressX(prisma, config) {
|
|
|
185
227
|
return service
|
|
186
228
|
}
|
|
187
229
|
|
|
188
|
-
// `app.service(name)` starts here!
|
|
189
|
-
function service(name) {
|
|
190
|
-
// get service from `services` cache
|
|
191
|
-
if (name in services) return services[name]
|
|
192
|
-
app.log('error', `there is no service named '${name}'`, 'missing-service')
|
|
193
|
-
}
|
|
194
|
-
|
|
195
230
|
function configure(callback) {
|
|
196
231
|
callback(app)
|
|
197
232
|
}
|
|
@@ -201,296 +236,33 @@ export function expressX(prisma, config) {
|
|
|
201
236
|
appHooks = hooks
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
app,
|
|
210
|
-
caller: 'client',
|
|
211
|
-
transport: 'http',
|
|
212
|
-
serviceName: service._name,
|
|
213
|
-
|
|
214
|
-
// params: { name: service._name }
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// introspect schema and return a map: field name => prisma type
|
|
218
|
-
function getTypesMap() {
|
|
219
|
-
const dmmf = service.prisma._runtimeDataModel
|
|
220
|
-
const fieldDescriptions = dmmf.models[service._name].fields
|
|
221
|
-
return fieldDescriptions.reduce((accu, descr) => {
|
|
222
|
-
accu[descr.name] = descr.type
|
|
223
|
-
return accu
|
|
224
|
-
}, {})
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
app.post(path, async (req, res) => {
|
|
229
|
-
app.log('verbose', `http request POST ${req.url}`)
|
|
230
|
-
context.req = req
|
|
231
|
-
try {
|
|
232
|
-
const value = await service.__create(context, { data: req.body })
|
|
233
|
-
res.json(value)
|
|
234
|
-
} catch(err) {
|
|
235
|
-
app.log('error', err)
|
|
236
|
-
res.status(500).send(err.toString())
|
|
237
|
-
}
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
app.get(path, async (req, res) => {
|
|
241
|
-
app.log('verbose', `http request GET ${req.url}`)
|
|
242
|
-
context.req = req
|
|
243
|
-
const query = { ...req.query }
|
|
244
|
-
try {
|
|
245
|
-
// the values in `req.query` are all strings, but Prisma need proper types
|
|
246
|
-
// we need to introspect column types and do the proper transtyping
|
|
247
|
-
for (const fieldName in query) {
|
|
248
|
-
const typesDict = getTypesMap()
|
|
249
|
-
const fieldType = typesDict[fieldName]
|
|
250
|
-
|
|
251
|
-
if (fieldType === 'Int') {
|
|
252
|
-
query[fieldName] = parseInt(query[fieldName])
|
|
253
|
-
} else if (fieldType === 'Float') {
|
|
254
|
-
query[fieldName] = parseFloat(query[fieldName])
|
|
255
|
-
} else if (fieldType === 'Boolean') {
|
|
256
|
-
query[fieldName] = (query[fieldName] === 't') ? true : false
|
|
257
|
-
} else if (fieldType === 'String') {
|
|
258
|
-
query[fieldName] = query[fieldName]
|
|
259
|
-
} else {
|
|
260
|
-
// ?
|
|
261
|
-
query[fieldName] = query[fieldName]
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// call __findMany
|
|
265
|
-
const values = await service.__findMany(context, {
|
|
266
|
-
where: query,
|
|
267
|
-
})
|
|
268
|
-
res.json(values)
|
|
269
|
-
} catch(err) {
|
|
270
|
-
app.log('error', err)
|
|
271
|
-
res.status(500).send(err.toString())
|
|
272
|
-
}
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
app.get(`${path}/:id`, async (req, res) => {
|
|
276
|
-
app.log('verbose', `http request GET ${req.url}`)
|
|
277
|
-
context.req = req
|
|
278
|
-
try {
|
|
279
|
-
const value = await service.__findUnique(context, {
|
|
280
|
-
where: {
|
|
281
|
-
id: parseInt(req.params.id)
|
|
282
|
-
}
|
|
283
|
-
})
|
|
284
|
-
res.json(value)
|
|
285
|
-
} catch(err) {
|
|
286
|
-
app.log('error', err)
|
|
287
|
-
res.status(500).send(err.toString())
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
app.patch(`${path}/:id`, async (req, res) => {
|
|
292
|
-
app.log('verbose', `http request PATCH ${req.url}`)
|
|
293
|
-
context.req = req
|
|
294
|
-
try {
|
|
295
|
-
const value = await service.__update(context, {
|
|
296
|
-
where: {
|
|
297
|
-
id: parseInt(req.params.id),
|
|
298
|
-
},
|
|
299
|
-
data: req.body,
|
|
300
|
-
})
|
|
301
|
-
res.json(value)
|
|
302
|
-
} catch(err) {
|
|
303
|
-
app.log('error', err)
|
|
304
|
-
res.status(500).send(err.toString())
|
|
305
|
-
}
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
app.delete(`${path}/:id`, async (req, res) => {
|
|
309
|
-
app.log('verbose', `http request DELETE ${req.url}`)
|
|
310
|
-
context.req = req
|
|
311
|
-
try {
|
|
312
|
-
const value = await service.__delete(context, {
|
|
313
|
-
where: {
|
|
314
|
-
id: parseInt(req.params.id)
|
|
315
|
-
}
|
|
316
|
-
})
|
|
317
|
-
res.json(value)
|
|
318
|
-
} catch(err) {
|
|
319
|
-
app.log('error', err)
|
|
320
|
-
res.status(500).send(err.toString())
|
|
321
|
-
}
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
app.log('info', `added HTTP endpoints for service '${service._name}' at path '${path}'`)
|
|
239
|
+
// `app.service(name)` starts here!
|
|
240
|
+
function service(name) {
|
|
241
|
+
// get service from `services` cache
|
|
242
|
+
if (name in services) return services[name]
|
|
243
|
+
app.log('error', `there is no service named '${name}'`, 'missing-service')
|
|
325
244
|
}
|
|
326
245
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const httpServer = new http.Server(app)
|
|
331
|
-
|
|
332
|
-
if (config.WS_TRANSPORT) {
|
|
333
|
-
/*
|
|
334
|
-
* Add websocket transport
|
|
335
|
-
*/
|
|
336
|
-
const io = new Server(httpServer, {
|
|
337
|
-
path: config.WS_PATH || '/socket.io/',
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
io.on('connection', async function(socket) {
|
|
342
|
-
const clientIP = socket.request?.connection?.remoteAddress || 'unknown'
|
|
343
|
-
const connection = await createConnection(clientIP)
|
|
344
|
-
app.log('verbose', `Client connected ${connection.id} from IP ${clientIP}`)
|
|
345
|
-
|
|
346
|
-
setSocket(connection.id, socket)
|
|
347
|
-
|
|
348
|
-
// emit 'connection' event for app (expressjs extends EventEmitter)
|
|
349
|
-
app.emit('connection', connection)
|
|
350
|
-
|
|
351
|
-
// send 'connected' event to client
|
|
352
|
-
socket.emit('connected', connection.id)
|
|
353
|
-
|
|
354
|
-
socket.on('disconnect', () => {
|
|
355
|
-
app.log('verbose', `Client disconnected ${connection.id}`)
|
|
356
|
-
|
|
357
|
-
// remove connection record after expiration delay, if it still exists
|
|
358
|
-
setTimeout(async () => {
|
|
359
|
-
const connectionId = connection.id
|
|
360
|
-
// check if connection still exists
|
|
361
|
-
const cnx = await getConnection(connectionId)
|
|
362
|
-
if (cnx) {
|
|
363
|
-
app.log('verbose', `Delete connection ${connectionId}`)
|
|
364
|
-
await deleteConnection(connectionId)
|
|
365
|
-
}
|
|
366
|
-
}, config.SESSION_EXPIRE_DELAY || 24*60*60000)
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// handle connection data transfer caused by a disconnection/reconnection (page reload, network issue, etc.)
|
|
371
|
-
socket.on('cnx-transfer', async ({ from, to }) => {
|
|
372
|
-
app.log('verbose', `cnx-transfer from ${from} to ${to}`)
|
|
373
|
-
// copy connection data from 'from' to 'to'
|
|
374
|
-
const fromConnection = await getConnection(from)
|
|
375
|
-
if (!fromConnection) return
|
|
376
|
-
const toConnection = await cloneConnection(to, fromConnection)
|
|
377
|
-
// associate socket to 'to'
|
|
378
|
-
setSocket(to, socket)
|
|
379
|
-
// delete 'from'
|
|
380
|
-
await deleteConnection(from)
|
|
381
|
-
// send acknowledge to client
|
|
382
|
-
socket.emit('cnx-transfer-ack', toConnection)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
/*
|
|
386
|
-
* Handle websocket client request
|
|
387
|
-
* Emit in return a 'client-response' message
|
|
388
|
-
*/
|
|
389
|
-
socket.on('client-request', async ({ uid, name, action, args }) => {
|
|
390
|
-
const trimmedArgs = args ? JSON.stringify(args).slice(0, 300) : ''
|
|
391
|
-
app.log('verbose', `client-request ${connection.id} ${uid} ${name} ${action} ${trimmedArgs}`)
|
|
392
|
-
if (name in services) {
|
|
393
|
-
const service = services[name]
|
|
394
|
-
try {
|
|
395
|
-
const serviceMethod = service['__' + action]
|
|
396
|
-
if (serviceMethod) {
|
|
397
|
-
const context = {
|
|
398
|
-
app,
|
|
399
|
-
caller: 'client',
|
|
400
|
-
transport: 'ws',
|
|
401
|
-
connectionId: connection.id,
|
|
402
|
-
serviceName: name,
|
|
403
|
-
methodName: action,
|
|
404
|
-
args,
|
|
405
|
-
|
|
406
|
-
// params: { connectionId: connection.id, name, action, args },
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
const result = await serviceMethod(context, ...args)
|
|
411
|
-
|
|
412
|
-
const trimmedResult = result ? JSON.stringify(result).slice(0, 300) : ''
|
|
413
|
-
app.log('verbose', `client-response ${connection.id} ${uid} ${trimmedResult}`)
|
|
414
|
-
socket.emit('client-response', {
|
|
415
|
-
uid,
|
|
416
|
-
result,
|
|
417
|
-
})
|
|
418
|
-
} catch(err) {
|
|
419
|
-
console.log('!!!!!!error', err.code, err)
|
|
420
|
-
app.log('verbose', err.stack)
|
|
421
|
-
socket.emit('client-response', {
|
|
422
|
-
uid,
|
|
423
|
-
error: {
|
|
424
|
-
code: err.code || 'unknown-error',
|
|
425
|
-
message: err.stack,
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
|
-
} else {
|
|
430
|
-
socket.emit('client-response', {
|
|
431
|
-
uid,
|
|
432
|
-
error: {
|
|
433
|
-
code: 'missing-method',
|
|
434
|
-
message: `there is no method named '${action}' for service '${name}'`,
|
|
435
|
-
}
|
|
436
|
-
})
|
|
437
|
-
}
|
|
438
|
-
} catch(error) {
|
|
439
|
-
console.log('err', err)
|
|
440
|
-
app.log('verbose', error.stack)
|
|
441
|
-
socket.emit('client-response', {
|
|
442
|
-
uid,
|
|
443
|
-
error: {
|
|
444
|
-
code: err.code || 'unknown-error',
|
|
445
|
-
message: error.stack,
|
|
446
|
-
}
|
|
447
|
-
})
|
|
448
|
-
}
|
|
449
|
-
} else {
|
|
450
|
-
socket.emit('client-response', {
|
|
451
|
-
uid,
|
|
452
|
-
error: {
|
|
453
|
-
code: 'missing-service',
|
|
454
|
-
message: `there is no service named '${name}'`,
|
|
455
|
-
}
|
|
456
|
-
})
|
|
457
|
-
}
|
|
458
|
-
})
|
|
459
|
-
})
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
async function joinChannel(channelName, connection) {
|
|
463
|
-
app.log('verbose', `Joining channel ${channelName} ${connection.id}`)
|
|
464
|
-
const channelNames = JSON.parse(connection.channelNames)
|
|
465
|
-
if (!channelNames.includes(channelName)) channelNames.push(channelName)
|
|
466
|
-
await app.prisma.Connection.update({
|
|
467
|
-
where: { id: connection.id },
|
|
468
|
-
data: { channelNames: JSON.stringify(channelNames) },
|
|
469
|
-
})
|
|
246
|
+
async function joinChannel(channelName, socket) {
|
|
247
|
+
app.log('verbose', `joining ${channelName}, ${JSON.stringify(socket.data)}`)
|
|
248
|
+
socket.join(channelName)
|
|
470
249
|
}
|
|
471
250
|
|
|
472
|
-
async function leaveChannel(channelName,
|
|
473
|
-
app.log('verbose', `
|
|
474
|
-
|
|
475
|
-
await app.prisma.Connection.update({
|
|
476
|
-
where: { id },
|
|
477
|
-
data: { channelNames: JSON.stringify(channelNames) },
|
|
478
|
-
})
|
|
251
|
+
async function leaveChannel(channelName, socket) {
|
|
252
|
+
app.log('verbose', `leaving ${channelName}, ${JSON.stringify(socket.data)}`)
|
|
253
|
+
socket.leave(channelName)
|
|
479
254
|
}
|
|
480
255
|
|
|
481
|
-
|
|
482
256
|
// enhance `app` with objects and methods
|
|
483
257
|
return Object.assign(app, {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
createDatabaseService,
|
|
258
|
+
io,
|
|
259
|
+
httpServer,
|
|
487
260
|
createService,
|
|
488
261
|
service,
|
|
489
262
|
configure,
|
|
490
263
|
hooks,
|
|
491
|
-
addHttpRest,
|
|
492
|
-
httpServer,
|
|
493
264
|
joinChannel,
|
|
494
265
|
leaveChannel,
|
|
495
266
|
})
|
|
267
|
+
|
|
496
268
|
}
|
package/src/context.mjs
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export async function getContextConnection(context) {
|
|
3
|
-
const id = context.connectionId
|
|
4
|
-
const connection = await context.app.prisma.Connection.findUnique({ where: { id }})
|
|
5
|
-
return connection
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function resetConnection(context) {
|
|
9
|
-
const id = context.connectionId
|
|
10
|
-
// by using updateMany, we cover the case where the connection `id` no longer exists
|
|
11
|
-
await context.app.prisma.Connection.updateMany({
|
|
12
|
-
where: { id },
|
|
13
|
-
data: {
|
|
14
|
-
data: '{}',
|
|
15
|
-
channelNames: '[]',
|
|
16
|
-
}
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function getConnectionDataItem(context, key) {
|
|
21
|
-
const id = context.connectionId
|
|
22
|
-
const connection = await context.app.prisma.Connection.findUnique({ where: { id }})
|
|
23
|
-
const data = JSON.parse(connection.data)
|
|
24
|
-
return data[key]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function setConnectionDataItem(context, key, value) {
|
|
28
|
-
const id = context.connectionId
|
|
29
|
-
const connection = await context.app.prisma.Connection.findUnique({ where: { id }})
|
|
30
|
-
const data = JSON.parse(connection.data)
|
|
31
|
-
data[key] = value
|
|
32
|
-
await context.app.prisma.Connection.update({
|
|
33
|
-
where: { id },
|
|
34
|
-
data: {
|
|
35
|
-
data: JSON.stringify(data)
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function removeConnectionDataItem(context, key) {
|
|
41
|
-
const id = context.connectionId
|
|
42
|
-
const connection = await context.app.prisma.Connection.findUnique({ where: { id }})
|
|
43
|
-
const data = JSON.parse(connection.data)
|
|
44
|
-
delete data[key]
|
|
45
|
-
await context.app.prisma.Connection.update({
|
|
46
|
-
where: { id },
|
|
47
|
-
data: {
|
|
48
|
-
data: JSON.stringify(data)
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function sendServiceEventToClient(context, name, action, result) {
|
|
54
|
-
const id = context.connectionId
|
|
55
|
-
const socket = context.app.cnx2Socket[id]
|
|
56
|
-
socket.emit('service-event', {
|
|
57
|
-
name,
|
|
58
|
-
action,
|
|
59
|
-
result,
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
}
|