@jcbuisson/express-x 1.1.4 → 1.2.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 +9 -18
- package/package.json +2 -1
- package/prisma/dev.db +0 -0
- package/src/client.mjs +95 -0
- package/src/common-hooks.mjs +53 -0
- package/src/index.mjs +9 -377
- package/src/server.mjs +381 -0
- package/test/index.test.js +7 -8
package/README.md
CHANGED
|
@@ -33,12 +33,11 @@ backed in a [Prisma](https://www.prisma.io/) database
|
|
|
33
33
|
|
|
34
34
|
```js
|
|
35
35
|
// app.js
|
|
36
|
-
import express from 'express'
|
|
37
36
|
import bodyParser from 'body-parser'
|
|
38
|
-
import
|
|
37
|
+
import { expressXServer } from '@jcbuisson/express-x'
|
|
39
38
|
|
|
40
|
-
// `app` is a regular express application, enhanced with
|
|
41
|
-
const app = expressX(
|
|
39
|
+
// `app` is a regular express application, enhanced with service and real-time features
|
|
40
|
+
const app = expressX()
|
|
42
41
|
|
|
43
42
|
// create two CRUD database services. They provide Prisma methods: `create`, 'createMany', 'find', 'findMany', 'upsert', etc.
|
|
44
43
|
app.createDatabaseService('User')
|
|
@@ -126,22 +125,16 @@ With a few lines of code, we got a complete REST API over the database tables. B
|
|
|
126
125
|
|
|
127
126
|
## Use it with a websocket client
|
|
128
127
|
|
|
129
|
-
First install ExpressX client library:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
npm i @jcbuisson/express-x-client
|
|
133
|
-
```
|
|
134
|
-
|
|
135
128
|
Create the following client NodeJS script:
|
|
136
129
|
|
|
137
130
|
```js
|
|
138
131
|
// client.js
|
|
139
132
|
import io from 'socket.io-client'
|
|
140
|
-
import
|
|
133
|
+
import { expressXClient } from '@jcbuisson/express-x'
|
|
141
134
|
|
|
142
135
|
const socket = io('http://localhost:8000', { transports: ["websocket"] })
|
|
143
136
|
|
|
144
|
-
const app =
|
|
137
|
+
const app = expressXClient(socket)
|
|
145
138
|
|
|
146
139
|
async function main() {
|
|
147
140
|
const user = await app.service('User').create({
|
|
@@ -180,7 +173,7 @@ node client.js
|
|
|
180
173
|
```
|
|
181
174
|
|
|
182
175
|
It prints the following lines in the console:
|
|
183
|
-
```
|
|
176
|
+
```json
|
|
184
177
|
joe {
|
|
185
178
|
id: 11,
|
|
186
179
|
name: 'Joe',
|
|
@@ -218,12 +211,11 @@ and then broacasted to all connected clients, leading to real-time updates.
|
|
|
218
211
|
|
|
219
212
|
```js
|
|
220
213
|
// app.js
|
|
221
|
-
import
|
|
222
|
-
import expressX from '@jcbuisson/express-x'
|
|
214
|
+
import { expressXServer } from '@jcbuisson/express-x'
|
|
223
215
|
import { PrismaClient } from '@prisma/client'
|
|
224
216
|
|
|
225
|
-
// `app` is a regular express application, enhanced with
|
|
226
|
-
const app = expressX(
|
|
217
|
+
// `app` is a regular express application, enhanced with service and real-time features
|
|
218
|
+
const app = expressX()
|
|
227
219
|
|
|
228
220
|
// configure prisma client from schema
|
|
229
221
|
app.set('prisma', new PrismaClient())
|
|
@@ -252,7 +244,6 @@ app.server.listen(8000, () => console.log(`App listening at http://localhost:800
|
|
|
252
244
|
Here is how a client may listen to channel events:
|
|
253
245
|
|
|
254
246
|
```js
|
|
255
|
-
// client.js
|
|
256
247
|
...
|
|
257
248
|
app.service('Post').on('create', post => {
|
|
258
249
|
console.log('post event created', post)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jcbuisson/express-x",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@jcbuisson/express-x-client": "^1.0.10",
|
|
22
22
|
"@prisma/client": "^4.10.1",
|
|
23
23
|
"axios": "^1.4.0",
|
|
24
|
+
"bcrypt": "^5.1.0",
|
|
24
25
|
"config": "^3.3.9",
|
|
25
26
|
"esm": "^3.2.25",
|
|
26
27
|
"express": "^4.18.2",
|
package/prisma/dev.db
CHANGED
|
Binary file
|
package/src/client.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import config from 'config'
|
|
3
|
+
import bcrypt from 'bcrypt'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// hash password of user record
|
|
7
|
+
// name of the password field is found in `config.authentication.local.passwordField` (default: 'password')
|
|
8
|
+
export async function hashPassword(context) {
|
|
9
|
+
const passwordField = config.authentication.local.passwordField
|
|
10
|
+
context.args[0][passwordField] = await bcrypt.hash(context.args[0][passwordField], 5)
|
|
11
|
+
return context
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export async function authenticate(context) {
|
|
16
|
+
console.log('authenticate hook', context.transport, context.name, context.action, context?.connection?.accessToken)
|
|
17
|
+
return context
|
|
18
|
+
|
|
19
|
+
if (context.transport === 'ws') {
|
|
20
|
+
// for WS transport, just check that connection.accessToken is set and valid
|
|
21
|
+
if (!context?.connection?.accessToken) throw new Error("WS connection is not secured by a JWT access token")
|
|
22
|
+
// TODO: check token validity
|
|
23
|
+
|
|
24
|
+
} else if (context.transport === 'http') {
|
|
25
|
+
// for HTTP transport, check that the associated request has a header with a valid JWT
|
|
26
|
+
// TODO: check headers for access token
|
|
27
|
+
}
|
|
28
|
+
return (context)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// remove `field` from `result`
|
|
33
|
+
export function protect(field) {
|
|
34
|
+
return async (context) => {
|
|
35
|
+
if (Array.isArray(context.result)) {
|
|
36
|
+
for (const value of context.result) {
|
|
37
|
+
delete value[field]
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
delete context.result[field]
|
|
41
|
+
}
|
|
42
|
+
return (context)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export async function setSessionJWT(context) {
|
|
48
|
+
if (context.transport === 'ws') {
|
|
49
|
+
// associate JWT token to WS connection to mark it as secure
|
|
50
|
+
context.connection.accessToken = context.result.accessToken
|
|
51
|
+
}
|
|
52
|
+
return context
|
|
53
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -1,381 +1,13 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { expressX } from './server.mjs'
|
|
3
|
+
import { expressXClient } from './client.mjs'
|
|
4
|
+
import { hashPassword, protect, setSessionJWT, } from './common-hooks.mjs'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
function expressX(app, options={}) {
|
|
6
|
+
export {
|
|
7
|
+
expressX,
|
|
8
|
+
expressXClient,
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const services = {}
|
|
15
|
-
const connections = {}
|
|
16
|
-
|
|
17
|
-
let lastConnectionId = 1
|
|
18
|
-
|
|
19
|
-
/*
|
|
20
|
-
* create a service `name` based on Prisma table `entity`
|
|
21
|
-
*/
|
|
22
|
-
function createDatabaseService(name, prismaOptions = { entity: name }) {
|
|
23
|
-
|
|
24
|
-
let prisma = app.get('prisma')
|
|
25
|
-
if (!prisma) {
|
|
26
|
-
prisma = new PrismaClient()
|
|
27
|
-
app.set('prisma', prisma)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// take all prisma methods on `entity` table
|
|
31
|
-
const methods = prisma[prismaOptions.entity]
|
|
32
|
-
|
|
33
|
-
const service = createService(name, methods)
|
|
34
|
-
|
|
35
|
-
service.prisma = prisma
|
|
36
|
-
service.entity = prismaOptions.entity
|
|
37
|
-
|
|
38
|
-
if (options.debug) console.log(`created service '${name}' over table '${prismaOptions.entity}'`)
|
|
39
|
-
return service
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/*
|
|
43
|
-
* create a service `name` with given `methods`
|
|
44
|
-
*/
|
|
45
|
-
function createService(name, methods) {
|
|
46
|
-
const service = { name }
|
|
47
|
-
|
|
48
|
-
for (const methodName in methods) {
|
|
49
|
-
const method = methods[methodName]
|
|
50
|
-
if (! method instanceof Function) continue
|
|
51
|
-
|
|
52
|
-
// `context` is the context of execution (transport type, connection, app)
|
|
53
|
-
// `args` is the list of arguments of the method
|
|
54
|
-
service['__' + methodName] = async (context, ...args) => {
|
|
55
|
-
context.args = args
|
|
56
|
-
|
|
57
|
-
// if a hook or the method throws an error, it will be caught by `socket.on('client-request'` (ws)
|
|
58
|
-
// or by express (http) and the client will get a rejected promise
|
|
59
|
-
|
|
60
|
-
// call 'before' hooks, modifying `context.args`
|
|
61
|
-
const beforeMethodHooks = service?.hooks?.before && service.hooks.before[methodName] || []
|
|
62
|
-
const beforeAllHooks = service?.hooks?.before?.all || []
|
|
63
|
-
for (const hook of [...beforeMethodHooks, ...beforeAllHooks]) {
|
|
64
|
-
context = await hook({ ...context, args })
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// call method
|
|
68
|
-
const result = await method(...context.args)
|
|
69
|
-
// if (options.debug) console.log('result', result)
|
|
70
|
-
|
|
71
|
-
// call 'after' hooks
|
|
72
|
-
const afterMethodHooks = service?.hooks?.after && service.hooks.after[methodName] || []
|
|
73
|
-
const afterAllHooks = service?.hooks?.after?.all || []
|
|
74
|
-
for (const hook of [...afterMethodHooks, ...afterAllHooks]) {
|
|
75
|
-
context = await hook({ ...context, result })
|
|
76
|
-
}
|
|
77
|
-
return result
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// hooked version of method: `create`, etc., to be called from backend with no context
|
|
81
|
-
service[methodName] = method
|
|
82
|
-
|
|
83
|
-
// un-hooked version of method: `_create`, etc., to be called from backend with no context
|
|
84
|
-
service['_' + methodName] = method
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// attach pub/sub publish callback
|
|
88
|
-
service.publish = async (func) => {
|
|
89
|
-
service.publishCallback = func
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
// attach hooks
|
|
93
|
-
service.hooks = (hooks) => {
|
|
94
|
-
service.hooks = hooks
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// cache service in `services`
|
|
98
|
-
services[name] = service
|
|
99
|
-
return service
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// `app.service(name)` starts here!
|
|
103
|
-
function service(name) {
|
|
104
|
-
// get service from `services` cache
|
|
105
|
-
if (name in services) return services[name]
|
|
106
|
-
throw Error(`there is no service named '${name}'`)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function configure(callback) {
|
|
110
|
-
callback(app)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/*
|
|
114
|
-
* add an HTTP REST endpoint at `path`, based on `service`
|
|
115
|
-
*/
|
|
116
|
-
async function addHttpRest(path, service) {
|
|
117
|
-
const context = {
|
|
118
|
-
app,
|
|
119
|
-
http: { name: service.name }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// introspect table schema
|
|
123
|
-
async function getFieldTypes() {
|
|
124
|
-
const fieldTypes = {}
|
|
125
|
-
if (service.prisma._activeProvider === 'sqlite') {
|
|
126
|
-
const fieldInfo = await service.prisma.$queryRawUnsafe(`
|
|
127
|
-
PRAGMA table_info(${service.entity})
|
|
128
|
-
`)
|
|
129
|
-
fieldInfo.forEach(column => {
|
|
130
|
-
fieldTypes[column.name] = column.type.toLowerCase()
|
|
131
|
-
})
|
|
132
|
-
} else if (service.prisma._activeProvider === 'postgresql') {
|
|
133
|
-
const fieldInfo = await service.prisma.$queryRawUnsafe(`
|
|
134
|
-
SELECT column_name, data_type
|
|
135
|
-
FROM information_schema.columns
|
|
136
|
-
WHERE table_name = '${service.entity}';
|
|
137
|
-
`)
|
|
138
|
-
fieldInfo.forEach(column => {
|
|
139
|
-
fieldTypes[column.column_name] = column.data_type
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
return fieldTypes
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
app.post(path, async (req, res) => {
|
|
147
|
-
if (options.debug) console.log("http request POST", req)
|
|
148
|
-
context.http.req = req
|
|
149
|
-
try {
|
|
150
|
-
const value = await service.__create(context, { data: req.body })
|
|
151
|
-
publish(service, 'create', value)
|
|
152
|
-
res.json(value)
|
|
153
|
-
} catch(err) {
|
|
154
|
-
console.log('callErr', err)
|
|
155
|
-
res.status(500).send(err.toString())
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
app.get(path, async (req, res) => {
|
|
160
|
-
if (options.debug) console.log("http request GET", req)
|
|
161
|
-
context.http.req = req
|
|
162
|
-
const query = { ...req.query }
|
|
163
|
-
try {
|
|
164
|
-
// the values in `req.query` are all strings, but Prisma need proper types
|
|
165
|
-
// we need to introspect column types and do the proper transtyping
|
|
166
|
-
for (const fieldName in query) {
|
|
167
|
-
if (!service.fieldTypes) service.fieldTypes = await getFieldTypes()
|
|
168
|
-
const fieldType = service.fieldTypes[fieldName]
|
|
169
|
-
|
|
170
|
-
if (fieldType === 'integer') {
|
|
171
|
-
query[fieldName] = parseInt(query[fieldName])
|
|
172
|
-
} else if (fieldType === 'numeric') {
|
|
173
|
-
query[fieldName] = parseFloat(query[fieldName])
|
|
174
|
-
} else if (fieldType === 'boolean') {
|
|
175
|
-
query[fieldName] = (query[fieldName] === 't') ? true : false
|
|
176
|
-
} else if (fieldType === 'text' || fieldType === 'character varying') {
|
|
177
|
-
query[fieldName] = query[fieldName]
|
|
178
|
-
} else {
|
|
179
|
-
// ?
|
|
180
|
-
query[fieldName] = query[fieldName]
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const values = await service.__findMany(context, {
|
|
185
|
-
where: query,
|
|
186
|
-
})
|
|
187
|
-
publish(service, 'findMany', values)
|
|
188
|
-
res.json(values)
|
|
189
|
-
} catch(err) {
|
|
190
|
-
console.log('callErr', err)
|
|
191
|
-
res.status(500).send(err.toString())
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
app.get(`${path}/:id`, async (req, res) => {
|
|
196
|
-
if (options.debug) console.log("http request GET", req)
|
|
197
|
-
context.http.req = req
|
|
198
|
-
try {
|
|
199
|
-
const value = await service.__findUnique(context, {
|
|
200
|
-
where: {
|
|
201
|
-
id: parseInt(req.params.id)
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
publish(service, 'findUnique', value)
|
|
205
|
-
res.json(value)
|
|
206
|
-
} catch(err) {
|
|
207
|
-
console.log('callErr', err)
|
|
208
|
-
res.status(500).send(err.toString())
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
app.patch(`${path}/:id`, async (req, res) => {
|
|
213
|
-
if (options.debug) console.log("http request PATCH", req)
|
|
214
|
-
context.http.req = req
|
|
215
|
-
try {
|
|
216
|
-
const value = await service.__update(context, {
|
|
217
|
-
where: {
|
|
218
|
-
id: parseInt(req.params.id),
|
|
219
|
-
},
|
|
220
|
-
data: req.body,
|
|
221
|
-
})
|
|
222
|
-
publish(service, 'update', value)
|
|
223
|
-
res.json(value)
|
|
224
|
-
} catch(err) {
|
|
225
|
-
console.log('callErr', err)
|
|
226
|
-
res.status(500).send(err.toString())
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
app.delete(`${path}/:id`, async (req, res) => {
|
|
231
|
-
if (options.debug) console.log("http request DELETE", req)
|
|
232
|
-
context.http.req = req
|
|
233
|
-
try {
|
|
234
|
-
const value = await service.__delete(context, {
|
|
235
|
-
where: {
|
|
236
|
-
id: parseInt(req.params.id)
|
|
237
|
-
}
|
|
238
|
-
})
|
|
239
|
-
publish(service, 'delete', value)
|
|
240
|
-
res.json(value)
|
|
241
|
-
} catch(err) {
|
|
242
|
-
console.log('callErr', err)
|
|
243
|
-
res.status(500).send(err.toString())
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
if (options.debug) console.log(`added HTTP endpoints for service '${service.name}' at path '${path}'`)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/*
|
|
251
|
-
* Create HTTP server
|
|
252
|
-
*/
|
|
253
|
-
const server = new http.Server(app)
|
|
254
|
-
|
|
255
|
-
if (options.ws) {
|
|
256
|
-
/*
|
|
257
|
-
* Add websocket transport
|
|
258
|
-
*/
|
|
259
|
-
const io = new Server(server)
|
|
260
|
-
|
|
261
|
-
io.on('connection', function(socket) {
|
|
262
|
-
if (options.debug) console.log('Client connected to the WebSocket')
|
|
263
|
-
const connection = {
|
|
264
|
-
id: lastConnectionId++,
|
|
265
|
-
socket,
|
|
266
|
-
channelNames: new Set(),
|
|
267
|
-
}
|
|
268
|
-
// store connection in cache
|
|
269
|
-
connections[connection.id] = connection
|
|
270
|
-
if (options.debug) console.log('active connections', Object.keys(connections))
|
|
271
|
-
|
|
272
|
-
// emit 'connection' event for app (expressjs extends EventEmitter)
|
|
273
|
-
app.emit('connection', connection)
|
|
274
|
-
|
|
275
|
-
// send 'connected' event to client
|
|
276
|
-
socket.emit('connected', connection.id)
|
|
277
|
-
|
|
278
|
-
socket.on('disconnect', () => {
|
|
279
|
-
if (options.debug) console.log('Client disconnected', connection.id)
|
|
280
|
-
delete connections[connection.id]
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
/*
|
|
285
|
-
* Handle websocket client request
|
|
286
|
-
* Emit in return a 'client-response' message
|
|
287
|
-
*/
|
|
288
|
-
socket.on('client-request', async ({ uid, name, action, args }) => {
|
|
289
|
-
if (options.debug) console.log("client-request", uid, name, action, args)
|
|
290
|
-
if (name in services) {
|
|
291
|
-
const service = services[name]
|
|
292
|
-
try {
|
|
293
|
-
const serviceMethod = service['__' + action]
|
|
294
|
-
if (serviceMethod) {
|
|
295
|
-
const context = {
|
|
296
|
-
app,
|
|
297
|
-
ws: { connection, name, action, args },
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
const result = await serviceMethod(context, ...args)
|
|
302
|
-
socket.emit('client-response', {
|
|
303
|
-
uid,
|
|
304
|
-
result,
|
|
305
|
-
})
|
|
306
|
-
// pub/sub: send event on associated channels
|
|
307
|
-
publish(service, action, result)
|
|
308
|
-
} catch(err) {
|
|
309
|
-
console.log('callErr', err)
|
|
310
|
-
io.emit('client-response', {
|
|
311
|
-
uid,
|
|
312
|
-
error: err.toString(),
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
io.emit('client-response', {
|
|
317
|
-
uid,
|
|
318
|
-
error: `there is no method named '${action}' for service '${name}'`,
|
|
319
|
-
})
|
|
320
|
-
}
|
|
321
|
-
} catch(error) {
|
|
322
|
-
io.emit('client-response', {
|
|
323
|
-
uid,
|
|
324
|
-
error,
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
io.emit('client-response', {
|
|
329
|
-
uid,
|
|
330
|
-
error: `there is no service named '${name}'`,
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
})
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// publish event on associated channels
|
|
338
|
-
async function publish(service, action, result) {
|
|
339
|
-
const publishFunc = service.publishCallback
|
|
340
|
-
if (publishFunc) {
|
|
341
|
-
const channelNames = await publishFunc(result, app)
|
|
342
|
-
if (options.debug) console.log('publish channels', service.name, action, channelNames)
|
|
343
|
-
for (const channelName of channelNames) {
|
|
344
|
-
if (options.debug) console.log('service-event', service.name, action, channelName)
|
|
345
|
-
const connectionList = Object.values(connections).filter(cnx => cnx.channelNames.has(channelName))
|
|
346
|
-
for (const connection of connectionList) {
|
|
347
|
-
if (options.debug) console.log('emit to', connection.id, service.name, action, result)
|
|
348
|
-
connection.socket.emit('service-event', {
|
|
349
|
-
name: service.name,
|
|
350
|
-
action,
|
|
351
|
-
result,
|
|
352
|
-
})
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function joinChannel(channelName, connection) {
|
|
359
|
-
connection.channelNames.add(channelName)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function leaveChannel(channelName, connection) {
|
|
363
|
-
connection.channelNames.delete(channelName)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// enhance `app` with objects and methods
|
|
367
|
-
Object.assign(app, {
|
|
368
|
-
options,
|
|
369
|
-
createDatabaseService,
|
|
370
|
-
createService,
|
|
371
|
-
service,
|
|
372
|
-
configure,
|
|
373
|
-
addHttpRest,
|
|
374
|
-
server,
|
|
375
|
-
joinChannel,
|
|
376
|
-
leaveChannel,
|
|
377
|
-
})
|
|
378
|
-
return app
|
|
10
|
+
hashPassword,
|
|
11
|
+
protect,
|
|
12
|
+
setSessionJWT,
|
|
379
13
|
}
|
|
380
|
-
|
|
381
|
-
export default expressX
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
|
|
2
|
+
import http from 'http'
|
|
3
|
+
import { Server } from "socket.io"
|
|
4
|
+
import express from 'express'
|
|
5
|
+
import { PrismaClient } from '@prisma/client'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Enhance `app` express application with services and real-time features
|
|
9
|
+
*/
|
|
10
|
+
export function expressX(options={ debug: false }) {
|
|
11
|
+
|
|
12
|
+
const app = express()
|
|
13
|
+
|
|
14
|
+
if (options.debug === undefined) options.debug = true
|
|
15
|
+
if (options.ws === undefined) options.ws = { ws_prefix: "expressx" }
|
|
16
|
+
|
|
17
|
+
const services = {}
|
|
18
|
+
const connections = {}
|
|
19
|
+
|
|
20
|
+
let lastConnectionId = 1
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* create a service `name` based on Prisma table `entity`
|
|
24
|
+
*/
|
|
25
|
+
function createDatabaseService(name, prismaOptions = { entity: name }) {
|
|
26
|
+
|
|
27
|
+
let prisma = app.get('prisma')
|
|
28
|
+
if (!prisma) {
|
|
29
|
+
prisma = new PrismaClient()
|
|
30
|
+
app.set('prisma', prisma)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// take all prisma methods on `entity` table
|
|
34
|
+
const methods = prisma[prismaOptions.entity]
|
|
35
|
+
|
|
36
|
+
const service = createService(name, methods)
|
|
37
|
+
|
|
38
|
+
service.prisma = prisma
|
|
39
|
+
service.entity = prismaOptions.entity
|
|
40
|
+
|
|
41
|
+
if (options.debug) console.log(`created service '${name}' over table '${prismaOptions.entity}'`)
|
|
42
|
+
return service
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* create a service `name` with given `methods`
|
|
47
|
+
*/
|
|
48
|
+
function createService(name, methods) {
|
|
49
|
+
const service = { name }
|
|
50
|
+
|
|
51
|
+
for (const methodName in methods) {
|
|
52
|
+
const method = methods[methodName]
|
|
53
|
+
if (! method instanceof Function) continue
|
|
54
|
+
|
|
55
|
+
// `context` is the context of execution (transport type, connection, app)
|
|
56
|
+
// `args` is the list of arguments of the method
|
|
57
|
+
service['__' + methodName] = async (context, ...args) => {
|
|
58
|
+
context.args = args
|
|
59
|
+
|
|
60
|
+
// if a hook or the method throws an error, it will be caught by `socket.on('client-request'` (ws)
|
|
61
|
+
// or by express (http) and the client will get a rejected promise
|
|
62
|
+
|
|
63
|
+
// call 'before' hooks, modifying `context.args`
|
|
64
|
+
const beforeMethodHooks = service?.hooks?.before && service.hooks.before[methodName] || []
|
|
65
|
+
const beforeAllHooks = service?.hooks?.before?.all || []
|
|
66
|
+
for (const hook of [...beforeMethodHooks, ...beforeAllHooks]) {
|
|
67
|
+
context = await hook({ ...context, args })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// call method
|
|
71
|
+
const result = await method(...context.args)
|
|
72
|
+
// if (options.debug) console.log('result', result)
|
|
73
|
+
|
|
74
|
+
// call 'after' hooks
|
|
75
|
+
const afterMethodHooks = service?.hooks?.after && service.hooks.after[methodName] || []
|
|
76
|
+
const afterAllHooks = service?.hooks?.after?.all || []
|
|
77
|
+
for (const hook of [...afterMethodHooks, ...afterAllHooks]) {
|
|
78
|
+
context = await hook({ ...context, result })
|
|
79
|
+
}
|
|
80
|
+
return result
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// hooked version of method: `create`, etc., to be called from backend with no context
|
|
84
|
+
service[methodName] = method
|
|
85
|
+
|
|
86
|
+
// un-hooked version of method: `_create`, etc., to be called from backend with no context
|
|
87
|
+
service['_' + methodName] = method
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// attach pub/sub publish callback
|
|
91
|
+
service.publish = async (func) => {
|
|
92
|
+
service.publishCallback = func
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// attach hooks
|
|
96
|
+
service.hooks = (hooks) => {
|
|
97
|
+
service.hooks = hooks
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// cache service in `services`
|
|
101
|
+
services[name] = service
|
|
102
|
+
return service
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// `app.service(name)` starts here!
|
|
106
|
+
function service(name) {
|
|
107
|
+
// get service from `services` cache
|
|
108
|
+
if (name in services) return services[name]
|
|
109
|
+
throw Error(`there is no service named '${name}'`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function configure(callback) {
|
|
113
|
+
callback(app)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
* add an HTTP REST endpoint at `path`, based on `service`
|
|
118
|
+
*/
|
|
119
|
+
async function addHttpRest(path, service) {
|
|
120
|
+
const context = {
|
|
121
|
+
app,
|
|
122
|
+
http: { name: service.name }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// introspect table schema
|
|
126
|
+
async function getFieldTypes() {
|
|
127
|
+
const fieldTypes = {}
|
|
128
|
+
if (service.prisma._activeProvider === 'sqlite') {
|
|
129
|
+
const fieldInfo = await service.prisma.$queryRawUnsafe(`
|
|
130
|
+
PRAGMA table_info(${service.entity})
|
|
131
|
+
`)
|
|
132
|
+
fieldInfo.forEach(column => {
|
|
133
|
+
fieldTypes[column.name] = column.type.toLowerCase()
|
|
134
|
+
})
|
|
135
|
+
} else if (service.prisma._activeProvider === 'postgresql') {
|
|
136
|
+
const fieldInfo = await service.prisma.$queryRawUnsafe(`
|
|
137
|
+
SELECT column_name, data_type
|
|
138
|
+
FROM information_schema.columns
|
|
139
|
+
WHERE table_name = '${service.entity}';
|
|
140
|
+
`)
|
|
141
|
+
fieldInfo.forEach(column => {
|
|
142
|
+
fieldTypes[column.column_name] = column.data_type
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
return fieldTypes
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
app.post(path, async (req, res) => {
|
|
150
|
+
if (options.debug) console.log("http request POST", req.url)
|
|
151
|
+
context.http.req = req
|
|
152
|
+
try {
|
|
153
|
+
const value = await service.__create(context, { data: req.body })
|
|
154
|
+
publish(service, 'create', value)
|
|
155
|
+
res.json(value)
|
|
156
|
+
} catch(err) {
|
|
157
|
+
console.log('callErr', err)
|
|
158
|
+
res.status(500).send(err.toString())
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
app.get(path, async (req, res) => {
|
|
163
|
+
if (options.debug) console.log("http request GET", req.url)
|
|
164
|
+
context.http.req = req
|
|
165
|
+
const query = { ...req.query }
|
|
166
|
+
try {
|
|
167
|
+
// the values in `req.query` are all strings, but Prisma need proper types
|
|
168
|
+
// we need to introspect column types and do the proper transtyping
|
|
169
|
+
for (const fieldName in query) {
|
|
170
|
+
if (!service.fieldTypes) service.fieldTypes = await getFieldTypes()
|
|
171
|
+
const fieldType = service.fieldTypes[fieldName]
|
|
172
|
+
|
|
173
|
+
if (fieldType === 'integer') {
|
|
174
|
+
query[fieldName] = parseInt(query[fieldName])
|
|
175
|
+
} else if (fieldType === 'numeric') {
|
|
176
|
+
query[fieldName] = parseFloat(query[fieldName])
|
|
177
|
+
} else if (fieldType === 'boolean') {
|
|
178
|
+
query[fieldName] = (query[fieldName] === 't') ? true : false
|
|
179
|
+
} else if (fieldType === 'text' || fieldType === 'character varying') {
|
|
180
|
+
query[fieldName] = query[fieldName]
|
|
181
|
+
} else {
|
|
182
|
+
// ?
|
|
183
|
+
query[fieldName] = query[fieldName]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const values = await service.__findMany(context, {
|
|
188
|
+
where: query,
|
|
189
|
+
})
|
|
190
|
+
publish(service, 'findMany', values)
|
|
191
|
+
res.json(values)
|
|
192
|
+
} catch(err) {
|
|
193
|
+
console.log('callErr', err)
|
|
194
|
+
res.status(500).send(err.toString())
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
app.get(`${path}/:id`, async (req, res) => {
|
|
199
|
+
if (options.debug) console.log("http request GET", req.url)
|
|
200
|
+
context.http.req = req
|
|
201
|
+
try {
|
|
202
|
+
const value = await service.__findUnique(context, {
|
|
203
|
+
where: {
|
|
204
|
+
id: parseInt(req.params.id)
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
publish(service, 'findUnique', value)
|
|
208
|
+
res.json(value)
|
|
209
|
+
} catch(err) {
|
|
210
|
+
console.log('callErr', err)
|
|
211
|
+
res.status(500).send(err.toString())
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
app.patch(`${path}/:id`, async (req, res) => {
|
|
216
|
+
if (options.debug) console.log("http request PATCH", req.url)
|
|
217
|
+
context.http.req = req
|
|
218
|
+
try {
|
|
219
|
+
const value = await service.__update(context, {
|
|
220
|
+
where: {
|
|
221
|
+
id: parseInt(req.params.id),
|
|
222
|
+
},
|
|
223
|
+
data: req.body,
|
|
224
|
+
})
|
|
225
|
+
publish(service, 'update', value)
|
|
226
|
+
res.json(value)
|
|
227
|
+
} catch(err) {
|
|
228
|
+
console.log('callErr', err)
|
|
229
|
+
res.status(500).send(err.toString())
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
app.delete(`${path}/:id`, async (req, res) => {
|
|
234
|
+
if (options.debug) console.log("http request DELETE", req.url)
|
|
235
|
+
context.http.req = req
|
|
236
|
+
try {
|
|
237
|
+
const value = await service.__delete(context, {
|
|
238
|
+
where: {
|
|
239
|
+
id: parseInt(req.params.id)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
publish(service, 'delete', value)
|
|
243
|
+
res.json(value)
|
|
244
|
+
} catch(err) {
|
|
245
|
+
console.log('callErr', err)
|
|
246
|
+
res.status(500).send(err.toString())
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (options.debug) console.log(`added HTTP endpoints for service '${service.name}' at path '${path}'`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* Create HTTP server
|
|
255
|
+
*/
|
|
256
|
+
const server = new http.Server(app)
|
|
257
|
+
|
|
258
|
+
if (options.ws) {
|
|
259
|
+
/*
|
|
260
|
+
* Add websocket transport
|
|
261
|
+
*/
|
|
262
|
+
const io = new Server(server)
|
|
263
|
+
|
|
264
|
+
io.on('connection', function(socket) {
|
|
265
|
+
if (options.debug) console.log('Client connected to the WebSocket')
|
|
266
|
+
const connection = {
|
|
267
|
+
id: lastConnectionId++,
|
|
268
|
+
socket,
|
|
269
|
+
channelNames: new Set(),
|
|
270
|
+
}
|
|
271
|
+
// store connection in cache
|
|
272
|
+
connections[connection.id] = connection
|
|
273
|
+
if (options.debug) console.log('active connections', Object.keys(connections))
|
|
274
|
+
|
|
275
|
+
// emit 'connection' event for app (expressjs extends EventEmitter)
|
|
276
|
+
app.emit('connection', connection)
|
|
277
|
+
|
|
278
|
+
// send 'connected' event to client
|
|
279
|
+
socket.emit('connected', connection.id)
|
|
280
|
+
|
|
281
|
+
socket.on('disconnect', () => {
|
|
282
|
+
if (options.debug) console.log('Client disconnected', connection.id)
|
|
283
|
+
delete connections[connection.id]
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* Handle websocket client request
|
|
289
|
+
* Emit in return a 'client-response' message
|
|
290
|
+
*/
|
|
291
|
+
socket.on('client-request', async ({ uid, name, action, args }) => {
|
|
292
|
+
if (options.debug) console.log("client-request", uid, name, action, args)
|
|
293
|
+
if (name in services) {
|
|
294
|
+
const service = services[name]
|
|
295
|
+
try {
|
|
296
|
+
const serviceMethod = service['__' + action]
|
|
297
|
+
if (serviceMethod) {
|
|
298
|
+
const context = {
|
|
299
|
+
app,
|
|
300
|
+
ws: { connection, name, action, args },
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const result = await serviceMethod(context, ...args)
|
|
305
|
+
socket.emit('client-response', {
|
|
306
|
+
uid,
|
|
307
|
+
result,
|
|
308
|
+
})
|
|
309
|
+
// pub/sub: send event on associated channels
|
|
310
|
+
publish(service, action, result)
|
|
311
|
+
} catch(err) {
|
|
312
|
+
console.log('callErr', err)
|
|
313
|
+
io.emit('client-response', {
|
|
314
|
+
uid,
|
|
315
|
+
error: err.toString(),
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
io.emit('client-response', {
|
|
320
|
+
uid,
|
|
321
|
+
error: `there is no method named '${action}' for service '${name}'`,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
} catch(error) {
|
|
325
|
+
io.emit('client-response', {
|
|
326
|
+
uid,
|
|
327
|
+
error,
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
io.emit('client-response', {
|
|
332
|
+
uid,
|
|
333
|
+
error: `there is no service named '${name}'`,
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// publish event on associated channels
|
|
341
|
+
async function publish(service, action, result) {
|
|
342
|
+
const publishFunc = service.publishCallback
|
|
343
|
+
if (publishFunc) {
|
|
344
|
+
const channelNames = await publishFunc(result, app)
|
|
345
|
+
if (options.debug) console.log('publish channels', service.name, action, channelNames)
|
|
346
|
+
for (const channelName of channelNames) {
|
|
347
|
+
if (options.debug) console.log('service-event', service.name, action, channelName)
|
|
348
|
+
const connectionList = Object.values(connections).filter(cnx => cnx.channelNames.has(channelName))
|
|
349
|
+
for (const connection of connectionList) {
|
|
350
|
+
if (options.debug) console.log('emit to', connection.id, service.name, action, result)
|
|
351
|
+
connection.socket.emit('service-event', {
|
|
352
|
+
name: service.name,
|
|
353
|
+
action,
|
|
354
|
+
result,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function joinChannel(channelName, connection) {
|
|
362
|
+
connection.channelNames.add(channelName)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function leaveChannel(channelName, connection) {
|
|
366
|
+
connection.channelNames.delete(channelName)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// enhance `app` with objects and methods
|
|
370
|
+
return Object.assign(app, {
|
|
371
|
+
options,
|
|
372
|
+
createDatabaseService,
|
|
373
|
+
createService,
|
|
374
|
+
service,
|
|
375
|
+
configure,
|
|
376
|
+
addHttpRest,
|
|
377
|
+
server,
|
|
378
|
+
joinChannel,
|
|
379
|
+
leaveChannel,
|
|
380
|
+
})
|
|
381
|
+
}
|
package/test/index.test.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
|
|
2
|
-
import express from 'express'
|
|
3
2
|
import bodyParser from 'body-parser'
|
|
4
3
|
import axios from 'axios'
|
|
5
4
|
import io from 'socket.io-client'
|
|
6
|
-
import expressxClient from '@jcbuisson/express-x-client'
|
|
7
5
|
|
|
8
6
|
|
|
9
|
-
import {
|
|
7
|
+
import { assert } from 'chai'
|
|
10
8
|
|
|
11
|
-
import expressX from '../src/index.mjs'
|
|
9
|
+
import { expressX, expressXClient } from '../src/index.mjs'
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
// `app` is a regular express application, enhanced with
|
|
15
|
-
const app = expressX(
|
|
12
|
+
// `app` is a regular express application, enhanced with services and real-time features
|
|
13
|
+
const app = expressX()
|
|
16
14
|
|
|
17
15
|
app.createDatabaseService('User')
|
|
18
16
|
app.createDatabaseService('Post')
|
|
@@ -23,7 +21,8 @@ describe('ExpressX API (no running server)', () => {
|
|
|
23
21
|
|
|
24
22
|
it("can delete all users", async () => {
|
|
25
23
|
const res = await app.service('User').deleteMany()
|
|
26
|
-
|
|
24
|
+
console.log('res delete', res)
|
|
25
|
+
assert(res.count >= 0)
|
|
27
26
|
})
|
|
28
27
|
|
|
29
28
|
it("can create a user", async () => {
|
|
@@ -125,7 +124,7 @@ describe('Client API', () => {
|
|
|
125
124
|
app.server.listen(8008, () => console.log(`App listening at http://localhost:8008`))
|
|
126
125
|
|
|
127
126
|
socket = io('http://localhost:8008', { transports: ["websocket"] })
|
|
128
|
-
clientApp =
|
|
127
|
+
clientApp = expressXClient(socket)
|
|
129
128
|
})
|
|
130
129
|
|
|
131
130
|
it("can create a user", async () => {
|