@jcbuisson/express-x 1.1.0 → 1.1.3
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 +262 -0
- package/package.json +10 -4
- package/prisma/dev.db +0 -0
- package/prisma/schema.prisma +22 -0
- package/src/index.mjs +101 -91
- package/test/index.test.js +110 -0
package/README.md
CHANGED
|
@@ -1 +1,263 @@
|
|
|
1
1
|
|
|
2
|
+
|
|
3
|
+
# Getting started
|
|
4
|
+
|
|
5
|
+
## Create a project
|
|
6
|
+
|
|
7
|
+
Let's create a new folder for our application:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
mkdir expressx-project
|
|
11
|
+
cd expressx-project
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Since any ExpressX application is a Node application, we can create a default package.json using npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm init es6 --yes
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `es6` argument adds `"type": "module"` in `package.json`. Beware! All further module imports must made with es6/esm `import` syntax.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Install ExpressX
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @jcbuisson/express-x
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Our first server application
|
|
30
|
+
|
|
31
|
+
Now we can create an ExpressX application which will provide a complete REST API on a `user` resource
|
|
32
|
+
backed in a [Prisma](https://www.prisma.io/) database
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// app.js
|
|
36
|
+
import express from 'express'
|
|
37
|
+
import bodyParser from 'body-parser'
|
|
38
|
+
import expressX from '@jcbuisson/express-x'
|
|
39
|
+
|
|
40
|
+
// `app` is a regular express application, enhanced with express-x features
|
|
41
|
+
const app = expressX(express())
|
|
42
|
+
|
|
43
|
+
// create two CRUD database services. They provide Prisma methods: `create`, 'createMany', 'find', 'findMany', 'upsert', etc.
|
|
44
|
+
app.createDatabaseService('User')
|
|
45
|
+
app.createDatabaseService('Post')
|
|
46
|
+
|
|
47
|
+
// add body parsers for http requests
|
|
48
|
+
app.use(bodyParser.json())
|
|
49
|
+
app.use(bodyParser.urlencoded({ extended: false }))
|
|
50
|
+
|
|
51
|
+
// add http/rest endpoints
|
|
52
|
+
app.addHttpRest('/api/user', app.service('User'))
|
|
53
|
+
app.addHttpRest('/api/post', app.service('Post'))
|
|
54
|
+
|
|
55
|
+
app.server.listen(8000, () => console.log(`App listening at http://localhost:8000`))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Before running it we need to setup the corresponding database.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Create the database
|
|
62
|
+
|
|
63
|
+
Presently, ExpressX only handles [Prisma](https://www.prisma.io/). Prisma is able to connect to most brands of relational or NoSQL databases.
|
|
64
|
+
|
|
65
|
+
First, provide the database schema in `prisma/schema.prisma`:
|
|
66
|
+
```prisma
|
|
67
|
+
generator client {
|
|
68
|
+
provider = "prisma-client-js"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
model User {
|
|
72
|
+
id Int @default(autoincrement()) @id
|
|
73
|
+
name String
|
|
74
|
+
posts Post[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
model Post {
|
|
78
|
+
id Int @default(autoincrement()) @id
|
|
79
|
+
text String
|
|
80
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
81
|
+
authorId Int
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
datasource db {
|
|
85
|
+
provider = "sqlite"
|
|
86
|
+
url = "file:./dev.db"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then create the database:
|
|
91
|
+
```bash
|
|
92
|
+
npx prisma migrate dev --name init
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The sqlite database file is created at `prisma/dev.db`
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Run the application
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
node app.js
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It prints the following lines in the console:
|
|
105
|
+
```bash
|
|
106
|
+
created service 'user' over table 'User'
|
|
107
|
+
created service 'post' over table 'Post'
|
|
108
|
+
added HTTP endpoints for service 'user' at path '/api/user'
|
|
109
|
+
added HTTP endpoints for service 'post' at path '/api/post'
|
|
110
|
+
App listening at http://localhost:8000
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## Enjoy your HTTP REST API
|
|
115
|
+
|
|
116
|
+
Now you can try the HTTP endpoints `/api/user` and `/api/post`; open a new console and run HTTP requests:
|
|
117
|
+
```bash
|
|
118
|
+
curl -X POST -H 'Content-Type: application/json' -d '{"name":"JC"}' http://localhost:8000/api/user
|
|
119
|
+
# --> {"id":1,"name":"JC"}
|
|
120
|
+
curl http://localhost:8000/api/user
|
|
121
|
+
# --> [{"id":1,"name":"JC"}]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With a few lines of code, we got a complete REST API over the database tables. But there is more!
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## Use it with a websocket client
|
|
128
|
+
|
|
129
|
+
First install ExpressX client library:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm i @jcbuisson/express-x-client
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Create the following client NodeJS script:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
// client.js
|
|
139
|
+
import io from 'socket.io-client'
|
|
140
|
+
import expressxClient from '@jcbuisson/express-x-client'
|
|
141
|
+
|
|
142
|
+
const socket = io('http://localhost:8000', { transports: ["websocket"] })
|
|
143
|
+
|
|
144
|
+
const app = expressxClient(socket)
|
|
145
|
+
|
|
146
|
+
async function main() {
|
|
147
|
+
const user = await app.service('User').create({
|
|
148
|
+
name: "Joe",
|
|
149
|
+
})
|
|
150
|
+
await app.service('Post').create({
|
|
151
|
+
authorId: user.id,
|
|
152
|
+
text: "Post#1"
|
|
153
|
+
})
|
|
154
|
+
await app.service('Post').create({
|
|
155
|
+
authorId: user.id,
|
|
156
|
+
text: "Post#2"
|
|
157
|
+
})
|
|
158
|
+
const joe = await app.service('User').findUnique({
|
|
159
|
+
where: {
|
|
160
|
+
id: user.id,
|
|
161
|
+
},
|
|
162
|
+
include: {
|
|
163
|
+
posts: true,
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
console.log('joe', joe)
|
|
167
|
+
process.exit(0)
|
|
168
|
+
}
|
|
169
|
+
main()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
For simplicity we use a node client, but you of course you would write something similar with your favorite front-end framework.
|
|
173
|
+
|
|
174
|
+
You can use on the client side the exact same statements on services as you would on the server side, such as: `app.service('User').create(...)`.
|
|
175
|
+
Of course the `app` object here on the client is quite different that the `app` object on the server; you can find explanations [here]().
|
|
176
|
+
|
|
177
|
+
Now run the client script:
|
|
178
|
+
```bash
|
|
179
|
+
node client.js
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
It prints the following lines in the console:
|
|
183
|
+
```
|
|
184
|
+
joe {
|
|
185
|
+
id: 11,
|
|
186
|
+
name: 'Joe',
|
|
187
|
+
posts: [
|
|
188
|
+
{ id: 12, text: 'Post#1', authorId: 11 },
|
|
189
|
+
{ id: 13, text: 'Post#2', authorId: 11 }
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
We have a GraphQL-like experience with the nested posts, thanks to Prisma and its use through ExpressX services.
|
|
195
|
+
|
|
196
|
+
::: info
|
|
197
|
+
We could have removed from `app.js` all lines related to HTTP, since we are only using the websocket transport.
|
|
198
|
+
:::
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## Real-time applications
|
|
202
|
+
|
|
203
|
+
When websocket transport is used (default situation) and when a connected client calls a service method,
|
|
204
|
+
two twings happen on method completion:
|
|
205
|
+
|
|
206
|
+
- the resulting value is sent to the client
|
|
207
|
+
- an event is emitted, and sent to connected clients we'll call subscribers. The calling client may or not be one of those subscribers.
|
|
208
|
+
|
|
209
|
+
For example in a medical application, whenever a patients's record is modified, an event could be sent to all his/her caregivers.
|
|
210
|
+
|
|
211
|
+
***Channels*** are used for this pub/sub mechanism. Service methods ***publish*** events on ***channels***, and clients ***subscribe***
|
|
212
|
+
to channels in order to receive those events. ExpressX provides functions to configure which events are published to which channels.
|
|
213
|
+
A channel is represented by a name and you can create and use as many channels as you need.
|
|
214
|
+
|
|
215
|
+
In the following example, every time a client connects to the server, it joins (= is subscribed to) the 'anonymous' channel.
|
|
216
|
+
And whenever an event is emited by the `post` or `user` service, this event is published on this channel,
|
|
217
|
+
and then broacasted to all connected clients, leading to real-time updates.
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
// app.js
|
|
221
|
+
import express from 'express'
|
|
222
|
+
import expressX from '@jcbuisson/express-x'
|
|
223
|
+
import { PrismaClient } from '@prisma/client'
|
|
224
|
+
|
|
225
|
+
// `app` is a regular express application, enhanced with express-x features
|
|
226
|
+
const app = expressX(express())
|
|
227
|
+
|
|
228
|
+
// configure prisma client from schema
|
|
229
|
+
app.set('prisma', new PrismaClient())
|
|
230
|
+
|
|
231
|
+
// create two CRUD database services. They provide Prisma methods: `create`, 'createMany', 'find', 'findMany', 'upsert', etc.
|
|
232
|
+
app.createDatabaseService('User')
|
|
233
|
+
app.createDatabaseService('Post')
|
|
234
|
+
|
|
235
|
+
// publish
|
|
236
|
+
app.service('User').publish(async (post, context) => {
|
|
237
|
+
return ['anonymous']
|
|
238
|
+
})
|
|
239
|
+
app.service('Post').publish(async (post, context) => {
|
|
240
|
+
return ['anonymous']
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// subscribe
|
|
244
|
+
app.on('connection', (connection) => {
|
|
245
|
+
console.log('connection', connection.id)
|
|
246
|
+
app.joinChannel('anonymous', connection)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
app.server.listen(8000, () => console.log(`App listening at http://localhost:8000`))
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Here is how a client may listen to channel events:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
// client.js
|
|
256
|
+
...
|
|
257
|
+
app.service('Post').on('create', post => {
|
|
258
|
+
console.log('post event created', post)
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The listener is triggered whenever the client receives from the server a `create` event from the service `post`.
|
|
263
|
+
This event results from the completion on the server of a call `app.service('Post').create()`
|
package/package.json
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jcbuisson/express-x",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git@
|
|
9
|
+
"url": "git@github.com:jcbuisson/express-x.git"
|
|
10
10
|
},
|
|
11
11
|
"author": "Jean-Christophe Buisson <buisson@enseeiht.fr> (jcbuisson.dev)",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"private": false,
|
|
14
14
|
"scripts": {
|
|
15
|
-
|
|
15
|
+
"test": "mocha --require esm 'test/**/*.test.js'",
|
|
16
|
+
"generate": "npx prisma generate",
|
|
17
|
+
"migrate": "npx prisma migrate dev --name init"
|
|
18
|
+
},
|
|
16
19
|
"keywords": [],
|
|
17
20
|
"dependencies": {
|
|
18
21
|
"@prisma/client": "^4.10.1",
|
|
22
|
+
"axios": "^1.4.0",
|
|
19
23
|
"config": "^3.3.9",
|
|
24
|
+
"esm": "^3.2.25",
|
|
20
25
|
"express": "^4.18.2",
|
|
21
26
|
"socket.io": "^4.6.0"
|
|
22
27
|
},
|
|
23
28
|
"devDependencies": {
|
|
24
|
-
"
|
|
29
|
+
"chai": "^4.3.7",
|
|
30
|
+
"mocha": "^10.2.0"
|
|
25
31
|
}
|
|
26
32
|
}
|
package/prisma/dev.db
ADDED
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
model User {
|
|
6
|
+
id Int @default(autoincrement()) @id
|
|
7
|
+
name String
|
|
8
|
+
email String @unique
|
|
9
|
+
posts Post[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
model Post {
|
|
13
|
+
id Int @default(autoincrement()) @id
|
|
14
|
+
text String
|
|
15
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
16
|
+
authorId Int
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
datasource db {
|
|
20
|
+
provider = "sqlite"
|
|
21
|
+
url = "file:./dev.db"
|
|
22
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { PrismaClient } from '@prisma/client'
|
|
|
8
8
|
*/
|
|
9
9
|
function expressX(app, options={}) {
|
|
10
10
|
|
|
11
|
-
if (options.debug === undefined) options.debug =
|
|
11
|
+
if (options.debug === undefined) options.debug = false
|
|
12
12
|
if (options.ws === undefined) options.ws = { ws_prefix: "expressx" }
|
|
13
13
|
|
|
14
14
|
const services = {}
|
|
@@ -19,62 +19,23 @@ function expressX(app, options={}) {
|
|
|
19
19
|
/*
|
|
20
20
|
* create a service `name` based on Prisma table `entity`
|
|
21
21
|
*/
|
|
22
|
-
function createDatabaseService(name, { entity
|
|
22
|
+
function createDatabaseService(name, prismaOptions = { entity: name }) {
|
|
23
|
+
|
|
23
24
|
let prisma = app.get('prisma')
|
|
24
25
|
if (!prisma) {
|
|
25
26
|
prisma = new PrismaClient()
|
|
26
27
|
app.set('prisma', prisma)
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
const service = createService(name, {
|
|
30
|
-
create: (data) => {
|
|
31
|
-
if (options.debug) console.log('create', name, data)
|
|
32
|
-
return prisma[entity].create({
|
|
33
|
-
data,
|
|
34
|
-
})
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
get: (id) => {
|
|
38
|
-
if (options.debug) console.log('get', name, id)
|
|
39
|
-
return prisma[entity].findUnique({
|
|
40
|
-
where: {
|
|
41
|
-
id,
|
|
42
|
-
},
|
|
43
|
-
})
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
patch: (id, data) => {
|
|
47
|
-
if (options.debug) console.log('patch', name, id, data)
|
|
48
|
-
return prisma[entity].update({
|
|
49
|
-
where: {
|
|
50
|
-
id,
|
|
51
|
-
},
|
|
52
|
-
data,
|
|
53
|
-
})
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
remove: (id) => {
|
|
57
|
-
if (options.debug) console.log('remove', name, id)
|
|
58
|
-
return prisma[entity].delete({
|
|
59
|
-
where: {
|
|
60
|
-
id,
|
|
61
|
-
},
|
|
62
|
-
})},
|
|
63
29
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return prisma[entity].findMany(options)
|
|
67
|
-
},
|
|
30
|
+
// take all prisma methods on `entity` table
|
|
31
|
+
const methods = prisma[prismaOptions.entity]
|
|
68
32
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return prisma[entity].upsert(options)
|
|
72
|
-
},
|
|
73
|
-
})
|
|
33
|
+
const service = createService(name, methods)
|
|
34
|
+
|
|
74
35
|
service.prisma = prisma
|
|
75
|
-
service.entity = entity
|
|
36
|
+
service.entity = prismaOptions.entity
|
|
76
37
|
|
|
77
|
-
if (options.debug) console.log(`created service '${name}' over table '${entity}'`)
|
|
38
|
+
if (options.debug) console.log(`created service '${name}' over table '${prismaOptions.entity}'`)
|
|
78
39
|
return service
|
|
79
40
|
}
|
|
80
41
|
|
|
@@ -86,6 +47,7 @@ function expressX(app, options={}) {
|
|
|
86
47
|
|
|
87
48
|
for (const methodName in methods) {
|
|
88
49
|
const method = methods[methodName]
|
|
50
|
+
if (! method instanceof Function) continue
|
|
89
51
|
|
|
90
52
|
// `context` is the context of execution (transport type, connection, app)
|
|
91
53
|
// `args` is the list of arguments of the method
|
|
@@ -151,16 +113,41 @@ function expressX(app, options={}) {
|
|
|
151
113
|
/*
|
|
152
114
|
* add an HTTP REST endpoint at `path`, based on `service`
|
|
153
115
|
*/
|
|
154
|
-
function addHttpRest(path, service) {
|
|
116
|
+
async function addHttpRest(path, service) {
|
|
155
117
|
const context = {
|
|
156
118
|
app,
|
|
157
119
|
http: { name: service.name }
|
|
158
120
|
}
|
|
159
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
|
+
|
|
160
146
|
app.post(path, async (req, res) => {
|
|
161
147
|
context.http.req = req
|
|
162
148
|
try {
|
|
163
|
-
const value = await service.__create(context, req.body)
|
|
149
|
+
const value = await service.__create(context, { data: req.body })
|
|
150
|
+
publish(service, 'create', value)
|
|
164
151
|
res.json(value)
|
|
165
152
|
} catch(err) {
|
|
166
153
|
console.log('callErr', err)
|
|
@@ -171,30 +158,31 @@ function expressX(app, options={}) {
|
|
|
171
158
|
app.get(path, async (req, res) => {
|
|
172
159
|
context.http.req = req
|
|
173
160
|
const query = { ...req.query }
|
|
174
|
-
for (const fieldName in query) {
|
|
175
|
-
const fieldInfo = await service.prisma.$queryRawUnsafe(`
|
|
176
|
-
SELECT column_name, data_type
|
|
177
|
-
FROM information_schema.columns
|
|
178
|
-
WHERE table_name = '${service.entity}' AND column_name = '${fieldName}';
|
|
179
|
-
`)
|
|
180
|
-
const fieldType = fieldInfo[0].data_type
|
|
181
|
-
if (fieldType === 'integer') {
|
|
182
|
-
query[fieldName] = parseInt(query[fieldName])
|
|
183
|
-
} else if (fieldType === 'numeric') {
|
|
184
|
-
query[fieldName] = parseFloat(query[fieldName])
|
|
185
|
-
} else if (fieldType === 'boolean') {
|
|
186
|
-
query[fieldName] = (query[fieldName] === 't') ? true : false
|
|
187
|
-
} else if (fieldType === 'text' || fieldType === 'character varying') {
|
|
188
|
-
query[fieldName] = query[fieldName]
|
|
189
|
-
} else {
|
|
190
|
-
// ?
|
|
191
|
-
query[fieldName] = query[fieldName]
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
161
|
try {
|
|
195
|
-
|
|
162
|
+
// the values in `req.query` are all strings, but Prisma need proper types
|
|
163
|
+
// we need to introspect column types and do the proper transtyping
|
|
164
|
+
for (const fieldName in query) {
|
|
165
|
+
if (!service.fieldTypes) service.fieldTypes = await getFieldTypes()
|
|
166
|
+
const fieldType = service.fieldTypes[fieldName]
|
|
167
|
+
|
|
168
|
+
if (fieldType === 'integer') {
|
|
169
|
+
query[fieldName] = parseInt(query[fieldName])
|
|
170
|
+
} else if (fieldType === 'numeric') {
|
|
171
|
+
query[fieldName] = parseFloat(query[fieldName])
|
|
172
|
+
} else if (fieldType === 'boolean') {
|
|
173
|
+
query[fieldName] = (query[fieldName] === 't') ? true : false
|
|
174
|
+
} else if (fieldType === 'text' || fieldType === 'character varying') {
|
|
175
|
+
query[fieldName] = query[fieldName]
|
|
176
|
+
} else {
|
|
177
|
+
// ?
|
|
178
|
+
query[fieldName] = query[fieldName]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const values = await service.__findMany(context, {
|
|
196
183
|
where: query,
|
|
197
184
|
})
|
|
185
|
+
publish(service, 'findMany', values)
|
|
198
186
|
res.json(values)
|
|
199
187
|
} catch(err) {
|
|
200
188
|
console.log('callErr', err)
|
|
@@ -205,7 +193,12 @@ function expressX(app, options={}) {
|
|
|
205
193
|
app.get(`${path}/:id`, async (req, res) => {
|
|
206
194
|
context.http.req = req
|
|
207
195
|
try {
|
|
208
|
-
const value = await service.
|
|
196
|
+
const value = await service.__findUnique(context, {
|
|
197
|
+
where: {
|
|
198
|
+
id: parseInt(req.params.id)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
publish(service, 'findUnique', value)
|
|
209
202
|
res.json(value)
|
|
210
203
|
} catch(err) {
|
|
211
204
|
console.log('callErr', err)
|
|
@@ -216,7 +209,13 @@ function expressX(app, options={}) {
|
|
|
216
209
|
app.patch(`${path}/:id`, async (req, res) => {
|
|
217
210
|
context.http.req = req
|
|
218
211
|
try {
|
|
219
|
-
const value = await service.
|
|
212
|
+
const value = await service.__update(context, {
|
|
213
|
+
where: {
|
|
214
|
+
id: parseInt(req.params.id),
|
|
215
|
+
},
|
|
216
|
+
data: req.body,
|
|
217
|
+
})
|
|
218
|
+
publish(service, 'update', value)
|
|
220
219
|
res.json(value)
|
|
221
220
|
} catch(err) {
|
|
222
221
|
console.log('callErr', err)
|
|
@@ -227,7 +226,12 @@ function expressX(app, options={}) {
|
|
|
227
226
|
app.delete(`${path}/:id`, async (req, res) => {
|
|
228
227
|
context.http.req = req
|
|
229
228
|
try {
|
|
230
|
-
const value = await service.
|
|
229
|
+
const value = await service.__delete(context, {
|
|
230
|
+
where: {
|
|
231
|
+
id: parseInt(req.params.id)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
publish(service, 'delete', value)
|
|
231
235
|
res.json(value)
|
|
232
236
|
} catch(err) {
|
|
233
237
|
console.log('callErr', err)
|
|
@@ -295,23 +299,7 @@ function expressX(app, options={}) {
|
|
|
295
299
|
result,
|
|
296
300
|
})
|
|
297
301
|
// pub/sub: send event on associated channels
|
|
298
|
-
|
|
299
|
-
if (publishFunc) {
|
|
300
|
-
const channelNames = await publishFunc(result, app)
|
|
301
|
-
if (options.debug) console.log('publish channels', name, action, channelNames)
|
|
302
|
-
for (const channelName of channelNames) {
|
|
303
|
-
if (options.debug) console.log('service-event', name, action, channelName)
|
|
304
|
-
const connectionList = Object.values(connections).filter(cnx => cnx.channelNames.has(channelName))
|
|
305
|
-
for (const connection of connectionList) {
|
|
306
|
-
if (options.debug) console.log('emit to', connection.id, name, action, result)
|
|
307
|
-
connection.socket.emit('service-event', {
|
|
308
|
-
name,
|
|
309
|
-
action,
|
|
310
|
-
result,
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
302
|
+
publish(service, action, result)
|
|
315
303
|
} catch(err) {
|
|
316
304
|
console.log('callErr', err)
|
|
317
305
|
io.emit('client-response', {
|
|
@@ -341,6 +329,28 @@ function expressX(app, options={}) {
|
|
|
341
329
|
})
|
|
342
330
|
}
|
|
343
331
|
|
|
332
|
+
// publish event on associated channels
|
|
333
|
+
async function publish(service, action, result) {
|
|
334
|
+
console.log('PUB!')
|
|
335
|
+
const publishFunc = service.publishCallback
|
|
336
|
+
if (publishFunc) {
|
|
337
|
+
const channelNames = await publishFunc(result, app)
|
|
338
|
+
if (options.debug) console.log('publish channels', service.name, action, channelNames)
|
|
339
|
+
for (const channelName of channelNames) {
|
|
340
|
+
if (options.debug) console.log('service-event', service.name, action, channelName)
|
|
341
|
+
const connectionList = Object.values(connections).filter(cnx => cnx.channelNames.has(channelName))
|
|
342
|
+
for (const connection of connectionList) {
|
|
343
|
+
if (options.debug) console.log('emit to', connection.id, service.name, action, result)
|
|
344
|
+
connection.socket.emit('service-event', {
|
|
345
|
+
name: service.name,
|
|
346
|
+
action,
|
|
347
|
+
result,
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
344
354
|
function joinChannel(channelName, connection) {
|
|
345
355
|
connection.channelNames.add(channelName)
|
|
346
356
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
import expressX from '../src/index.mjs'
|
|
3
|
+
import express from 'express'
|
|
4
|
+
import bodyParser from 'body-parser'
|
|
5
|
+
import axios from 'axios'
|
|
6
|
+
|
|
7
|
+
import { expect, assert } from 'chai'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// `app` is a regular express application, enhanced with express-x features
|
|
11
|
+
const app = expressX(express(), { debug: false })
|
|
12
|
+
|
|
13
|
+
app.createDatabaseService('User')
|
|
14
|
+
app.createDatabaseService('Post')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
describe('ExpressX API', () => {
|
|
19
|
+
|
|
20
|
+
it("can delete all users", async () => {
|
|
21
|
+
const res = await app.service('User').deleteMany()
|
|
22
|
+
assert(res.count === 1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("can create a user", async () => {
|
|
26
|
+
const user = await app.service('User').create({
|
|
27
|
+
data: {
|
|
28
|
+
name: "chris",
|
|
29
|
+
email: 'chris@mail.fr'
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
assert(user.name === 'chris')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("can find a user by name", async () => {
|
|
36
|
+
const users = await app.service('User').findMany({
|
|
37
|
+
where: {
|
|
38
|
+
name: {
|
|
39
|
+
startsWith: "ch"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
assert(users.length > 0)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("can find a unique user by email", async () => {
|
|
47
|
+
const chris = await app.service('User').findUnique({
|
|
48
|
+
where: {
|
|
49
|
+
email: "chris@mail.fr"
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
assert(chris.name === 'chris')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
describe('HTTP/REST API', () => {
|
|
58
|
+
|
|
59
|
+
// add body parsers for http requests
|
|
60
|
+
app.use(bodyParser.json())
|
|
61
|
+
app.use(bodyParser.urlencoded({ extended: false }))
|
|
62
|
+
|
|
63
|
+
// add http/rest endpoints
|
|
64
|
+
app.addHttpRest('/api/user', app.service('User'))
|
|
65
|
+
app.addHttpRest('/api/post', app.service('Post'))
|
|
66
|
+
|
|
67
|
+
app.server.listen(8008, () => console.log(`App listening at http://localhost:8008`))
|
|
68
|
+
|
|
69
|
+
let chris
|
|
70
|
+
|
|
71
|
+
it("can create a user", async () => {
|
|
72
|
+
const res = await axios.post('http://localhost:8008/api/user', {
|
|
73
|
+
name: "carole",
|
|
74
|
+
email: 'carole@mail.fr'
|
|
75
|
+
})
|
|
76
|
+
assert(res?.data?.name === 'carole')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("can find users", async () => {
|
|
80
|
+
const res = await axios.get('http://localhost:8008/api/user')
|
|
81
|
+
assert(res?.data?.length > 0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("can find a user by name", async () => {
|
|
85
|
+
const res = await axios.get('http://localhost:8008/api/user?name=chris')
|
|
86
|
+
assert(res?.data?.length > 0)
|
|
87
|
+
chris = res.data[0]
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("can find a user by id", async () => {
|
|
91
|
+
const res = await axios.get(`http://localhost:8008/api/user/${chris.id}`)
|
|
92
|
+
assert(res?.data?.id === chris.id)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("can patch a user", async () => {
|
|
96
|
+
const res = await axios.patch(`http://localhost:8008/api/user/${chris.id}`, {
|
|
97
|
+
name: "Christophe",
|
|
98
|
+
})
|
|
99
|
+
assert(res?.data?.name === "Christophe")
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("can delete a user", async () => {
|
|
103
|
+
const res = await axios.delete(`http://localhost:8008/api/user/${chris.id}`)
|
|
104
|
+
assert(res?.data?.name === "Christophe")
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("can stop server", () => {
|
|
108
|
+
app.server.close()
|
|
109
|
+
})
|
|
110
|
+
})
|