@jcbuisson/express-x 1.1.2 → 1.1.4

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 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,12 +1,12 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git@gitlab.com:buisson31/express-x.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",
@@ -15,9 +15,10 @@
15
15
  "test": "mocha --require esm 'test/**/*.test.js'",
16
16
  "generate": "npx prisma generate",
17
17
  "migrate": "npx prisma migrate dev --name init"
18
- },
18
+ },
19
19
  "keywords": [],
20
20
  "dependencies": {
21
+ "@jcbuisson/express-x-client": "^1.0.10",
21
22
  "@prisma/client": "^4.10.1",
22
23
  "axios": "^1.4.0",
23
24
  "config": "^3.3.9",
package/prisma/dev.db CHANGED
Binary file
package/src/index.mjs CHANGED
@@ -144,9 +144,11 @@ function expressX(app, options={}) {
144
144
 
145
145
 
146
146
  app.post(path, async (req, res) => {
147
+ if (options.debug) console.log("http request POST", req)
147
148
  context.http.req = req
148
149
  try {
149
150
  const value = await service.__create(context, { data: req.body })
151
+ publish(service, 'create', value)
150
152
  res.json(value)
151
153
  } catch(err) {
152
154
  console.log('callErr', err)
@@ -155,9 +157,12 @@ function expressX(app, options={}) {
155
157
  })
156
158
 
157
159
  app.get(path, async (req, res) => {
160
+ if (options.debug) console.log("http request GET", req)
158
161
  context.http.req = req
159
162
  const query = { ...req.query }
160
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
161
166
  for (const fieldName in query) {
162
167
  if (!service.fieldTypes) service.fieldTypes = await getFieldTypes()
163
168
  const fieldType = service.fieldTypes[fieldName]
@@ -179,6 +184,7 @@ function expressX(app, options={}) {
179
184
  const values = await service.__findMany(context, {
180
185
  where: query,
181
186
  })
187
+ publish(service, 'findMany', values)
182
188
  res.json(values)
183
189
  } catch(err) {
184
190
  console.log('callErr', err)
@@ -187,6 +193,7 @@ function expressX(app, options={}) {
187
193
  })
188
194
 
189
195
  app.get(`${path}/:id`, async (req, res) => {
196
+ if (options.debug) console.log("http request GET", req)
190
197
  context.http.req = req
191
198
  try {
192
199
  const value = await service.__findUnique(context, {
@@ -194,6 +201,7 @@ function expressX(app, options={}) {
194
201
  id: parseInt(req.params.id)
195
202
  }
196
203
  })
204
+ publish(service, 'findUnique', value)
197
205
  res.json(value)
198
206
  } catch(err) {
199
207
  console.log('callErr', err)
@@ -202,6 +210,7 @@ function expressX(app, options={}) {
202
210
  })
203
211
 
204
212
  app.patch(`${path}/:id`, async (req, res) => {
213
+ if (options.debug) console.log("http request PATCH", req)
205
214
  context.http.req = req
206
215
  try {
207
216
  const value = await service.__update(context, {
@@ -210,6 +219,7 @@ function expressX(app, options={}) {
210
219
  },
211
220
  data: req.body,
212
221
  })
222
+ publish(service, 'update', value)
213
223
  res.json(value)
214
224
  } catch(err) {
215
225
  console.log('callErr', err)
@@ -218,6 +228,7 @@ function expressX(app, options={}) {
218
228
  })
219
229
 
220
230
  app.delete(`${path}/:id`, async (req, res) => {
231
+ if (options.debug) console.log("http request DELETE", req)
221
232
  context.http.req = req
222
233
  try {
223
234
  const value = await service.__delete(context, {
@@ -225,6 +236,7 @@ function expressX(app, options={}) {
225
236
  id: parseInt(req.params.id)
226
237
  }
227
238
  })
239
+ publish(service, 'delete', value)
228
240
  res.json(value)
229
241
  } catch(err) {
230
242
  console.log('callErr', err)
@@ -292,23 +304,7 @@ function expressX(app, options={}) {
292
304
  result,
293
305
  })
294
306
  // pub/sub: send event on associated channels
295
- const publishFunc = service.publishCallback
296
- if (publishFunc) {
297
- const channelNames = await publishFunc(result, app)
298
- if (options.debug) console.log('publish channels', name, action, channelNames)
299
- for (const channelName of channelNames) {
300
- if (options.debug) console.log('service-event', name, action, channelName)
301
- const connectionList = Object.values(connections).filter(cnx => cnx.channelNames.has(channelName))
302
- for (const connection of connectionList) {
303
- if (options.debug) console.log('emit to', connection.id, name, action, result)
304
- connection.socket.emit('service-event', {
305
- name,
306
- action,
307
- result,
308
- })
309
- }
310
- }
311
- }
307
+ publish(service, action, result)
312
308
  } catch(err) {
313
309
  console.log('callErr', err)
314
310
  io.emit('client-response', {
@@ -338,6 +334,27 @@ function expressX(app, options={}) {
338
334
  })
339
335
  }
340
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
+
341
358
  function joinChannel(channelName, connection) {
342
359
  connection.channelNames.add(channelName)
343
360
  }
@@ -1,11 +1,15 @@
1
1
 
2
- import expressX from '../src/index.mjs'
3
2
  import express from 'express'
4
3
  import bodyParser from 'body-parser'
5
4
  import axios from 'axios'
5
+ import io from 'socket.io-client'
6
+ import expressxClient from '@jcbuisson/express-x-client'
7
+
6
8
 
7
9
  import { expect, assert } from 'chai'
8
10
 
11
+ import expressX from '../src/index.mjs'
12
+
9
13
 
10
14
  // `app` is a regular express application, enhanced with express-x features
11
15
  const app = expressX(express(), { debug: false })
@@ -15,11 +19,11 @@ app.createDatabaseService('Post')
15
19
 
16
20
 
17
21
 
18
- describe('ExpressX API', () => {
22
+ describe('ExpressX API (no running server)', () => {
19
23
 
20
24
  it("can delete all users", async () => {
21
25
  const res = await app.service('User').deleteMany()
22
- assert('chris' === 'chris')
26
+ assert(res.count === 1)
23
27
  })
24
28
 
25
29
  it("can create a user", async () => {
@@ -56,17 +60,19 @@ describe('ExpressX API', () => {
56
60
 
57
61
  describe('HTTP/REST API', () => {
58
62
 
59
- // add body parsers for http requests
60
- app.use(bodyParser.json())
61
- app.use(bodyParser.urlencoded({ extended: false }))
63
+ let chris
62
64
 
63
- // add http/rest endpoints
64
- app.addHttpRest('/api/user', app.service('User'))
65
- app.addHttpRest('/api/post', app.service('Post'))
65
+ before(() => {
66
+ // add body parsers for http requests
67
+ app.use(bodyParser.json())
68
+ app.use(bodyParser.urlencoded({ extended: false }))
66
69
 
67
- app.server.listen(8008, () => console.log(`App listening at http://localhost:8008`))
70
+ // add http/rest endpoints
71
+ app.addHttpRest('/api/user', app.service('User'))
72
+ app.addHttpRest('/api/post', app.service('Post'))
68
73
 
69
- let chris
74
+ app.server.listen(8008, () => console.log(`App listening at http://localhost:8008`))
75
+ })
70
76
 
71
77
  it("can create a user", async () => {
72
78
  const res = await axios.post('http://localhost:8008/api/user', {
@@ -104,7 +110,36 @@ describe('HTTP/REST API', () => {
104
110
  assert(res?.data?.name === "Christophe")
105
111
  })
106
112
 
107
- it("can stop server", () => {
108
- app.server.close()
113
+ after(async () => {
114
+ await app.server.close()
115
+ })
116
+ })
117
+
118
+
119
+ // test compatibility with `express-x-client`
120
+ describe('Client API', () => {
121
+
122
+ let clientApp, socket
123
+
124
+ before(() => {
125
+ app.server.listen(8008, () => console.log(`App listening at http://localhost:8008`))
126
+
127
+ socket = io('http://localhost:8008', { transports: ["websocket"] })
128
+ clientApp = expressxClient(socket)
129
+ })
130
+
131
+ it("can create a user", async () => {
132
+ const user = await clientApp.service('User').create({
133
+ data: {
134
+ name: "chris",
135
+ email: 'chris@mail.fr'
136
+ },
137
+ })
138
+ assert(user.name === 'chris')
139
+ })
140
+
141
+ after(async () => {
142
+ await socket.close()
143
+ await app.server.close()
109
144
  })
110
145
  })