@jcbuisson/express-x 1.1.0 → 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 +71 -74
- 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.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)
|
|
@@ -171,28 +157,26 @@ function expressX(app, options={}) {
|
|
|
171
157
|
app.get(path, async (req, res) => {
|
|
172
158
|
context.http.req = req
|
|
173
159
|
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
160
|
try {
|
|
195
|
-
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, {
|
|
196
180
|
where: query,
|
|
197
181
|
})
|
|
198
182
|
res.json(values)
|
|
@@ -205,7 +189,11 @@ function expressX(app, options={}) {
|
|
|
205
189
|
app.get(`${path}/:id`, async (req, res) => {
|
|
206
190
|
context.http.req = req
|
|
207
191
|
try {
|
|
208
|
-
const value = await service.
|
|
192
|
+
const value = await service.__findUnique(context, {
|
|
193
|
+
where: {
|
|
194
|
+
id: parseInt(req.params.id)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
209
197
|
res.json(value)
|
|
210
198
|
} catch(err) {
|
|
211
199
|
console.log('callErr', err)
|
|
@@ -216,7 +204,12 @@ function expressX(app, options={}) {
|
|
|
216
204
|
app.patch(`${path}/:id`, async (req, res) => {
|
|
217
205
|
context.http.req = req
|
|
218
206
|
try {
|
|
219
|
-
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
|
+
})
|
|
220
213
|
res.json(value)
|
|
221
214
|
} catch(err) {
|
|
222
215
|
console.log('callErr', err)
|
|
@@ -227,7 +220,11 @@ function expressX(app, options={}) {
|
|
|
227
220
|
app.delete(`${path}/:id`, async (req, res) => {
|
|
228
221
|
context.http.req = req
|
|
229
222
|
try {
|
|
230
|
-
const value = await service.
|
|
223
|
+
const value = await service.__delete(context, {
|
|
224
|
+
where: {
|
|
225
|
+
id: parseInt(req.params.id)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
231
228
|
res.json(value)
|
|
232
229
|
} catch(err) {
|
|
233
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
|
+
})
|