@jcbuisson/express-x 1.0.22 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -3
- package/prisma/dev.db +0 -0
- package/prisma/schema.prisma +22 -0
- package/src/index.mjs +74 -54
- package/test/index.test.js +110 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jcbuisson/express-x",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
@@ -12,15 +12,21 @@
|
|
|
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,40 @@ 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 })
|
|
164
150
|
res.json(value)
|
|
165
151
|
} catch(err) {
|
|
166
152
|
console.log('callErr', err)
|
|
@@ -170,8 +156,29 @@ function expressX(app, options={}) {
|
|
|
170
156
|
|
|
171
157
|
app.get(path, async (req, res) => {
|
|
172
158
|
context.http.req = req
|
|
159
|
+
const query = { ...req.query }
|
|
173
160
|
try {
|
|
174
|
-
const
|
|
161
|
+
for (const fieldName in query) {
|
|
162
|
+
if (!service.fieldTypes) service.fieldTypes = await getFieldTypes()
|
|
163
|
+
const fieldType = service.fieldTypes[fieldName]
|
|
164
|
+
|
|
165
|
+
if (fieldType === 'integer') {
|
|
166
|
+
query[fieldName] = parseInt(query[fieldName])
|
|
167
|
+
} else if (fieldType === 'numeric') {
|
|
168
|
+
query[fieldName] = parseFloat(query[fieldName])
|
|
169
|
+
} else if (fieldType === 'boolean') {
|
|
170
|
+
query[fieldName] = (query[fieldName] === 't') ? true : false
|
|
171
|
+
} else if (fieldType === 'text' || fieldType === 'character varying') {
|
|
172
|
+
query[fieldName] = query[fieldName]
|
|
173
|
+
} else {
|
|
174
|
+
// ?
|
|
175
|
+
query[fieldName] = query[fieldName]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const values = await service.__findMany(context, {
|
|
180
|
+
where: query,
|
|
181
|
+
})
|
|
175
182
|
res.json(values)
|
|
176
183
|
} catch(err) {
|
|
177
184
|
console.log('callErr', err)
|
|
@@ -182,7 +189,11 @@ function expressX(app, options={}) {
|
|
|
182
189
|
app.get(`${path}/:id`, async (req, res) => {
|
|
183
190
|
context.http.req = req
|
|
184
191
|
try {
|
|
185
|
-
const value = await service.
|
|
192
|
+
const value = await service.__findUnique(context, {
|
|
193
|
+
where: {
|
|
194
|
+
id: parseInt(req.params.id)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
186
197
|
res.json(value)
|
|
187
198
|
} catch(err) {
|
|
188
199
|
console.log('callErr', err)
|
|
@@ -193,7 +204,12 @@ function expressX(app, options={}) {
|
|
|
193
204
|
app.patch(`${path}/:id`, async (req, res) => {
|
|
194
205
|
context.http.req = req
|
|
195
206
|
try {
|
|
196
|
-
const value = await service.
|
|
207
|
+
const value = await service.__update(context, {
|
|
208
|
+
where: {
|
|
209
|
+
id: parseInt(req.params.id),
|
|
210
|
+
},
|
|
211
|
+
data: req.body,
|
|
212
|
+
})
|
|
197
213
|
res.json(value)
|
|
198
214
|
} catch(err) {
|
|
199
215
|
console.log('callErr', err)
|
|
@@ -204,7 +220,11 @@ function expressX(app, options={}) {
|
|
|
204
220
|
app.delete(`${path}/:id`, async (req, res) => {
|
|
205
221
|
context.http.req = req
|
|
206
222
|
try {
|
|
207
|
-
const value = await service.
|
|
223
|
+
const value = await service.__delete(context, {
|
|
224
|
+
where: {
|
|
225
|
+
id: parseInt(req.params.id)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
208
228
|
res.json(value)
|
|
209
229
|
} catch(err) {
|
|
210
230
|
console.log('callErr', err)
|
|
@@ -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('chris' === 'chris')
|
|
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
|
+
})
|