@anthonylzq/simba.js 4.3.0 → 5.0.0
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 +197 -20
- package/lib/index.js +17 -11
- package/lib/src/functions/api/database.js +177 -0
- package/lib/src/functions/api/index.js +230 -0
- package/lib/src/functions/api/network.js +2536 -0
- package/lib/src/functions/api/schemas.js +122 -0
- package/lib/src/functions/api/services.js +220 -0
- package/lib/src/functions/api/types.js +110 -0
- package/lib/src/functions/api/utils.js +466 -0
- package/lib/src/index.js +21 -7
- package/lib/src/utils/writeFile.js +2 -2
- package/package.json +19 -4
- package/lib/src/functions/api.js +0 -1983
|
@@ -0,0 +1,2536 @@
|
|
|
1
|
+
const { platform } = require('os')
|
|
2
|
+
const { promisify } = require('util')
|
|
3
|
+
const exec = promisify(require('child_process').exec)
|
|
4
|
+
const writeFile = require('../../utils/writeFile')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} args
|
|
8
|
+
* @param {String} args.projectName
|
|
9
|
+
*/
|
|
10
|
+
const expressRestNetwork = async ({ projectName }) => {
|
|
11
|
+
const createFoldersCommand = `mkdir ${projectName}/src/network/routes/utils`
|
|
12
|
+
|
|
13
|
+
if (platform() === 'win32')
|
|
14
|
+
await exec(createFoldersCommand.replaceAll('/', '\\'))
|
|
15
|
+
else await exec(createFoldersCommand)
|
|
16
|
+
|
|
17
|
+
const network = {
|
|
18
|
+
response: {
|
|
19
|
+
content: `interface ResponseProps {
|
|
20
|
+
error: boolean
|
|
21
|
+
message: unknown
|
|
22
|
+
res: CustomResponse
|
|
23
|
+
status: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const response = ({ error, message, res, status }: ResponseProps): void => {
|
|
27
|
+
res.status(status).send({ error, message })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { response }
|
|
31
|
+
`,
|
|
32
|
+
file: `${projectName}/src/network/response.ts`
|
|
33
|
+
},
|
|
34
|
+
router: {
|
|
35
|
+
content: `import { Application, Response, Request, Router, NextFunction } from 'express'
|
|
36
|
+
import swaggerUi from 'swagger-ui-express'
|
|
37
|
+
import httpErrors from 'http-errors'
|
|
38
|
+
|
|
39
|
+
import { response } from './response'
|
|
40
|
+
import { Home, User } from './routes'
|
|
41
|
+
import { docs } from 'utils'
|
|
42
|
+
|
|
43
|
+
const routers = [User]
|
|
44
|
+
const applyRoutes = (app: Application): void => {
|
|
45
|
+
app.use('/', Home)
|
|
46
|
+
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(docs))
|
|
47
|
+
routers.forEach((router: Router): Application => app.use('/api', router))
|
|
48
|
+
|
|
49
|
+
// Handling 404 error
|
|
50
|
+
app.use((req, res, next) => {
|
|
51
|
+
next(new httpErrors.NotFound('This route does not exists'))
|
|
52
|
+
})
|
|
53
|
+
app.use(
|
|
54
|
+
(
|
|
55
|
+
error: httpErrors.HttpError,
|
|
56
|
+
req: Request,
|
|
57
|
+
res: Response,
|
|
58
|
+
next: NextFunction
|
|
59
|
+
) => {
|
|
60
|
+
response({
|
|
61
|
+
error: true,
|
|
62
|
+
message: error.message,
|
|
63
|
+
res,
|
|
64
|
+
status: error.status
|
|
65
|
+
})
|
|
66
|
+
next()
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { applyRoutes }
|
|
72
|
+
`,
|
|
73
|
+
file: `${projectName}/src/network/router.ts`
|
|
74
|
+
},
|
|
75
|
+
server: {
|
|
76
|
+
content: `import express from 'express'
|
|
77
|
+
import mongoose from 'mongoose'
|
|
78
|
+
import cors from 'cors'
|
|
79
|
+
import pino, { HttpLogger } from 'express-pino-logger'
|
|
80
|
+
|
|
81
|
+
import { applyRoutes } from './router'
|
|
82
|
+
|
|
83
|
+
const PORT = (process.env.PORT as string) || 1996
|
|
84
|
+
|
|
85
|
+
class Server {
|
|
86
|
+
#app: express.Application
|
|
87
|
+
#connection: mongoose.Connection | undefined
|
|
88
|
+
#log: HttpLogger
|
|
89
|
+
|
|
90
|
+
constructor() {
|
|
91
|
+
this.#app = express()
|
|
92
|
+
this.#log = pino({
|
|
93
|
+
transport: {
|
|
94
|
+
target: 'pino-pretty',
|
|
95
|
+
options: {
|
|
96
|
+
colorize: true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
this.#config()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#config() {
|
|
104
|
+
this.#app.use(cors())
|
|
105
|
+
this.#app.use(express.json())
|
|
106
|
+
this.#app.use(express.urlencoded({ extended: false }))
|
|
107
|
+
this.#app.use(
|
|
108
|
+
(
|
|
109
|
+
req: express.Request,
|
|
110
|
+
res: express.Response,
|
|
111
|
+
next: express.NextFunction
|
|
112
|
+
) => {
|
|
113
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
|
|
114
|
+
res.header('Access-Control-Allow-Origin', '*')
|
|
115
|
+
res.header(
|
|
116
|
+
'Access-Control-Allow-Headers',
|
|
117
|
+
'Authorization, Content-Type'
|
|
118
|
+
)
|
|
119
|
+
res.header('x-powered-by', 'Simba.js')
|
|
120
|
+
next()
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
applyRoutes(this.#app)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async #mongo(): Promise<void> {
|
|
127
|
+
this.#connection = mongoose.connection
|
|
128
|
+
const connection = {
|
|
129
|
+
keepAlive: true,
|
|
130
|
+
useNewUrlParser: true,
|
|
131
|
+
useUnifiedTopology: true
|
|
132
|
+
}
|
|
133
|
+
this.#connection.on('connected', () => {
|
|
134
|
+
this.#log.logger.info('Mongo connection established.')
|
|
135
|
+
})
|
|
136
|
+
this.#connection.on('reconnected', () => {
|
|
137
|
+
this.#log.logger.info('Mongo connection reestablished')
|
|
138
|
+
})
|
|
139
|
+
this.#connection.on('disconnected', () => {
|
|
140
|
+
this.#log.logger.info('Mongo connection disconnected')
|
|
141
|
+
this.#log.logger.info('Trying to reconnected to Mongo...')
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
mongoose.connect(process.env.MONGO_URI as string, {
|
|
144
|
+
...connection,
|
|
145
|
+
connectTimeoutMS: 3000,
|
|
146
|
+
socketTimeoutMS: 3000
|
|
147
|
+
})
|
|
148
|
+
}, 3000)
|
|
149
|
+
})
|
|
150
|
+
this.#connection.on('close', () => {
|
|
151
|
+
this.#log.logger.info('Mongo connection closed')
|
|
152
|
+
})
|
|
153
|
+
this.#connection.on('error', (e: Error) => {
|
|
154
|
+
this.#log.logger.info('Mongo connection error:')
|
|
155
|
+
this.#log.logger.error(e)
|
|
156
|
+
})
|
|
157
|
+
await mongoose.connect(process.env.MONGO_URI as string, connection)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public start(): void {
|
|
161
|
+
this.#app.listen(PORT, () => {
|
|
162
|
+
this.#log.logger.info(\`Server running at port \${PORT}\`)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
this.#mongo()
|
|
167
|
+
} catch (e) {
|
|
168
|
+
this.#log.logger.error(e)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const server = new Server()
|
|
174
|
+
|
|
175
|
+
export { server as Server }
|
|
176
|
+
`,
|
|
177
|
+
file: `${projectName}/src/network/server.ts`
|
|
178
|
+
},
|
|
179
|
+
routes: {
|
|
180
|
+
home: {
|
|
181
|
+
content: `import { Response, Request, Router } from 'express'
|
|
182
|
+
|
|
183
|
+
import { response } from 'network/response'
|
|
184
|
+
|
|
185
|
+
const Home = Router()
|
|
186
|
+
|
|
187
|
+
Home.route('').get((req: Request, res: Response) => {
|
|
188
|
+
response({
|
|
189
|
+
error: false,
|
|
190
|
+
message: 'Welcome to your Express Backend!',
|
|
191
|
+
res,
|
|
192
|
+
status: 200
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
export { Home }
|
|
197
|
+
`,
|
|
198
|
+
file: `${projectName}/src/network/routes/home.ts`
|
|
199
|
+
},
|
|
200
|
+
index: {
|
|
201
|
+
content: `export * from './home'
|
|
202
|
+
export * from './user'
|
|
203
|
+
`,
|
|
204
|
+
file: `${projectName}/src/network/routes/index.ts`
|
|
205
|
+
},
|
|
206
|
+
user: {
|
|
207
|
+
content: `import { NextFunction, Router } from 'express'
|
|
208
|
+
|
|
209
|
+
import { response } from 'network/response'
|
|
210
|
+
import { UserService } from 'services'
|
|
211
|
+
import { idSchema, storeUserSchema, UserDTO } from 'schemas'
|
|
212
|
+
import { validatorCompiler } from './utils'
|
|
213
|
+
|
|
214
|
+
const User = Router()
|
|
215
|
+
|
|
216
|
+
User.route('/users')
|
|
217
|
+
.post(
|
|
218
|
+
validatorCompiler(storeUserSchema, 'body'),
|
|
219
|
+
async (
|
|
220
|
+
req: CustomRequest,
|
|
221
|
+
res: CustomResponse,
|
|
222
|
+
next: NextFunction
|
|
223
|
+
): Promise<void> => {
|
|
224
|
+
try {
|
|
225
|
+
const {
|
|
226
|
+
body: { args }
|
|
227
|
+
} = req
|
|
228
|
+
const us = new UserService({ userDtoWithoutId: args })
|
|
229
|
+
const result = await us.process({ type: 'store' })
|
|
230
|
+
|
|
231
|
+
response({ error: false, message: result, res, status: 201 })
|
|
232
|
+
} catch (error) {
|
|
233
|
+
next(error)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
.get(
|
|
238
|
+
async (
|
|
239
|
+
req: CustomRequest,
|
|
240
|
+
res: CustomResponse,
|
|
241
|
+
next: NextFunction
|
|
242
|
+
): Promise<void> => {
|
|
243
|
+
try {
|
|
244
|
+
const us = new UserService()
|
|
245
|
+
const result = await us.process({ type: 'getAll' })
|
|
246
|
+
|
|
247
|
+
response({ error: false, message: result, res, status: 200 })
|
|
248
|
+
} catch (error) {
|
|
249
|
+
next(error)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
.delete(
|
|
254
|
+
async (
|
|
255
|
+
req: CustomRequest,
|
|
256
|
+
res: CustomResponse,
|
|
257
|
+
next: NextFunction
|
|
258
|
+
): Promise<void> => {
|
|
259
|
+
try {
|
|
260
|
+
const us = new UserService()
|
|
261
|
+
const result = await us.process({ type: 'deleteAll' })
|
|
262
|
+
|
|
263
|
+
response({ error: false, message: result, res, status: 200 })
|
|
264
|
+
} catch (error) {
|
|
265
|
+
next(error)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
User.route('/user/:id')
|
|
271
|
+
.get(
|
|
272
|
+
validatorCompiler(idSchema, 'params'),
|
|
273
|
+
async (
|
|
274
|
+
req: CustomRequest,
|
|
275
|
+
res: CustomResponse,
|
|
276
|
+
next: NextFunction
|
|
277
|
+
): Promise<void> => {
|
|
278
|
+
try {
|
|
279
|
+
const {
|
|
280
|
+
params: { id }
|
|
281
|
+
} = req
|
|
282
|
+
const us = new UserService({ id })
|
|
283
|
+
const result = await us.process({ type: 'getOne' })
|
|
284
|
+
|
|
285
|
+
response({ error: false, message: result, res, status: 200 })
|
|
286
|
+
} catch (error) {
|
|
287
|
+
next(error)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
.patch(
|
|
292
|
+
validatorCompiler(idSchema, 'params'),
|
|
293
|
+
validatorCompiler(storeUserSchema, 'body'),
|
|
294
|
+
async (
|
|
295
|
+
req: CustomRequest,
|
|
296
|
+
res: CustomResponse,
|
|
297
|
+
next: NextFunction
|
|
298
|
+
): Promise<void> => {
|
|
299
|
+
try {
|
|
300
|
+
const {
|
|
301
|
+
body: { args },
|
|
302
|
+
params: { id }
|
|
303
|
+
} = req
|
|
304
|
+
const userDto = {
|
|
305
|
+
id,
|
|
306
|
+
...args
|
|
307
|
+
} as UserDTO
|
|
308
|
+
const us = new UserService({ userDto })
|
|
309
|
+
const result = await us.process({ type: 'update' })
|
|
310
|
+
|
|
311
|
+
response({ error: false, message: result, res, status: 200 })
|
|
312
|
+
} catch (error) {
|
|
313
|
+
next(error)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
.delete(
|
|
318
|
+
validatorCompiler(idSchema, 'params'),
|
|
319
|
+
async (
|
|
320
|
+
req: CustomRequest,
|
|
321
|
+
res: CustomResponse,
|
|
322
|
+
next: NextFunction
|
|
323
|
+
): Promise<void> => {
|
|
324
|
+
try {
|
|
325
|
+
const {
|
|
326
|
+
params: { id }
|
|
327
|
+
} = req
|
|
328
|
+
const us = new UserService({ id })
|
|
329
|
+
const result = await us.process({ type: 'delete' })
|
|
330
|
+
|
|
331
|
+
response({ error: false, message: result, res, status: 200 })
|
|
332
|
+
} catch (error) {
|
|
333
|
+
next(error)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
export { User }
|
|
339
|
+
`,
|
|
340
|
+
file: `${projectName}/src/network/routes/user.ts`
|
|
341
|
+
},
|
|
342
|
+
utils: {
|
|
343
|
+
index: {
|
|
344
|
+
content: `import { NextFunction } from 'express'
|
|
345
|
+
import httpErrors from 'http-errors'
|
|
346
|
+
import { TObject, TProperties } from '@sinclair/typebox'
|
|
347
|
+
import Ajv from 'ajv'
|
|
348
|
+
|
|
349
|
+
const ajv = new Ajv({
|
|
350
|
+
removeAdditional: true,
|
|
351
|
+
useDefaults: true,
|
|
352
|
+
coerceTypes: true,
|
|
353
|
+
nullable: true
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
type Middleware = (
|
|
357
|
+
req: CustomRequest,
|
|
358
|
+
res: CustomResponse,
|
|
359
|
+
next: NextFunction
|
|
360
|
+
) => void
|
|
361
|
+
|
|
362
|
+
const validatorCompiler = <T extends TProperties>(
|
|
363
|
+
schema: TObject<T>,
|
|
364
|
+
value: 'body' | 'params'
|
|
365
|
+
): Middleware => {
|
|
366
|
+
return (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
|
|
367
|
+
const validate = ajv.compile(schema)
|
|
368
|
+
const ok = validate(req[value])
|
|
369
|
+
|
|
370
|
+
if (!ok && validate.errors) {
|
|
371
|
+
const [error] = validate.errors
|
|
372
|
+
const errorMessage = \`\${error.dataPath.replace('.', '')} \${error.message}\`
|
|
373
|
+
|
|
374
|
+
return next(new httpErrors.UnprocessableEntity(errorMessage))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
next()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { validatorCompiler }
|
|
382
|
+
`,
|
|
383
|
+
file: `${projectName}/src/network/routes/utils/index.ts`
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
await Promise.all([
|
|
390
|
+
writeFile(network.response.file, network.response.content),
|
|
391
|
+
writeFile(network.router.file, network.router.content),
|
|
392
|
+
writeFile(network.server.file, network.server.content),
|
|
393
|
+
writeFile(network.routes.home.file, network.routes.home.content),
|
|
394
|
+
writeFile(network.routes.index.file, network.routes.index.content),
|
|
395
|
+
writeFile(network.routes.user.file, network.routes.user.content),
|
|
396
|
+
writeFile(
|
|
397
|
+
network.routes.utils.index.file,
|
|
398
|
+
network.routes.utils.index.content
|
|
399
|
+
)
|
|
400
|
+
])
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* @param {Object} args
|
|
405
|
+
* @param {String} args.projectName
|
|
406
|
+
*/
|
|
407
|
+
const expressGraphQLNetwork = async ({ projectName }) => {
|
|
408
|
+
const createFoldersCommand = `mkdir ${projectName}/src/network/routes/utils \
|
|
409
|
+
${projectName}/src/graphQL/models \
|
|
410
|
+
${projectName}/src/graphQL/models/User \
|
|
411
|
+
${projectName}/src/graphQL/models/utils \
|
|
412
|
+
${projectName}/src/graphQL/models/utils/messages`
|
|
413
|
+
|
|
414
|
+
if (platform() === 'win32')
|
|
415
|
+
await exec(createFoldersCommand.replaceAll('/', '\\'))
|
|
416
|
+
else await exec(createFoldersCommand)
|
|
417
|
+
|
|
418
|
+
const graphQL = {
|
|
419
|
+
index: {
|
|
420
|
+
content: "export * from './models'\n",
|
|
421
|
+
file: `${projectName}/src/graphQL/index.ts`
|
|
422
|
+
},
|
|
423
|
+
models: {
|
|
424
|
+
User: {
|
|
425
|
+
index: {
|
|
426
|
+
content: `import { makeExecutableSchema } from '@graphql-tools/schema'
|
|
427
|
+
|
|
428
|
+
import { User as UserTD } from './typeDefs'
|
|
429
|
+
import { Query } from './queriesResolver'
|
|
430
|
+
import { Mutation } from './mutationsResolver'
|
|
431
|
+
|
|
432
|
+
const resolvers = {
|
|
433
|
+
Query,
|
|
434
|
+
Mutation
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const User = makeExecutableSchema({
|
|
438
|
+
typeDefs: UserTD,
|
|
439
|
+
resolvers
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
export { User }
|
|
443
|
+
`,
|
|
444
|
+
file: `${projectName}/src/graphQL/models/User/index.ts`
|
|
445
|
+
},
|
|
446
|
+
mutations: {
|
|
447
|
+
content: `import { ApolloError } from 'apollo-server-core'
|
|
448
|
+
|
|
449
|
+
import { store, remove, update } from 'database'
|
|
450
|
+
import { UserDTO } from 'schemas'
|
|
451
|
+
import { EFU, MFU, GE, errorHandling } from '../utils'
|
|
452
|
+
|
|
453
|
+
const storeUser = async (
|
|
454
|
+
{ user }: { user: UserDTO },
|
|
455
|
+
{ log }: Context
|
|
456
|
+
): Promise<UserDTO> => {
|
|
457
|
+
try {
|
|
458
|
+
const result = await store(user)
|
|
459
|
+
|
|
460
|
+
return result
|
|
461
|
+
} catch (e) {
|
|
462
|
+
return errorHandling({
|
|
463
|
+
e,
|
|
464
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
465
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
466
|
+
log
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const deleteAllUsers = async ({ log }: Context): Promise<string> => {
|
|
472
|
+
try {
|
|
473
|
+
const usersDeleted = (await remove()) as number
|
|
474
|
+
|
|
475
|
+
if (usersDeleted >= 1) return MFU.ALL_USERS_DELETED
|
|
476
|
+
|
|
477
|
+
if (usersDeleted === 0)
|
|
478
|
+
throw new ApolloError(EFU.NOTHING_TO_DELETE, 'NOTHING_TO_DELETE')
|
|
479
|
+
|
|
480
|
+
throw new ApolloError(GE.INTERNAL_SERVER_ERROR, 'INTERNAL_SERVER_ERROR')
|
|
481
|
+
} catch (e) {
|
|
482
|
+
return errorHandling({
|
|
483
|
+
e,
|
|
484
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
485
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
486
|
+
log
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const updateUser = async (
|
|
492
|
+
{ user }: { user: UserDTO },
|
|
493
|
+
{ log }: Context
|
|
494
|
+
): Promise<UserDTO> => {
|
|
495
|
+
try {
|
|
496
|
+
const updatedUser = await update(user)
|
|
497
|
+
|
|
498
|
+
if (!updatedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
|
|
499
|
+
|
|
500
|
+
return updatedUser
|
|
501
|
+
} catch (e) {
|
|
502
|
+
return errorHandling({
|
|
503
|
+
e,
|
|
504
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
505
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
506
|
+
log
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const deleteUser = async (
|
|
512
|
+
{ id }: { id: string },
|
|
513
|
+
{ log }: Context
|
|
514
|
+
): Promise<string> => {
|
|
515
|
+
try {
|
|
516
|
+
const deletedUser = await remove(id)
|
|
517
|
+
|
|
518
|
+
if (!deletedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
|
|
519
|
+
|
|
520
|
+
return MFU.USER_DELETED
|
|
521
|
+
} catch (e) {
|
|
522
|
+
return errorHandling({
|
|
523
|
+
e,
|
|
524
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
525
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
526
|
+
log
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export { storeUser, deleteAllUsers, updateUser, deleteUser }
|
|
532
|
+
`,
|
|
533
|
+
file: `${projectName}/src/graphQL/models/User/mutations.ts`
|
|
534
|
+
},
|
|
535
|
+
mutationsResolver: {
|
|
536
|
+
content: `import { DefinedError } from 'ajv'
|
|
537
|
+
import { ApolloError } from 'apollo-server-core'
|
|
538
|
+
|
|
539
|
+
import { ajv, idSchema, UserDTO } from 'schemas'
|
|
540
|
+
import { storeUserSchema, updateUserSchema } from './schemas'
|
|
541
|
+
import { storeUser, updateUser, deleteUser, deleteAllUsers } from './mutations'
|
|
542
|
+
import { errorHandling, GE } from '../utils'
|
|
543
|
+
|
|
544
|
+
const Mutation = {
|
|
545
|
+
storeUser: async (
|
|
546
|
+
parent: unknown,
|
|
547
|
+
{ user }: { user: UserDTO },
|
|
548
|
+
context: Context
|
|
549
|
+
): Promise<UserDTO> => {
|
|
550
|
+
const { log } = context
|
|
551
|
+
const validate = ajv.compile(storeUserSchema)
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
const ok = validate(user)
|
|
555
|
+
|
|
556
|
+
if (!ok)
|
|
557
|
+
throw new ApolloError(
|
|
558
|
+
\`\${(validate.errors as DefinedError[])[0].instancePath.replace(
|
|
559
|
+
'/',
|
|
560
|
+
''
|
|
561
|
+
)} \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
562
|
+
'UNPROCESSABLE_ENTITY'
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
return await storeUser({ user }, context)
|
|
566
|
+
} catch (e) {
|
|
567
|
+
log.error(validate.errors)
|
|
568
|
+
|
|
569
|
+
return errorHandling({
|
|
570
|
+
e,
|
|
571
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
572
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
573
|
+
log
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
updateUser: async (
|
|
578
|
+
parent: unknown,
|
|
579
|
+
{ user }: { user: UserDTO },
|
|
580
|
+
context: Context
|
|
581
|
+
): Promise<UserDTO> => {
|
|
582
|
+
const validate = ajv.compile(updateUserSchema)
|
|
583
|
+
const { log } = context
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const ok = validate(user)
|
|
587
|
+
|
|
588
|
+
if (!ok)
|
|
589
|
+
throw new ApolloError(
|
|
590
|
+
\`\${(validate.errors as DefinedError[])[0].instancePath.replace(
|
|
591
|
+
'/',
|
|
592
|
+
''
|
|
593
|
+
)} \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
594
|
+
'UNPROCESSABLE_ENTITY'
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
return await updateUser({ user }, context)
|
|
598
|
+
} catch (e) {
|
|
599
|
+
log.error(validate.errors)
|
|
600
|
+
|
|
601
|
+
return errorHandling({
|
|
602
|
+
e,
|
|
603
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
604
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
605
|
+
log
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
deleteUser: async (
|
|
610
|
+
parent: unknown,
|
|
611
|
+
{ id }: { id: string },
|
|
612
|
+
context: Context
|
|
613
|
+
): Promise<string> => {
|
|
614
|
+
const validate = ajv.compile(idSchema)
|
|
615
|
+
const { log } = context
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const ok = validate({ id })
|
|
619
|
+
|
|
620
|
+
if (!ok)
|
|
621
|
+
throw new ApolloError(
|
|
622
|
+
\`\${(validate.errors as DefinedError[])[0].instancePath.replace(
|
|
623
|
+
'/',
|
|
624
|
+
''
|
|
625
|
+
)} \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
626
|
+
'UNPROCESSABLE_ENTITY'
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
return await deleteUser({ id }, context)
|
|
630
|
+
} catch (e) {
|
|
631
|
+
log.error(validate.errors)
|
|
632
|
+
|
|
633
|
+
return errorHandling({
|
|
634
|
+
e,
|
|
635
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
636
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
637
|
+
log
|
|
638
|
+
})
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
deleteAllUsers: async (
|
|
642
|
+
parent: unknown,
|
|
643
|
+
args: unknown,
|
|
644
|
+
context: Context
|
|
645
|
+
): Promise<string> => {
|
|
646
|
+
const { log } = context
|
|
647
|
+
try {
|
|
648
|
+
return await deleteAllUsers(context)
|
|
649
|
+
} catch (e) {
|
|
650
|
+
return errorHandling({
|
|
651
|
+
e,
|
|
652
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
653
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
654
|
+
log
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export { Mutation }
|
|
661
|
+
`,
|
|
662
|
+
file: `${projectName}/src/graphQL/models/User/mutationsResolver.ts`
|
|
663
|
+
},
|
|
664
|
+
queries: {
|
|
665
|
+
content: `import { ApolloError } from 'apollo-server-core'
|
|
666
|
+
|
|
667
|
+
import { get } from 'database'
|
|
668
|
+
import { UserDTO } from 'schemas'
|
|
669
|
+
import { EFU, GE, errorHandling } from '../utils'
|
|
670
|
+
|
|
671
|
+
const getUsers = async (
|
|
672
|
+
parent: unknown,
|
|
673
|
+
args: unknown,
|
|
674
|
+
{ log }: Context
|
|
675
|
+
): Promise<UserDTO[]> => {
|
|
676
|
+
try {
|
|
677
|
+
const users = (await get()) as UserDTO[]
|
|
678
|
+
|
|
679
|
+
return users
|
|
680
|
+
} catch (e) {
|
|
681
|
+
return errorHandling({
|
|
682
|
+
e,
|
|
683
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
684
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
685
|
+
log
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const getUser = async (
|
|
691
|
+
{ id }: { id: string },
|
|
692
|
+
{ log }: Context
|
|
693
|
+
): Promise<UserDTO> => {
|
|
694
|
+
try {
|
|
695
|
+
const user = (await get(id as string)) as UserDTO | null
|
|
696
|
+
|
|
697
|
+
if (!user) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
|
|
698
|
+
|
|
699
|
+
return user
|
|
700
|
+
} catch (e) {
|
|
701
|
+
return errorHandling({
|
|
702
|
+
e,
|
|
703
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
704
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
705
|
+
log
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export { getUsers, getUser }
|
|
711
|
+
`,
|
|
712
|
+
file: `${projectName}/src/graphQL/models/User/queries.ts`
|
|
713
|
+
},
|
|
714
|
+
queriesResolver: {
|
|
715
|
+
content: `import { DefinedError } from 'ajv'
|
|
716
|
+
import { ApolloError } from 'apollo-server-core'
|
|
717
|
+
|
|
718
|
+
import { ajv, idSchema, UserDTO } from 'schemas'
|
|
719
|
+
import { getUser, getUsers } from './queries'
|
|
720
|
+
import { errorHandling, GE } from '../utils'
|
|
721
|
+
|
|
722
|
+
const Query = {
|
|
723
|
+
getUser: async (
|
|
724
|
+
parent: unknown,
|
|
725
|
+
{ id }: { id: string },
|
|
726
|
+
context: Context
|
|
727
|
+
): Promise<UserDTO> => {
|
|
728
|
+
const { log } = context
|
|
729
|
+
const validate = ajv.compile(idSchema)
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
const ok = validate({ id })
|
|
733
|
+
|
|
734
|
+
if (!ok)
|
|
735
|
+
throw new ApolloError(
|
|
736
|
+
\`id \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
737
|
+
'UNPROCESSABLE_ENTITY'
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
return await getUser({ id }, context)
|
|
741
|
+
} catch (e) {
|
|
742
|
+
log.error(validate.errors)
|
|
743
|
+
|
|
744
|
+
return errorHandling({
|
|
745
|
+
e,
|
|
746
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
747
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
748
|
+
log
|
|
749
|
+
})
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
getUsers
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export { Query }
|
|
756
|
+
`,
|
|
757
|
+
file: `${projectName}/src/graphQL/models/User/queriesResolver.ts`
|
|
758
|
+
},
|
|
759
|
+
schemas: {
|
|
760
|
+
content: `import { Type } from '@sinclair/typebox'
|
|
761
|
+
|
|
762
|
+
const updateUserSchema = Type.Object(
|
|
763
|
+
{
|
|
764
|
+
id: Type.String({ minLength: 24, maxLength: 24 }),
|
|
765
|
+
lastName: Type.String(),
|
|
766
|
+
name: Type.String()
|
|
767
|
+
},
|
|
768
|
+
{ additionalProperties: false }
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
const storeUserSchema = Type.Object(
|
|
772
|
+
{
|
|
773
|
+
lastName: Type.String({ minLength: 1 }),
|
|
774
|
+
name: Type.String({ minLength: 1 })
|
|
775
|
+
},
|
|
776
|
+
{ additionalProperties: false }
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
export { updateUserSchema, storeUserSchema }
|
|
780
|
+
`,
|
|
781
|
+
file: `${projectName}/src/graphQL/models/User/schemas.ts`
|
|
782
|
+
},
|
|
783
|
+
typeDefs: {
|
|
784
|
+
content: `import { gql } from 'apollo-server-core'
|
|
785
|
+
|
|
786
|
+
const User = gql\`
|
|
787
|
+
type User {
|
|
788
|
+
id: ID!
|
|
789
|
+
name: String!
|
|
790
|
+
lastName: String!
|
|
791
|
+
updatedAt: String!
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
type Query {
|
|
795
|
+
getUsers: [User!]!
|
|
796
|
+
getUser(id: ID!): User!
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
input StoreUserInput {
|
|
800
|
+
lastName: String!
|
|
801
|
+
name: String!
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
input UpdateUserInput {
|
|
805
|
+
id: String!
|
|
806
|
+
lastName: String!
|
|
807
|
+
name: String!
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
type Mutation {
|
|
811
|
+
storeUser(user: StoreUserInput!): User!
|
|
812
|
+
deleteAllUsers: String
|
|
813
|
+
updateUser(user: UpdateUserInput!): User!
|
|
814
|
+
deleteUser(id: ID!): String
|
|
815
|
+
}
|
|
816
|
+
\`
|
|
817
|
+
|
|
818
|
+
export { User }
|
|
819
|
+
`,
|
|
820
|
+
file: `${projectName}/src/graphQL/models/User/typeDefs.ts`
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
utils: {
|
|
824
|
+
messages: {
|
|
825
|
+
index: {
|
|
826
|
+
content: `enum GenericErrors {
|
|
827
|
+
INTERNAL_SERVER_ERROR = 'Something went wrong'
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export { GenericErrors as GE }
|
|
831
|
+
export * from './user'
|
|
832
|
+
`,
|
|
833
|
+
file: `${projectName}/src/graphQL/models/utils/messages/index.ts`
|
|
834
|
+
},
|
|
835
|
+
user: {
|
|
836
|
+
content: `enum ErrorForUser {
|
|
837
|
+
NOT_FOUND = 'The requested user does not exists',
|
|
838
|
+
NOTHING_TO_DELETE = 'There is no user to be deleted'
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
enum MessageForUser {
|
|
842
|
+
ALL_USERS_DELETED = 'All the users were deleted successfully',
|
|
843
|
+
USER_DELETED = 'The requested user was successfully deleted'
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export { ErrorForUser as EFU, MessageForUser as MFU }
|
|
847
|
+
`,
|
|
848
|
+
file: `${projectName}/src/graphQL/models/utils/messages/user.ts`
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
index: {
|
|
852
|
+
content: `import { ApolloError } from 'apollo-server-core'
|
|
853
|
+
import { FastifyLoggerInstance } from 'fastify'
|
|
854
|
+
|
|
855
|
+
const errorHandling = ({
|
|
856
|
+
e,
|
|
857
|
+
message,
|
|
858
|
+
code,
|
|
859
|
+
log
|
|
860
|
+
}: {
|
|
861
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
862
|
+
e: any
|
|
863
|
+
message: string
|
|
864
|
+
code: string
|
|
865
|
+
log: FastifyLoggerInstance
|
|
866
|
+
}): never => {
|
|
867
|
+
log.error(e)
|
|
868
|
+
|
|
869
|
+
if (e instanceof ApolloError) throw e
|
|
870
|
+
|
|
871
|
+
throw new ApolloError(message ?? e.message, code)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export { errorHandling }
|
|
875
|
+
export * from './messages'
|
|
876
|
+
`,
|
|
877
|
+
file: `${projectName}/src/graphQL/models/utils/index.ts`
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
index: {
|
|
881
|
+
content: `import { mergeSchemas } from '@graphql-tools/schema'
|
|
882
|
+
|
|
883
|
+
import { User } from './User'
|
|
884
|
+
|
|
885
|
+
const mergedSchema = mergeSchemas({
|
|
886
|
+
schemas: [User]
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
export { mergedSchema }
|
|
890
|
+
`,
|
|
891
|
+
file: `${projectName}/src/graphQL/models/index.ts`
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const network = {
|
|
897
|
+
response: {
|
|
898
|
+
content: `interface ResponseProps {
|
|
899
|
+
error: boolean
|
|
900
|
+
message: unknown
|
|
901
|
+
res: CustomResponse
|
|
902
|
+
status: number
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const response = ({ error, message, res, status }: ResponseProps): void => {
|
|
906
|
+
res.status(status).send({ error, message })
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export { response }
|
|
910
|
+
`,
|
|
911
|
+
file: `${projectName}/src/network/response.ts`
|
|
912
|
+
},
|
|
913
|
+
router: {
|
|
914
|
+
content: `import { Application, Response, Request, Router, NextFunction } from 'express'
|
|
915
|
+
import swaggerUi from 'swagger-ui-express'
|
|
916
|
+
import httpErrors from 'http-errors'
|
|
917
|
+
|
|
918
|
+
import { response } from './response'
|
|
919
|
+
import { Home } from './routes'
|
|
920
|
+
import { docs } from 'utils'
|
|
921
|
+
|
|
922
|
+
const routers: Router[] = []
|
|
923
|
+
const applyRoutes = (app: Application): void => {
|
|
924
|
+
app.use('/', Home)
|
|
925
|
+
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(docs))
|
|
926
|
+
routers.forEach((router: Router): Application => app.use('/api', router))
|
|
927
|
+
|
|
928
|
+
// Handling 404 error
|
|
929
|
+
app.use((req, res, next) => {
|
|
930
|
+
next(new httpErrors.NotFound('This route does not exists'))
|
|
931
|
+
})
|
|
932
|
+
app.use(
|
|
933
|
+
(
|
|
934
|
+
error: httpErrors.HttpError,
|
|
935
|
+
req: Request,
|
|
936
|
+
res: Response,
|
|
937
|
+
next: NextFunction
|
|
938
|
+
) => {
|
|
939
|
+
response({
|
|
940
|
+
error: true,
|
|
941
|
+
message: error.message,
|
|
942
|
+
res,
|
|
943
|
+
status: error.status
|
|
944
|
+
})
|
|
945
|
+
next()
|
|
946
|
+
}
|
|
947
|
+
)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export { applyRoutes }
|
|
951
|
+
`,
|
|
952
|
+
file: `${projectName}/src/network/router.ts`
|
|
953
|
+
},
|
|
954
|
+
server: {
|
|
955
|
+
content: `import http from 'http'
|
|
956
|
+
import express from 'express'
|
|
957
|
+
import mongoose from 'mongoose'
|
|
958
|
+
import cors from 'cors'
|
|
959
|
+
import pino, { HttpLogger } from 'express-pino-logger'
|
|
960
|
+
import { ApolloServer } from 'apollo-server-express'
|
|
961
|
+
import {
|
|
962
|
+
ApolloServerPluginDrainHttpServer,
|
|
963
|
+
ApolloServerPluginLandingPageDisabled,
|
|
964
|
+
ApolloServerPluginLandingPageGraphQLPlayground
|
|
965
|
+
} from 'apollo-server-core'
|
|
966
|
+
|
|
967
|
+
import { mergedSchema as schema } from 'graphQL'
|
|
968
|
+
import { applyRoutes } from './router'
|
|
969
|
+
|
|
970
|
+
const PORT = (process.env.PORT as string) || 1996
|
|
971
|
+
|
|
972
|
+
class Server {
|
|
973
|
+
#app: express.Application
|
|
974
|
+
#connection: mongoose.Connection | undefined
|
|
975
|
+
#httpServer: http.Server
|
|
976
|
+
#log: HttpLogger
|
|
977
|
+
|
|
978
|
+
constructor() {
|
|
979
|
+
this.#app = express()
|
|
980
|
+
this.#httpServer = http.createServer(this.#app)
|
|
981
|
+
this.#log = pino({
|
|
982
|
+
transport: {
|
|
983
|
+
target: 'pino-pretty',
|
|
984
|
+
options: {
|
|
985
|
+
colorize: true
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
})
|
|
989
|
+
this.#config()
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
#config() {
|
|
993
|
+
this.#app.use(cors())
|
|
994
|
+
this.#app.use(this.#log)
|
|
995
|
+
this.#app.use(express.json())
|
|
996
|
+
this.#app.use(express.urlencoded({ extended: false }))
|
|
997
|
+
this.#app.use(
|
|
998
|
+
(
|
|
999
|
+
req: express.Request,
|
|
1000
|
+
res: express.Response,
|
|
1001
|
+
next: express.NextFunction
|
|
1002
|
+
) => {
|
|
1003
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
|
|
1004
|
+
res.header('Access-Control-Allow-Origin', '*')
|
|
1005
|
+
res.header(
|
|
1006
|
+
'Access-Control-Allow-Headers',
|
|
1007
|
+
'Authorization, Content-Type'
|
|
1008
|
+
)
|
|
1009
|
+
res.header('x-powered-by', 'Simba.js')
|
|
1010
|
+
next()
|
|
1011
|
+
}
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async #mongo(): Promise<void> {
|
|
1016
|
+
this.#connection = mongoose.connection
|
|
1017
|
+
const connection = {
|
|
1018
|
+
keepAlive: true,
|
|
1019
|
+
useNewUrlParser: true,
|
|
1020
|
+
useUnifiedTopology: true
|
|
1021
|
+
}
|
|
1022
|
+
this.#connection.on('connected', () => {
|
|
1023
|
+
this.#log.logger.info('Mongo connection established.')
|
|
1024
|
+
})
|
|
1025
|
+
this.#connection.on('reconnected', () => {
|
|
1026
|
+
this.#log.logger.info('Mongo connection reestablished')
|
|
1027
|
+
})
|
|
1028
|
+
this.#connection.on('disconnected', () => {
|
|
1029
|
+
this.#log.logger.info('Mongo connection disconnected')
|
|
1030
|
+
this.#log.logger.info('Trying to reconnected to Mongo...')
|
|
1031
|
+
setTimeout(() => {
|
|
1032
|
+
mongoose.connect(process.env.MONGO_URI as string, {
|
|
1033
|
+
...connection,
|
|
1034
|
+
connectTimeoutMS: 3000,
|
|
1035
|
+
socketTimeoutMS: 3000
|
|
1036
|
+
})
|
|
1037
|
+
}, 3000)
|
|
1038
|
+
})
|
|
1039
|
+
this.#connection.on('close', () => {
|
|
1040
|
+
this.#log.logger.info('Mongo connection closed')
|
|
1041
|
+
})
|
|
1042
|
+
this.#connection.on('error', (e: Error) => {
|
|
1043
|
+
this.#log.logger.info('Mongo connection error:')
|
|
1044
|
+
this.#log.logger.error(e)
|
|
1045
|
+
})
|
|
1046
|
+
await mongoose.connect(process.env.MONGO_URI as string, connection)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
public async start(): Promise<void> {
|
|
1050
|
+
const server = new ApolloServer({
|
|
1051
|
+
schema,
|
|
1052
|
+
plugins: [
|
|
1053
|
+
ApolloServerPluginDrainHttpServer({ httpServer: this.#httpServer }),
|
|
1054
|
+
process.env.NODE_ENV === 'production'
|
|
1055
|
+
? ApolloServerPluginLandingPageDisabled()
|
|
1056
|
+
: ApolloServerPluginLandingPageGraphQLPlayground()
|
|
1057
|
+
],
|
|
1058
|
+
context: (): Context => ({
|
|
1059
|
+
log: this.#log.logger
|
|
1060
|
+
})
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
await server.start()
|
|
1065
|
+
server.applyMiddleware({
|
|
1066
|
+
app: this.#app,
|
|
1067
|
+
path: '/api'
|
|
1068
|
+
})
|
|
1069
|
+
applyRoutes(this.#app)
|
|
1070
|
+
await this.#mongo()
|
|
1071
|
+
this.#httpServer.listen(PORT, () => {
|
|
1072
|
+
this.#log.logger.info(\`Server listening at port: \${PORT}\`)
|
|
1073
|
+
this.#log.logger.info(
|
|
1074
|
+
\`GraphQL server listening at: http://localhost:\${PORT}\${server.graphqlPath}\`
|
|
1075
|
+
)
|
|
1076
|
+
})
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
console.error(e)
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const server = new Server()
|
|
1084
|
+
|
|
1085
|
+
export { server as Server }
|
|
1086
|
+
`,
|
|
1087
|
+
file: `${projectName}/src/network/server.ts`
|
|
1088
|
+
},
|
|
1089
|
+
routes: {
|
|
1090
|
+
home: {
|
|
1091
|
+
content: `import { Response, Request, Router } from 'express'
|
|
1092
|
+
|
|
1093
|
+
import { response } from 'network/response'
|
|
1094
|
+
|
|
1095
|
+
const Home = Router()
|
|
1096
|
+
|
|
1097
|
+
Home.route('').get((req: Request, res: Response) => {
|
|
1098
|
+
response({
|
|
1099
|
+
error: false,
|
|
1100
|
+
message: 'Welcome to your GraphQL Express Backend!',
|
|
1101
|
+
res,
|
|
1102
|
+
status: 200
|
|
1103
|
+
})
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
export { Home }
|
|
1107
|
+
`,
|
|
1108
|
+
file: `${projectName}/src/network/routes/home.ts`
|
|
1109
|
+
},
|
|
1110
|
+
index: {
|
|
1111
|
+
content: "export * from './home'\n",
|
|
1112
|
+
file: `${projectName}/src/network/routes/index.ts`
|
|
1113
|
+
},
|
|
1114
|
+
utils: {
|
|
1115
|
+
index: {
|
|
1116
|
+
content: `import { NextFunction } from 'express'
|
|
1117
|
+
import httpErrors from 'http-errors'
|
|
1118
|
+
import { TObject, TProperties } from '@sinclair/typebox'
|
|
1119
|
+
import Ajv from 'ajv'
|
|
1120
|
+
|
|
1121
|
+
const ajv = new Ajv({
|
|
1122
|
+
removeAdditional: true,
|
|
1123
|
+
useDefaults: true,
|
|
1124
|
+
coerceTypes: true,
|
|
1125
|
+
nullable: true
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
type Middleware = (
|
|
1129
|
+
req: CustomRequest,
|
|
1130
|
+
res: CustomResponse,
|
|
1131
|
+
next: NextFunction
|
|
1132
|
+
) => void
|
|
1133
|
+
|
|
1134
|
+
const validatorCompiler = <T extends TProperties>(
|
|
1135
|
+
schema: TObject<T>,
|
|
1136
|
+
value: 'body' | 'params'
|
|
1137
|
+
): Middleware => {
|
|
1138
|
+
return (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
|
|
1139
|
+
const validate = ajv.compile(schema)
|
|
1140
|
+
const ok = validate(req[value])
|
|
1141
|
+
|
|
1142
|
+
if (!ok && validate.errors) {
|
|
1143
|
+
const [error] = validate.errors
|
|
1144
|
+
const errorMessage = \`\${error.dataPath.replace('.', '')} \${error.message}\`
|
|
1145
|
+
|
|
1146
|
+
return next(new httpErrors.UnprocessableEntity(errorMessage))
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
next()
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
export { validatorCompiler }
|
|
1154
|
+
`,
|
|
1155
|
+
file: `${projectName}/src/network/routes/utils/index.ts`
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
await Promise.all([
|
|
1162
|
+
writeFile(graphQL.index.file, graphQL.index.content),
|
|
1163
|
+
writeFile(
|
|
1164
|
+
graphQL.models.User.index.file,
|
|
1165
|
+
graphQL.models.User.index.content
|
|
1166
|
+
),
|
|
1167
|
+
writeFile(
|
|
1168
|
+
graphQL.models.User.mutations.file,
|
|
1169
|
+
graphQL.models.User.mutations.content
|
|
1170
|
+
),
|
|
1171
|
+
writeFile(
|
|
1172
|
+
graphQL.models.User.mutationsResolver.file,
|
|
1173
|
+
graphQL.models.User.mutationsResolver.content
|
|
1174
|
+
),
|
|
1175
|
+
writeFile(
|
|
1176
|
+
graphQL.models.User.queries.file,
|
|
1177
|
+
graphQL.models.User.queries.content
|
|
1178
|
+
),
|
|
1179
|
+
writeFile(
|
|
1180
|
+
graphQL.models.User.queriesResolver.file,
|
|
1181
|
+
graphQL.models.User.queriesResolver.content
|
|
1182
|
+
),
|
|
1183
|
+
writeFile(
|
|
1184
|
+
graphQL.models.User.schemas.file,
|
|
1185
|
+
graphQL.models.User.schemas.content
|
|
1186
|
+
),
|
|
1187
|
+
writeFile(
|
|
1188
|
+
graphQL.models.User.typeDefs.file,
|
|
1189
|
+
graphQL.models.User.typeDefs.content
|
|
1190
|
+
),
|
|
1191
|
+
writeFile(
|
|
1192
|
+
graphQL.models.utils.messages.index.file,
|
|
1193
|
+
graphQL.models.utils.messages.index.content
|
|
1194
|
+
),
|
|
1195
|
+
writeFile(
|
|
1196
|
+
graphQL.models.utils.messages.user.file,
|
|
1197
|
+
graphQL.models.utils.messages.user.content
|
|
1198
|
+
),
|
|
1199
|
+
writeFile(
|
|
1200
|
+
graphQL.models.utils.index.file,
|
|
1201
|
+
graphQL.models.utils.index.content
|
|
1202
|
+
),
|
|
1203
|
+
writeFile(graphQL.models.index.file, graphQL.models.index.content),
|
|
1204
|
+
writeFile(network.response.file, network.response.content),
|
|
1205
|
+
writeFile(network.router.file, network.router.content),
|
|
1206
|
+
writeFile(network.server.file, network.server.content),
|
|
1207
|
+
writeFile(network.routes.home.file, network.routes.home.content),
|
|
1208
|
+
writeFile(network.routes.index.file, network.routes.index.content),
|
|
1209
|
+
writeFile(
|
|
1210
|
+
network.routes.utils.index.file,
|
|
1211
|
+
network.routes.utils.index.content
|
|
1212
|
+
)
|
|
1213
|
+
])
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* @param {Object} args
|
|
1218
|
+
* @param {String} args.projectName
|
|
1219
|
+
*/
|
|
1220
|
+
const fastifyRestNetwork = async ({ projectName }) => {
|
|
1221
|
+
const createFoldersCommand = `mkdir ${projectName}/src/network/utils`
|
|
1222
|
+
|
|
1223
|
+
if (platform() === 'win32')
|
|
1224
|
+
await exec(createFoldersCommand.replaceAll('/', '\\'))
|
|
1225
|
+
else await exec(createFoldersCommand)
|
|
1226
|
+
|
|
1227
|
+
const network = {
|
|
1228
|
+
response: {
|
|
1229
|
+
content: `import { FastifyReply } from 'fastify'
|
|
1230
|
+
|
|
1231
|
+
const response = ({
|
|
1232
|
+
error,
|
|
1233
|
+
message,
|
|
1234
|
+
reply,
|
|
1235
|
+
status
|
|
1236
|
+
}: {
|
|
1237
|
+
error: boolean
|
|
1238
|
+
message: unknown
|
|
1239
|
+
reply: FastifyReply
|
|
1240
|
+
status: number
|
|
1241
|
+
}): void => {
|
|
1242
|
+
reply.code(status).send({ error, message })
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export { response }
|
|
1246
|
+
`,
|
|
1247
|
+
file: `${projectName}/src/network/response.ts`
|
|
1248
|
+
},
|
|
1249
|
+
router: {
|
|
1250
|
+
content: `import { FastifyInstance } from 'fastify'
|
|
1251
|
+
import { HttpError } from 'http-errors'
|
|
1252
|
+
|
|
1253
|
+
import { response } from './response'
|
|
1254
|
+
import { Home, User, Docs } from './routes'
|
|
1255
|
+
|
|
1256
|
+
const routers = [Docs, User]
|
|
1257
|
+
const applyRoutes = (app: FastifyInstance): void => {
|
|
1258
|
+
Home(app)
|
|
1259
|
+
routers.forEach(router => router(app))
|
|
1260
|
+
|
|
1261
|
+
// Handling 404 error
|
|
1262
|
+
app.setNotFoundHandler((request, reply) => {
|
|
1263
|
+
response({
|
|
1264
|
+
error: true,
|
|
1265
|
+
message: 'This route does not exists',
|
|
1266
|
+
reply,
|
|
1267
|
+
status: 404
|
|
1268
|
+
})
|
|
1269
|
+
})
|
|
1270
|
+
app.setErrorHandler<HttpError>((error, request, reply) => {
|
|
1271
|
+
response({
|
|
1272
|
+
error: true,
|
|
1273
|
+
message: error.message,
|
|
1274
|
+
reply,
|
|
1275
|
+
status: error.status ?? 500
|
|
1276
|
+
})
|
|
1277
|
+
})
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
export { applyRoutes }
|
|
1281
|
+
`,
|
|
1282
|
+
file: `${projectName}/src/network/router.ts`
|
|
1283
|
+
},
|
|
1284
|
+
server: {
|
|
1285
|
+
content: `import fastify, { FastifyInstance } from 'fastify'
|
|
1286
|
+
import mongoose from 'mongoose'
|
|
1287
|
+
|
|
1288
|
+
import { applyRoutes } from './router'
|
|
1289
|
+
import { validatorCompiler } from './utils'
|
|
1290
|
+
|
|
1291
|
+
const PORT = process.env.PORT ?? 1996
|
|
1292
|
+
|
|
1293
|
+
class Server {
|
|
1294
|
+
#app: FastifyInstance
|
|
1295
|
+
#connection: mongoose.Connection | undefined
|
|
1296
|
+
|
|
1297
|
+
constructor() {
|
|
1298
|
+
this.#app = fastify({ logger: { prettyPrint: true } })
|
|
1299
|
+
this.#config()
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
#config() {
|
|
1303
|
+
this.#app.register(require('fastify-cors'), {})
|
|
1304
|
+
this.#app.addHook('preHandler', (req, reply, done) => {
|
|
1305
|
+
reply.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
|
|
1306
|
+
reply.header('Access-Control-Allow-Origin', '*')
|
|
1307
|
+
reply.header(
|
|
1308
|
+
'Access-Control-Allow-Headers',
|
|
1309
|
+
'Authorization, Content-Type'
|
|
1310
|
+
)
|
|
1311
|
+
reply.header('x-powered-by', 'Simba.js')
|
|
1312
|
+
done()
|
|
1313
|
+
})
|
|
1314
|
+
this.#app.setValidatorCompiler(validatorCompiler)
|
|
1315
|
+
applyRoutes(this.#app)
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
async #mongo(): Promise<void> {
|
|
1319
|
+
this.#connection = mongoose.connection
|
|
1320
|
+
const connection = {
|
|
1321
|
+
keepAlive: true,
|
|
1322
|
+
useNewUrlParser: true,
|
|
1323
|
+
useUnifiedTopology: true
|
|
1324
|
+
}
|
|
1325
|
+
this.#connection.on('connected', () => {
|
|
1326
|
+
this.#app.log.info('Mongo connection established.')
|
|
1327
|
+
})
|
|
1328
|
+
this.#connection.on('reconnected', () => {
|
|
1329
|
+
this.#app.log.info('Mongo connection reestablished')
|
|
1330
|
+
})
|
|
1331
|
+
this.#connection.on('disconnected', () => {
|
|
1332
|
+
this.#app.log.info('Mongo connection disconnected')
|
|
1333
|
+
this.#app.log.info('Trying to reconnected to Mongo...')
|
|
1334
|
+
setTimeout(() => {
|
|
1335
|
+
mongoose.connect(process.env.MONGO_URI as string, {
|
|
1336
|
+
...connection,
|
|
1337
|
+
connectTimeoutMS: 3000,
|
|
1338
|
+
socketTimeoutMS: 3000
|
|
1339
|
+
})
|
|
1340
|
+
}, 3000)
|
|
1341
|
+
})
|
|
1342
|
+
this.#connection.on('close', () => {
|
|
1343
|
+
this.#app.log.info('Mongo connection closed')
|
|
1344
|
+
})
|
|
1345
|
+
this.#connection.on('error', (e: Error) => {
|
|
1346
|
+
this.#app.log.info('Mongo connection error:')
|
|
1347
|
+
this.#app.log.error(e)
|
|
1348
|
+
})
|
|
1349
|
+
await mongoose.connect(process.env.MONGO_URI as string, connection)
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
public async start(): Promise<void> {
|
|
1353
|
+
try {
|
|
1354
|
+
await this.#app.listen(PORT)
|
|
1355
|
+
this.#mongo()
|
|
1356
|
+
} catch (e) {
|
|
1357
|
+
console.error(e)
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const server = new Server()
|
|
1363
|
+
|
|
1364
|
+
export { server as Server }
|
|
1365
|
+
`,
|
|
1366
|
+
file: `${projectName}/src/network/server.ts`
|
|
1367
|
+
},
|
|
1368
|
+
routes: {
|
|
1369
|
+
docs: {
|
|
1370
|
+
content: `import { FastifyInstance } from 'fastify'
|
|
1371
|
+
import fastifySwagger from 'fastify-swagger'
|
|
1372
|
+
|
|
1373
|
+
const Docs = (app: FastifyInstance, prefix = '/api'): void => {
|
|
1374
|
+
app.register(fastifySwagger, {
|
|
1375
|
+
routePrefix: \`\${prefix}/docs\`,
|
|
1376
|
+
openapi: {
|
|
1377
|
+
info: {
|
|
1378
|
+
title: 'Test swagger',
|
|
1379
|
+
description: 'Testing the Fastify swagger API',
|
|
1380
|
+
version: '0.1.0',
|
|
1381
|
+
contact: {
|
|
1382
|
+
email: 'sluzquinosa@uni.pe'
|
|
1383
|
+
},
|
|
1384
|
+
license: {
|
|
1385
|
+
name: 'MIT',
|
|
1386
|
+
url: 'https://opensource.org/licenses/MIT'
|
|
1387
|
+
}
|
|
1388
|
+
},
|
|
1389
|
+
servers: [
|
|
1390
|
+
{
|
|
1391
|
+
url: 'http://localhost:1996/api',
|
|
1392
|
+
description: 'test-fastify local API'
|
|
1393
|
+
}
|
|
1394
|
+
],
|
|
1395
|
+
tags: [
|
|
1396
|
+
{
|
|
1397
|
+
name: 'user',
|
|
1398
|
+
description: 'User related endpoints'
|
|
1399
|
+
}
|
|
1400
|
+
]
|
|
1401
|
+
},
|
|
1402
|
+
exposeRoute: true
|
|
1403
|
+
})
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
export { Docs }
|
|
1407
|
+
`,
|
|
1408
|
+
file: `${projectName}/src/network/routes/docs.ts`
|
|
1409
|
+
},
|
|
1410
|
+
home: {
|
|
1411
|
+
content: `import { FastifyInstance } from 'fastify'
|
|
1412
|
+
import { response } from 'network/response'
|
|
1413
|
+
|
|
1414
|
+
const Home = (app: FastifyInstance, prefix = '/'): void => {
|
|
1415
|
+
app.get(\`\${prefix}\`, (request, reply) => {
|
|
1416
|
+
response({
|
|
1417
|
+
error: false,
|
|
1418
|
+
message: 'Welcome to your Fastify Backend!',
|
|
1419
|
+
reply,
|
|
1420
|
+
status: 200
|
|
1421
|
+
})
|
|
1422
|
+
})
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
export { Home }
|
|
1426
|
+
`,
|
|
1427
|
+
file: `${projectName}/src/network/routes/home.ts`
|
|
1428
|
+
},
|
|
1429
|
+
index: {
|
|
1430
|
+
content: `export * from './home'
|
|
1431
|
+
export * from './user'
|
|
1432
|
+
export * from './docs'
|
|
1433
|
+
`,
|
|
1434
|
+
file: `${projectName}/src/network/routes/index.ts`
|
|
1435
|
+
},
|
|
1436
|
+
user: {
|
|
1437
|
+
content: `import { FastifyInstance } from 'fastify'
|
|
1438
|
+
import { Type } from '@sinclair/typebox'
|
|
1439
|
+
|
|
1440
|
+
import { response } from 'network/response'
|
|
1441
|
+
import {
|
|
1442
|
+
userDto,
|
|
1443
|
+
idSchema,
|
|
1444
|
+
IdSchema,
|
|
1445
|
+
storeUserSchema,
|
|
1446
|
+
StoreUser
|
|
1447
|
+
} from 'schemas'
|
|
1448
|
+
import { UserService } from 'services'
|
|
1449
|
+
|
|
1450
|
+
const User = (app: FastifyInstance, prefix = '/api'): void => {
|
|
1451
|
+
app
|
|
1452
|
+
.post<{ Body: StoreUser }>(
|
|
1453
|
+
\`\${prefix}/users\`,
|
|
1454
|
+
{
|
|
1455
|
+
schema: {
|
|
1456
|
+
body: storeUserSchema,
|
|
1457
|
+
response: {
|
|
1458
|
+
200: {
|
|
1459
|
+
error: {
|
|
1460
|
+
type: 'boolean'
|
|
1461
|
+
},
|
|
1462
|
+
message: userDto
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
tags: ['user']
|
|
1466
|
+
}
|
|
1467
|
+
},
|
|
1468
|
+
async (request, reply) => {
|
|
1469
|
+
const {
|
|
1470
|
+
body: {
|
|
1471
|
+
args: { lastName, name }
|
|
1472
|
+
}
|
|
1473
|
+
} = request
|
|
1474
|
+
const us = new UserService({
|
|
1475
|
+
userDtoWithoutId: { lastName, name }
|
|
1476
|
+
})
|
|
1477
|
+
const user = await us.process({ type: 'store' })
|
|
1478
|
+
|
|
1479
|
+
response({
|
|
1480
|
+
error: false,
|
|
1481
|
+
message: user,
|
|
1482
|
+
reply,
|
|
1483
|
+
status: 201
|
|
1484
|
+
})
|
|
1485
|
+
}
|
|
1486
|
+
)
|
|
1487
|
+
.get(
|
|
1488
|
+
\`\${prefix}/users\`,
|
|
1489
|
+
{
|
|
1490
|
+
schema: {
|
|
1491
|
+
response: {
|
|
1492
|
+
200: {
|
|
1493
|
+
error: {
|
|
1494
|
+
type: 'boolean'
|
|
1495
|
+
},
|
|
1496
|
+
message: Type.Array(userDto)
|
|
1497
|
+
}
|
|
1498
|
+
},
|
|
1499
|
+
tags: ['user']
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
async (request, reply) => {
|
|
1503
|
+
const us = new UserService()
|
|
1504
|
+
const users = await us.process({ type: 'getAll' })
|
|
1505
|
+
|
|
1506
|
+
response({
|
|
1507
|
+
error: false,
|
|
1508
|
+
message: users,
|
|
1509
|
+
reply,
|
|
1510
|
+
status: 200
|
|
1511
|
+
})
|
|
1512
|
+
}
|
|
1513
|
+
)
|
|
1514
|
+
.delete(
|
|
1515
|
+
\`\${prefix}/users\`,
|
|
1516
|
+
{
|
|
1517
|
+
schema: {
|
|
1518
|
+
response: {
|
|
1519
|
+
200: {
|
|
1520
|
+
error: {
|
|
1521
|
+
type: 'boolean'
|
|
1522
|
+
},
|
|
1523
|
+
message: {
|
|
1524
|
+
type: 'string'
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
},
|
|
1528
|
+
tags: ['user']
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
async (request, reply) => {
|
|
1532
|
+
const us = new UserService()
|
|
1533
|
+
const result = await us.process({ type: 'deleteAll' })
|
|
1534
|
+
|
|
1535
|
+
response({
|
|
1536
|
+
error: false,
|
|
1537
|
+
message: result,
|
|
1538
|
+
reply,
|
|
1539
|
+
status: 200
|
|
1540
|
+
})
|
|
1541
|
+
}
|
|
1542
|
+
)
|
|
1543
|
+
.get<{ Params: IdSchema }>(
|
|
1544
|
+
\`\${prefix}/user/:id\`,
|
|
1545
|
+
{
|
|
1546
|
+
schema: {
|
|
1547
|
+
params: idSchema,
|
|
1548
|
+
response: {
|
|
1549
|
+
200: {
|
|
1550
|
+
error: {
|
|
1551
|
+
type: 'boolean'
|
|
1552
|
+
},
|
|
1553
|
+
message: userDto
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
tags: ['user']
|
|
1557
|
+
}
|
|
1558
|
+
},
|
|
1559
|
+
async (request, reply) => {
|
|
1560
|
+
const {
|
|
1561
|
+
params: { id }
|
|
1562
|
+
} = request
|
|
1563
|
+
const us = new UserService({ id })
|
|
1564
|
+
const user = await us.process({ type: 'getOne' })
|
|
1565
|
+
|
|
1566
|
+
response({
|
|
1567
|
+
error: false,
|
|
1568
|
+
message: user,
|
|
1569
|
+
reply,
|
|
1570
|
+
status: 200
|
|
1571
|
+
})
|
|
1572
|
+
}
|
|
1573
|
+
)
|
|
1574
|
+
.patch<{ Body: StoreUser; Params: IdSchema }>(
|
|
1575
|
+
\`\${prefix}/user/:id\`,
|
|
1576
|
+
{
|
|
1577
|
+
schema: {
|
|
1578
|
+
body: storeUserSchema,
|
|
1579
|
+
params: idSchema,
|
|
1580
|
+
response: {
|
|
1581
|
+
200: {
|
|
1582
|
+
error: {
|
|
1583
|
+
type: 'boolean'
|
|
1584
|
+
},
|
|
1585
|
+
message: userDto
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
tags: ['user']
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
async (request, reply) => {
|
|
1592
|
+
const {
|
|
1593
|
+
body: {
|
|
1594
|
+
args: { name, lastName }
|
|
1595
|
+
},
|
|
1596
|
+
params: { id }
|
|
1597
|
+
} = request
|
|
1598
|
+
const us = new UserService({
|
|
1599
|
+
userDto: { name, lastName, id }
|
|
1600
|
+
})
|
|
1601
|
+
const user = await us.process({ type: 'update' })
|
|
1602
|
+
|
|
1603
|
+
response({
|
|
1604
|
+
error: false,
|
|
1605
|
+
message: user,
|
|
1606
|
+
reply,
|
|
1607
|
+
status: 200
|
|
1608
|
+
})
|
|
1609
|
+
}
|
|
1610
|
+
)
|
|
1611
|
+
.delete<{ Params: IdSchema }>(
|
|
1612
|
+
\`\${prefix}/user/:id\`,
|
|
1613
|
+
{
|
|
1614
|
+
schema: {
|
|
1615
|
+
params: idSchema,
|
|
1616
|
+
response: {
|
|
1617
|
+
200: {
|
|
1618
|
+
error: {
|
|
1619
|
+
type: 'boolean'
|
|
1620
|
+
},
|
|
1621
|
+
message: {
|
|
1622
|
+
type: 'string'
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
tags: ['user']
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
async (request, reply) => {
|
|
1630
|
+
const {
|
|
1631
|
+
params: { id }
|
|
1632
|
+
} = request
|
|
1633
|
+
const us = new UserService({ id })
|
|
1634
|
+
const result = await us.process({ type: 'delete' })
|
|
1635
|
+
|
|
1636
|
+
response({
|
|
1637
|
+
error: false,
|
|
1638
|
+
message: result,
|
|
1639
|
+
reply,
|
|
1640
|
+
status: 200
|
|
1641
|
+
})
|
|
1642
|
+
}
|
|
1643
|
+
)
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
export { User }
|
|
1647
|
+
`,
|
|
1648
|
+
file: `${projectName}/src/network/routes/user.ts`
|
|
1649
|
+
}
|
|
1650
|
+
},
|
|
1651
|
+
utils: {
|
|
1652
|
+
index: {
|
|
1653
|
+
content: `/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1654
|
+
import {
|
|
1655
|
+
FastifyRouteSchemaDef,
|
|
1656
|
+
FastifyValidationResult
|
|
1657
|
+
} from 'fastify/types/schema'
|
|
1658
|
+
import httpErrors from 'http-errors'
|
|
1659
|
+
import Ajv from 'ajv'
|
|
1660
|
+
|
|
1661
|
+
const ajv = new Ajv({
|
|
1662
|
+
removeAdditional: true,
|
|
1663
|
+
useDefaults: true,
|
|
1664
|
+
coerceTypes: true,
|
|
1665
|
+
nullable: true
|
|
1666
|
+
})
|
|
1667
|
+
|
|
1668
|
+
const validatorCompiler = ({
|
|
1669
|
+
schema
|
|
1670
|
+
}: FastifyRouteSchemaDef<any>): FastifyValidationResult => {
|
|
1671
|
+
const validate = ajv.compile(schema)
|
|
1672
|
+
|
|
1673
|
+
return (data: unknown): boolean => {
|
|
1674
|
+
const ok = validate(data)
|
|
1675
|
+
|
|
1676
|
+
if (!ok && validate.errors) {
|
|
1677
|
+
const [error] = validate.errors
|
|
1678
|
+
const errorMessage = \`\${error.dataPath.replace('.', '')} \${error.message}\`
|
|
1679
|
+
|
|
1680
|
+
throw new httpErrors.UnprocessableEntity(errorMessage)
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
return true
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
export { validatorCompiler }
|
|
1688
|
+
`,
|
|
1689
|
+
file: `${projectName}/src/network/utils/index.ts`
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
await Promise.all([
|
|
1695
|
+
writeFile(network.response.file, network.response.content),
|
|
1696
|
+
writeFile(network.router.file, network.router.content),
|
|
1697
|
+
writeFile(network.server.file, network.server.content),
|
|
1698
|
+
writeFile(network.routes.docs.file, network.routes.docs.content),
|
|
1699
|
+
writeFile(network.routes.home.file, network.routes.home.content),
|
|
1700
|
+
writeFile(network.routes.index.file, network.routes.index.content),
|
|
1701
|
+
writeFile(network.routes.user.file, network.routes.user.content),
|
|
1702
|
+
writeFile(network.utils.index.file, network.utils.index.content)
|
|
1703
|
+
])
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* @param {Object} args
|
|
1708
|
+
* @param {String} args.projectName
|
|
1709
|
+
*/
|
|
1710
|
+
const fastifyGraphQLNetwork = async ({ projectName }) => {
|
|
1711
|
+
const createFoldersCommand = `mkdir ${projectName}/src/graphQL/models \
|
|
1712
|
+
${projectName}/src/graphQL/models/User \
|
|
1713
|
+
${projectName}/src/graphQL/models/utils \
|
|
1714
|
+
${projectName}/src/graphQL/models/utils/messages`
|
|
1715
|
+
|
|
1716
|
+
if (platform() === 'win32')
|
|
1717
|
+
await exec(createFoldersCommand.replaceAll('/', '\\'))
|
|
1718
|
+
else await exec(createFoldersCommand)
|
|
1719
|
+
|
|
1720
|
+
const graphQL = {
|
|
1721
|
+
index: {
|
|
1722
|
+
content: "export * from './models'\n",
|
|
1723
|
+
file: `${projectName}/src/graphQL/index.ts`
|
|
1724
|
+
},
|
|
1725
|
+
models: {
|
|
1726
|
+
User: {
|
|
1727
|
+
index: {
|
|
1728
|
+
content: `import { makeExecutableSchema } from '@graphql-tools/schema'
|
|
1729
|
+
|
|
1730
|
+
import { User as UserTD } from './typeDefs'
|
|
1731
|
+
import { Query } from './queriesResolver'
|
|
1732
|
+
import { Mutation } from './mutationsResolver'
|
|
1733
|
+
|
|
1734
|
+
const resolvers = {
|
|
1735
|
+
Query,
|
|
1736
|
+
Mutation
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const User = makeExecutableSchema({
|
|
1740
|
+
typeDefs: UserTD,
|
|
1741
|
+
resolvers
|
|
1742
|
+
})
|
|
1743
|
+
|
|
1744
|
+
export { User }
|
|
1745
|
+
`,
|
|
1746
|
+
file: `${projectName}/src/graphQL/models/User/index.ts`
|
|
1747
|
+
},
|
|
1748
|
+
mutations: {
|
|
1749
|
+
content: `import { ApolloError } from 'apollo-server-core'
|
|
1750
|
+
|
|
1751
|
+
import { store, remove, update } from 'database'
|
|
1752
|
+
import { UserDTO } from 'schemas'
|
|
1753
|
+
import { EFU, MFU, GE, errorHandling } from '../utils'
|
|
1754
|
+
|
|
1755
|
+
const storeUser = async (
|
|
1756
|
+
{ user }: { user: UserDTO },
|
|
1757
|
+
{ log }: Context
|
|
1758
|
+
): Promise<UserDTO> => {
|
|
1759
|
+
try {
|
|
1760
|
+
const result = await store(user)
|
|
1761
|
+
|
|
1762
|
+
return result
|
|
1763
|
+
} catch (e) {
|
|
1764
|
+
return errorHandling({
|
|
1765
|
+
e,
|
|
1766
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1767
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1768
|
+
log
|
|
1769
|
+
})
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const deleteAllUsers = async ({ log }: Context): Promise<string> => {
|
|
1774
|
+
try {
|
|
1775
|
+
const usersDeleted = (await remove()) as number
|
|
1776
|
+
|
|
1777
|
+
if (usersDeleted >= 1) return MFU.ALL_USERS_DELETED
|
|
1778
|
+
|
|
1779
|
+
if (usersDeleted === 0)
|
|
1780
|
+
throw new ApolloError(EFU.NOTHING_TO_DELETE, 'NOTHING_TO_DELETE')
|
|
1781
|
+
|
|
1782
|
+
throw new ApolloError(GE.INTERNAL_SERVER_ERROR, 'INTERNAL_SERVER_ERROR')
|
|
1783
|
+
} catch (e) {
|
|
1784
|
+
return errorHandling({
|
|
1785
|
+
e,
|
|
1786
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1787
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1788
|
+
log
|
|
1789
|
+
})
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const updateUser = async (
|
|
1794
|
+
{ user }: { user: UserDTO },
|
|
1795
|
+
{ log }: Context
|
|
1796
|
+
): Promise<UserDTO> => {
|
|
1797
|
+
try {
|
|
1798
|
+
const updatedUser = await update(user)
|
|
1799
|
+
|
|
1800
|
+
if (!updatedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
|
|
1801
|
+
|
|
1802
|
+
return updatedUser
|
|
1803
|
+
} catch (e) {
|
|
1804
|
+
return errorHandling({
|
|
1805
|
+
e,
|
|
1806
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1807
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1808
|
+
log
|
|
1809
|
+
})
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
const deleteUser = async (
|
|
1814
|
+
{ id }: { id: string },
|
|
1815
|
+
{ log }: Context
|
|
1816
|
+
): Promise<string> => {
|
|
1817
|
+
try {
|
|
1818
|
+
const deletedUser = await remove(id)
|
|
1819
|
+
|
|
1820
|
+
if (!deletedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
|
|
1821
|
+
|
|
1822
|
+
return MFU.USER_DELETED
|
|
1823
|
+
} catch (e) {
|
|
1824
|
+
return errorHandling({
|
|
1825
|
+
e,
|
|
1826
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1827
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1828
|
+
log
|
|
1829
|
+
})
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
export { storeUser, deleteAllUsers, updateUser, deleteUser }
|
|
1834
|
+
`,
|
|
1835
|
+
file: `${projectName}/src/graphQL/models/User/mutations.ts`
|
|
1836
|
+
},
|
|
1837
|
+
mutationsResolver: {
|
|
1838
|
+
content: `import { DefinedError } from 'ajv'
|
|
1839
|
+
import { ApolloError } from 'apollo-server-core'
|
|
1840
|
+
|
|
1841
|
+
import { ajv, idSchema, UserDTO } from 'schemas'
|
|
1842
|
+
import { storeUserSchema, updateUserSchema } from './schemas'
|
|
1843
|
+
import { storeUser, updateUser, deleteUser, deleteAllUsers } from './mutations'
|
|
1844
|
+
import { errorHandling, GE } from '../utils'
|
|
1845
|
+
|
|
1846
|
+
const Mutation = {
|
|
1847
|
+
storeUser: async (
|
|
1848
|
+
parent: unknown,
|
|
1849
|
+
{ user }: { user: UserDTO },
|
|
1850
|
+
context: Context
|
|
1851
|
+
): Promise<UserDTO> => {
|
|
1852
|
+
const { log } = context
|
|
1853
|
+
const validate = ajv.compile(storeUserSchema)
|
|
1854
|
+
|
|
1855
|
+
try {
|
|
1856
|
+
const ok = validate(user)
|
|
1857
|
+
|
|
1858
|
+
if (!ok)
|
|
1859
|
+
throw new ApolloError(
|
|
1860
|
+
\`\${(validate.errors as DefinedError[])[0].instancePath.replace(
|
|
1861
|
+
'/',
|
|
1862
|
+
''
|
|
1863
|
+
)} \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
1864
|
+
'UNPROCESSABLE_ENTITY'
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
return await storeUser({ user }, context)
|
|
1868
|
+
} catch (e) {
|
|
1869
|
+
log.error(validate.errors)
|
|
1870
|
+
|
|
1871
|
+
return errorHandling({
|
|
1872
|
+
e,
|
|
1873
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1874
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1875
|
+
log
|
|
1876
|
+
})
|
|
1877
|
+
}
|
|
1878
|
+
},
|
|
1879
|
+
updateUser: async (
|
|
1880
|
+
parent: unknown,
|
|
1881
|
+
{ user }: { user: UserDTO },
|
|
1882
|
+
context: Context
|
|
1883
|
+
): Promise<UserDTO> => {
|
|
1884
|
+
const validate = ajv.compile(updateUserSchema)
|
|
1885
|
+
const { log } = context
|
|
1886
|
+
|
|
1887
|
+
try {
|
|
1888
|
+
const ok = validate(user)
|
|
1889
|
+
|
|
1890
|
+
if (!ok)
|
|
1891
|
+
throw new ApolloError(
|
|
1892
|
+
\`\${(validate.errors as DefinedError[])[0].instancePath.replace(
|
|
1893
|
+
'/',
|
|
1894
|
+
''
|
|
1895
|
+
)} \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
1896
|
+
'UNPROCESSABLE_ENTITY'
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
return await updateUser({ user }, context)
|
|
1900
|
+
} catch (e) {
|
|
1901
|
+
log.error(validate.errors)
|
|
1902
|
+
|
|
1903
|
+
return errorHandling({
|
|
1904
|
+
e,
|
|
1905
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1906
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1907
|
+
log
|
|
1908
|
+
})
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
deleteUser: async (
|
|
1912
|
+
parent: unknown,
|
|
1913
|
+
{ id }: { id: string },
|
|
1914
|
+
context: Context
|
|
1915
|
+
): Promise<string> => {
|
|
1916
|
+
const validate = ajv.compile(idSchema)
|
|
1917
|
+
const { log } = context
|
|
1918
|
+
|
|
1919
|
+
try {
|
|
1920
|
+
const ok = validate({ id })
|
|
1921
|
+
|
|
1922
|
+
if (!ok)
|
|
1923
|
+
throw new ApolloError(
|
|
1924
|
+
\`\${(validate.errors as DefinedError[])[0].instancePath.replace(
|
|
1925
|
+
'/',
|
|
1926
|
+
''
|
|
1927
|
+
)} \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
1928
|
+
'UNPROCESSABLE_ENTITY'
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
return await deleteUser({ id }, context)
|
|
1932
|
+
} catch (e) {
|
|
1933
|
+
log.error(validate.errors)
|
|
1934
|
+
|
|
1935
|
+
return errorHandling({
|
|
1936
|
+
e,
|
|
1937
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1938
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1939
|
+
log
|
|
1940
|
+
})
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
deleteAllUsers: async (
|
|
1944
|
+
parent: unknown,
|
|
1945
|
+
args: unknown,
|
|
1946
|
+
context: Context
|
|
1947
|
+
): Promise<string> => {
|
|
1948
|
+
const { log } = context
|
|
1949
|
+
try {
|
|
1950
|
+
return await deleteAllUsers(context)
|
|
1951
|
+
} catch (e) {
|
|
1952
|
+
return errorHandling({
|
|
1953
|
+
e,
|
|
1954
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1955
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1956
|
+
log
|
|
1957
|
+
})
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
export { Mutation }
|
|
1963
|
+
`,
|
|
1964
|
+
file: `${projectName}/src/graphQL/models/User/mutationsResolver.ts`
|
|
1965
|
+
},
|
|
1966
|
+
queries: {
|
|
1967
|
+
content: `import { ApolloError } from 'apollo-server-core'
|
|
1968
|
+
|
|
1969
|
+
import { get } from 'database'
|
|
1970
|
+
import { UserDTO } from 'schemas'
|
|
1971
|
+
import { EFU, GE, errorHandling } from '../utils'
|
|
1972
|
+
|
|
1973
|
+
const getUsers = async (
|
|
1974
|
+
parent: unknown,
|
|
1975
|
+
args: unknown,
|
|
1976
|
+
{ log }: Context
|
|
1977
|
+
): Promise<UserDTO[]> => {
|
|
1978
|
+
try {
|
|
1979
|
+
const users = (await get()) as UserDTO[]
|
|
1980
|
+
|
|
1981
|
+
return users
|
|
1982
|
+
} catch (e) {
|
|
1983
|
+
return errorHandling({
|
|
1984
|
+
e,
|
|
1985
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
1986
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1987
|
+
log
|
|
1988
|
+
})
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const getUser = async (
|
|
1993
|
+
{ id }: { id: string },
|
|
1994
|
+
{ log }: Context
|
|
1995
|
+
): Promise<UserDTO> => {
|
|
1996
|
+
try {
|
|
1997
|
+
const user = (await get(id as string)) as UserDTO | null
|
|
1998
|
+
|
|
1999
|
+
if (!user) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
|
|
2000
|
+
|
|
2001
|
+
return user
|
|
2002
|
+
} catch (e) {
|
|
2003
|
+
return errorHandling({
|
|
2004
|
+
e,
|
|
2005
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
2006
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
2007
|
+
log
|
|
2008
|
+
})
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
export { getUsers, getUser }
|
|
2013
|
+
`,
|
|
2014
|
+
file: `${projectName}/src/graphQL/models/User/queries.ts`
|
|
2015
|
+
},
|
|
2016
|
+
queriesResolver: {
|
|
2017
|
+
content: `import { DefinedError } from 'ajv'
|
|
2018
|
+
import { ApolloError } from 'apollo-server-core'
|
|
2019
|
+
|
|
2020
|
+
import { ajv, idSchema, UserDTO } from 'schemas'
|
|
2021
|
+
import { getUser, getUsers } from './queries'
|
|
2022
|
+
import { errorHandling, GE } from '../utils'
|
|
2023
|
+
|
|
2024
|
+
const Query = {
|
|
2025
|
+
getUser: async (
|
|
2026
|
+
parent: unknown,
|
|
2027
|
+
{ id }: { id: string },
|
|
2028
|
+
context: Context
|
|
2029
|
+
): Promise<UserDTO> => {
|
|
2030
|
+
const { log } = context
|
|
2031
|
+
const validate = ajv.compile(idSchema)
|
|
2032
|
+
|
|
2033
|
+
try {
|
|
2034
|
+
const ok = validate({ id })
|
|
2035
|
+
|
|
2036
|
+
if (!ok)
|
|
2037
|
+
throw new ApolloError(
|
|
2038
|
+
\`id \${(validate.errors as DefinedError[])[0].message as string}\`,
|
|
2039
|
+
'UNPROCESSABLE_ENTITY'
|
|
2040
|
+
)
|
|
2041
|
+
|
|
2042
|
+
return await getUser({ id }, context)
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
log.error(validate.errors)
|
|
2045
|
+
|
|
2046
|
+
return errorHandling({
|
|
2047
|
+
e,
|
|
2048
|
+
message: GE.INTERNAL_SERVER_ERROR,
|
|
2049
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
2050
|
+
log
|
|
2051
|
+
})
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
getUsers
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
export { Query }
|
|
2058
|
+
`,
|
|
2059
|
+
file: `${projectName}/src/graphQL/models/User/queriesResolver.ts`
|
|
2060
|
+
},
|
|
2061
|
+
schemas: {
|
|
2062
|
+
content: `import { Type } from '@sinclair/typebox'
|
|
2063
|
+
|
|
2064
|
+
const updateUserSchema = Type.Object(
|
|
2065
|
+
{
|
|
2066
|
+
id: Type.String({ minLength: 24, maxLength: 24 }),
|
|
2067
|
+
lastName: Type.String(),
|
|
2068
|
+
name: Type.String()
|
|
2069
|
+
},
|
|
2070
|
+
{ additionalProperties: false }
|
|
2071
|
+
)
|
|
2072
|
+
|
|
2073
|
+
const storeUserSchema = Type.Object(
|
|
2074
|
+
{
|
|
2075
|
+
lastName: Type.String({ minLength: 1 }),
|
|
2076
|
+
name: Type.String({ minLength: 1 })
|
|
2077
|
+
},
|
|
2078
|
+
{ additionalProperties: false }
|
|
2079
|
+
)
|
|
2080
|
+
|
|
2081
|
+
export { updateUserSchema, storeUserSchema }
|
|
2082
|
+
`,
|
|
2083
|
+
file: `${projectName}/src/graphQL/models/User/schemas.ts`
|
|
2084
|
+
},
|
|
2085
|
+
typeDefs: {
|
|
2086
|
+
content: `import { gql } from 'apollo-server-core'
|
|
2087
|
+
|
|
2088
|
+
const User = gql\`
|
|
2089
|
+
type User {
|
|
2090
|
+
id: ID!
|
|
2091
|
+
name: String!
|
|
2092
|
+
lastName: String!
|
|
2093
|
+
updatedAt: String!
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
type Query {
|
|
2097
|
+
getUsers: [User!]!
|
|
2098
|
+
getUser(id: ID!): User!
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
input StoreUserInput {
|
|
2102
|
+
lastName: String!
|
|
2103
|
+
name: String!
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
input UpdateUserInput {
|
|
2107
|
+
id: String!
|
|
2108
|
+
lastName: String!
|
|
2109
|
+
name: String!
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
type Mutation {
|
|
2113
|
+
storeUser(user: StoreUserInput!): User!
|
|
2114
|
+
deleteAllUsers: String
|
|
2115
|
+
updateUser(user: UpdateUserInput!): User!
|
|
2116
|
+
deleteUser(id: ID!): String
|
|
2117
|
+
}
|
|
2118
|
+
\`
|
|
2119
|
+
|
|
2120
|
+
export { User }
|
|
2121
|
+
`,
|
|
2122
|
+
file: `${projectName}/src/graphQL/models/User/typeDefs.ts`
|
|
2123
|
+
}
|
|
2124
|
+
},
|
|
2125
|
+
utils: {
|
|
2126
|
+
messages: {
|
|
2127
|
+
index: {
|
|
2128
|
+
content: `enum GenericErrors {
|
|
2129
|
+
INTERNAL_SERVER_ERROR = 'Something went wrong'
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
export { GenericErrors as GE }
|
|
2133
|
+
export * from './user'
|
|
2134
|
+
`,
|
|
2135
|
+
file: `${projectName}/src/graphQL/models/utils/messages/index.ts`
|
|
2136
|
+
},
|
|
2137
|
+
user: {
|
|
2138
|
+
content: `enum ErrorForUser {
|
|
2139
|
+
NOT_FOUND = 'The requested user does not exists',
|
|
2140
|
+
NOTHING_TO_DELETE = 'There is no user to be deleted'
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
enum MessageForUser {
|
|
2144
|
+
ALL_USERS_DELETED = 'All the users were deleted successfully',
|
|
2145
|
+
USER_DELETED = 'The requested user was successfully deleted'
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
export { ErrorForUser as EFU, MessageForUser as MFU }
|
|
2149
|
+
`,
|
|
2150
|
+
file: `${projectName}/src/graphQL/models/utils/messages/user.ts`
|
|
2151
|
+
}
|
|
2152
|
+
},
|
|
2153
|
+
index: {
|
|
2154
|
+
content: `import { ApolloError } from 'apollo-server-core'
|
|
2155
|
+
import { FastifyLoggerInstance } from 'fastify'
|
|
2156
|
+
|
|
2157
|
+
const errorHandling = ({
|
|
2158
|
+
e,
|
|
2159
|
+
message,
|
|
2160
|
+
code,
|
|
2161
|
+
log
|
|
2162
|
+
}: {
|
|
2163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2164
|
+
e: any
|
|
2165
|
+
message: string
|
|
2166
|
+
code: string
|
|
2167
|
+
log: FastifyLoggerInstance
|
|
2168
|
+
}): never => {
|
|
2169
|
+
log.error(e)
|
|
2170
|
+
|
|
2171
|
+
if (e instanceof ApolloError) throw e
|
|
2172
|
+
|
|
2173
|
+
throw new ApolloError(message ?? e.message, code)
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
export { errorHandling }
|
|
2177
|
+
export * from './messages'
|
|
2178
|
+
`,
|
|
2179
|
+
file: `${projectName}/src/graphQL/models/utils/index.ts`
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
2182
|
+
index: {
|
|
2183
|
+
content: `import { mergeSchemas } from '@graphql-tools/schema'
|
|
2184
|
+
|
|
2185
|
+
import { User } from './User'
|
|
2186
|
+
|
|
2187
|
+
const mergedSchema = mergeSchemas({
|
|
2188
|
+
schemas: [User]
|
|
2189
|
+
})
|
|
2190
|
+
|
|
2191
|
+
export { mergedSchema }
|
|
2192
|
+
`,
|
|
2193
|
+
file: `${projectName}/src/graphQL/models/index.ts`
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
const network = {
|
|
2198
|
+
response: {
|
|
2199
|
+
content: `import { FastifyReply } from 'fastify'
|
|
2200
|
+
|
|
2201
|
+
const response = ({
|
|
2202
|
+
error,
|
|
2203
|
+
message,
|
|
2204
|
+
reply,
|
|
2205
|
+
status
|
|
2206
|
+
}: {
|
|
2207
|
+
error: boolean
|
|
2208
|
+
message: unknown
|
|
2209
|
+
reply: FastifyReply
|
|
2210
|
+
status: number
|
|
2211
|
+
}): void => {
|
|
2212
|
+
reply.code(status).send({ error, message })
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
export { response }
|
|
2216
|
+
`,
|
|
2217
|
+
file: `${projectName}/src/network/response.ts`
|
|
2218
|
+
},
|
|
2219
|
+
router: {
|
|
2220
|
+
content: `import { FastifyInstance } from 'fastify'
|
|
2221
|
+
import { HttpError } from 'http-errors'
|
|
2222
|
+
|
|
2223
|
+
import { response } from './response'
|
|
2224
|
+
import { Home, Docs } from './routes'
|
|
2225
|
+
|
|
2226
|
+
const routers = [Docs]
|
|
2227
|
+
const applyRoutes = (app: FastifyInstance): void => {
|
|
2228
|
+
Home(app)
|
|
2229
|
+
routers.forEach(router => router(app))
|
|
2230
|
+
|
|
2231
|
+
// Handling 404 error
|
|
2232
|
+
app.setNotFoundHandler((request, reply) => {
|
|
2233
|
+
response({
|
|
2234
|
+
error: true,
|
|
2235
|
+
message: 'This route does not exists',
|
|
2236
|
+
reply,
|
|
2237
|
+
status: 404
|
|
2238
|
+
})
|
|
2239
|
+
})
|
|
2240
|
+
app.setErrorHandler<HttpError>((error, request, reply) => {
|
|
2241
|
+
response({
|
|
2242
|
+
error: true,
|
|
2243
|
+
message: error.message,
|
|
2244
|
+
reply,
|
|
2245
|
+
status: error.status ?? 500
|
|
2246
|
+
})
|
|
2247
|
+
})
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
export { applyRoutes }
|
|
2251
|
+
`,
|
|
2252
|
+
file: `${projectName}/src/network/router.ts`
|
|
2253
|
+
},
|
|
2254
|
+
server: {
|
|
2255
|
+
content: `import fastify, { FastifyInstance } from 'fastify'
|
|
2256
|
+
import { ApolloServer } from 'apollo-server-fastify'
|
|
2257
|
+
import {
|
|
2258
|
+
ApolloServerPluginDrainHttpServer,
|
|
2259
|
+
ApolloServerPluginLandingPageGraphQLPlayground,
|
|
2260
|
+
ApolloServerPluginLandingPageDisabled
|
|
2261
|
+
} from 'apollo-server-core'
|
|
2262
|
+
import { ApolloServerPlugin } from 'apollo-server-plugin-base'
|
|
2263
|
+
import mongoose from 'mongoose'
|
|
2264
|
+
|
|
2265
|
+
import { mergedSchema as schema } from 'graphQL'
|
|
2266
|
+
import { applyRoutes } from './router'
|
|
2267
|
+
|
|
2268
|
+
const PORT = process.env.PORT ?? 1996
|
|
2269
|
+
|
|
2270
|
+
class Server {
|
|
2271
|
+
#app: FastifyInstance
|
|
2272
|
+
#connection: mongoose.Connection | undefined
|
|
2273
|
+
|
|
2274
|
+
constructor() {
|
|
2275
|
+
this.#app = fastify({ logger: { prettyPrint: true } })
|
|
2276
|
+
this.#config()
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
#config() {
|
|
2280
|
+
this.#app.addHook('preHandler', (req, reply, done) => {
|
|
2281
|
+
reply.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
|
|
2282
|
+
reply.header('Access-Control-Allow-Origin', '*')
|
|
2283
|
+
reply.header(
|
|
2284
|
+
'Access-Control-Allow-Headers',
|
|
2285
|
+
'Authorization, Content-Type'
|
|
2286
|
+
)
|
|
2287
|
+
reply.header('x-powered-by', 'Simba.js')
|
|
2288
|
+
done()
|
|
2289
|
+
})
|
|
2290
|
+
applyRoutes(this.#app)
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
async #mongo(): Promise<void> {
|
|
2294
|
+
this.#connection = mongoose.connection
|
|
2295
|
+
const connection = {
|
|
2296
|
+
keepAlive: true,
|
|
2297
|
+
useNewUrlParser: true,
|
|
2298
|
+
useUnifiedTopology: true
|
|
2299
|
+
}
|
|
2300
|
+
this.#connection.on('connected', () => {
|
|
2301
|
+
this.#app.log.info('Mongo connection established.')
|
|
2302
|
+
})
|
|
2303
|
+
this.#connection.on('reconnected', () => {
|
|
2304
|
+
this.#app.log.info('Mongo connection reestablished')
|
|
2305
|
+
})
|
|
2306
|
+
this.#connection.on('disconnected', () => {
|
|
2307
|
+
this.#app.log.info('Mongo connection disconnected')
|
|
2308
|
+
this.#app.log.info('Trying to reconnected to Mongo...')
|
|
2309
|
+
setTimeout(() => {
|
|
2310
|
+
mongoose.connect(process.env.MONGO_URI as string, {
|
|
2311
|
+
...connection,
|
|
2312
|
+
connectTimeoutMS: 3000,
|
|
2313
|
+
socketTimeoutMS: 3000
|
|
2314
|
+
})
|
|
2315
|
+
}, 3000)
|
|
2316
|
+
})
|
|
2317
|
+
this.#connection.on('close', () => {
|
|
2318
|
+
this.#app.log.info('Mongo connection closed')
|
|
2319
|
+
})
|
|
2320
|
+
this.#connection.on('error', (e: Error) => {
|
|
2321
|
+
this.#app.log.info('Mongo connection error:')
|
|
2322
|
+
this.#app.log.error(e)
|
|
2323
|
+
})
|
|
2324
|
+
await mongoose.connect(process.env.MONGO_URI as string, connection)
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
#fastifyAppClosePlugin(): ApolloServerPlugin {
|
|
2328
|
+
const app = this.#app
|
|
2329
|
+
|
|
2330
|
+
return {
|
|
2331
|
+
async serverWillStart() {
|
|
2332
|
+
return {
|
|
2333
|
+
async drainServer() {
|
|
2334
|
+
await app.close()
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
public async start(): Promise<void> {
|
|
2342
|
+
const server = new ApolloServer({
|
|
2343
|
+
schema,
|
|
2344
|
+
plugins: [
|
|
2345
|
+
this.#fastifyAppClosePlugin(),
|
|
2346
|
+
ApolloServerPluginDrainHttpServer({ httpServer: this.#app.server }),
|
|
2347
|
+
process.env.NODE_ENV === 'production'
|
|
2348
|
+
? ApolloServerPluginLandingPageDisabled()
|
|
2349
|
+
: ApolloServerPluginLandingPageGraphQLPlayground()
|
|
2350
|
+
],
|
|
2351
|
+
context: (): Context => ({
|
|
2352
|
+
log: this.#app.log
|
|
2353
|
+
})
|
|
2354
|
+
})
|
|
2355
|
+
|
|
2356
|
+
try {
|
|
2357
|
+
await server.start()
|
|
2358
|
+
this.#app.register(
|
|
2359
|
+
server.createHandler({
|
|
2360
|
+
path: '/api'
|
|
2361
|
+
})
|
|
2362
|
+
)
|
|
2363
|
+
await this.#mongo()
|
|
2364
|
+
await this.#app.listen(PORT)
|
|
2365
|
+
this.#app.log.info(
|
|
2366
|
+
\`GraphQL server listening at: http://localhost:\${PORT}\${server.graphqlPath}\`
|
|
2367
|
+
)
|
|
2368
|
+
} catch (e) {
|
|
2369
|
+
console.error(e)
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const server = new Server()
|
|
2375
|
+
|
|
2376
|
+
export { server as Server }
|
|
2377
|
+
`,
|
|
2378
|
+
file: `${projectName}/src/network/server.ts`
|
|
2379
|
+
},
|
|
2380
|
+
routes: {
|
|
2381
|
+
docs: {
|
|
2382
|
+
content: `import { FastifyInstance } from 'fastify'
|
|
2383
|
+
import fastifySwagger from 'fastify-swagger'
|
|
2384
|
+
|
|
2385
|
+
const Docs = (app: FastifyInstance, prefix = '/api'): void => {
|
|
2386
|
+
app.register(fastifySwagger, {
|
|
2387
|
+
routePrefix: \`\${prefix}/docs\`,
|
|
2388
|
+
openapi: {
|
|
2389
|
+
info: {
|
|
2390
|
+
title: 'Test swagger',
|
|
2391
|
+
description: 'Testing the Fastify swagger API',
|
|
2392
|
+
version: '0.1.0',
|
|
2393
|
+
contact: {
|
|
2394
|
+
email: 'sluzquinosa@uni.pe'
|
|
2395
|
+
},
|
|
2396
|
+
license: {
|
|
2397
|
+
name: 'MIT',
|
|
2398
|
+
url: 'https://opensource.org/licenses/MIT'
|
|
2399
|
+
}
|
|
2400
|
+
},
|
|
2401
|
+
servers: [
|
|
2402
|
+
{
|
|
2403
|
+
url: 'http://localhost:1996/api',
|
|
2404
|
+
description: 'test-fastify local API'
|
|
2405
|
+
}
|
|
2406
|
+
],
|
|
2407
|
+
tags: [
|
|
2408
|
+
{
|
|
2409
|
+
name: 'user',
|
|
2410
|
+
description: 'User related endpoints'
|
|
2411
|
+
}
|
|
2412
|
+
]
|
|
2413
|
+
},
|
|
2414
|
+
exposeRoute: true
|
|
2415
|
+
})
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
export { Docs }
|
|
2419
|
+
`,
|
|
2420
|
+
file: `${projectName}/src/network/routes/docs.ts`
|
|
2421
|
+
},
|
|
2422
|
+
home: {
|
|
2423
|
+
content: `import { FastifyInstance } from 'fastify'
|
|
2424
|
+
import { response } from 'network/response'
|
|
2425
|
+
|
|
2426
|
+
const Home = (app: FastifyInstance, prefix = '/'): void => {
|
|
2427
|
+
app.get(\`\${prefix}\`, (request, reply) => {
|
|
2428
|
+
response({
|
|
2429
|
+
error: false,
|
|
2430
|
+
message: 'Welcome to your Fastify GraphQL Backend!',
|
|
2431
|
+
reply,
|
|
2432
|
+
status: 200
|
|
2433
|
+
})
|
|
2434
|
+
})
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
export { Home }
|
|
2438
|
+
`,
|
|
2439
|
+
file: `${projectName}/src/network/routes/home.ts`
|
|
2440
|
+
},
|
|
2441
|
+
index: {
|
|
2442
|
+
content: `export * from './home'
|
|
2443
|
+
export * from './docs'
|
|
2444
|
+
`,
|
|
2445
|
+
file: `${projectName}/src/network/routes/index.ts`
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
await Promise.all([
|
|
2451
|
+
writeFile(graphQL.index.file, graphQL.index.content),
|
|
2452
|
+
writeFile(
|
|
2453
|
+
graphQL.models.User.index.file,
|
|
2454
|
+
graphQL.models.User.index.content
|
|
2455
|
+
),
|
|
2456
|
+
writeFile(
|
|
2457
|
+
graphQL.models.User.mutations.file,
|
|
2458
|
+
graphQL.models.User.mutations.content
|
|
2459
|
+
),
|
|
2460
|
+
writeFile(
|
|
2461
|
+
graphQL.models.User.mutationsResolver.file,
|
|
2462
|
+
graphQL.models.User.mutationsResolver.content
|
|
2463
|
+
),
|
|
2464
|
+
writeFile(
|
|
2465
|
+
graphQL.models.User.queries.file,
|
|
2466
|
+
graphQL.models.User.queries.content
|
|
2467
|
+
),
|
|
2468
|
+
writeFile(
|
|
2469
|
+
graphQL.models.User.queriesResolver.file,
|
|
2470
|
+
graphQL.models.User.queriesResolver.content
|
|
2471
|
+
),
|
|
2472
|
+
writeFile(
|
|
2473
|
+
graphQL.models.User.schemas.file,
|
|
2474
|
+
graphQL.models.User.schemas.content
|
|
2475
|
+
),
|
|
2476
|
+
writeFile(
|
|
2477
|
+
graphQL.models.User.typeDefs.file,
|
|
2478
|
+
graphQL.models.User.typeDefs.content
|
|
2479
|
+
),
|
|
2480
|
+
writeFile(
|
|
2481
|
+
graphQL.models.utils.messages.index.file,
|
|
2482
|
+
graphQL.models.utils.messages.index.content
|
|
2483
|
+
),
|
|
2484
|
+
writeFile(
|
|
2485
|
+
graphQL.models.utils.messages.user.file,
|
|
2486
|
+
graphQL.models.utils.messages.user.content
|
|
2487
|
+
),
|
|
2488
|
+
writeFile(
|
|
2489
|
+
graphQL.models.utils.index.file,
|
|
2490
|
+
graphQL.models.utils.index.content
|
|
2491
|
+
),
|
|
2492
|
+
writeFile(graphQL.models.index.file, graphQL.models.index.content),
|
|
2493
|
+
writeFile(network.response.file, network.response.content),
|
|
2494
|
+
writeFile(network.router.file, network.router.content),
|
|
2495
|
+
writeFile(network.server.file, network.server.content),
|
|
2496
|
+
writeFile(network.routes.docs.file, network.routes.docs.content),
|
|
2497
|
+
writeFile(network.routes.home.file, network.routes.home.content),
|
|
2498
|
+
writeFile(network.routes.index.file, network.routes.index.content)
|
|
2499
|
+
])
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* @param {Object} args
|
|
2504
|
+
* @param {Boolean} args.fastify
|
|
2505
|
+
* @param {String} args.projectName
|
|
2506
|
+
* @param {Boolean} args.graphql
|
|
2507
|
+
*/
|
|
2508
|
+
module.exports = async ({ fastify, projectName, graphql }) => {
|
|
2509
|
+
const createFoldersCommand = `mkdir ${projectName}/src/network \
|
|
2510
|
+
${projectName}/src/network/routes ${
|
|
2511
|
+
graphql ? `${projectName}/src/graphQL` : ''
|
|
2512
|
+
}`
|
|
2513
|
+
|
|
2514
|
+
if (platform() === 'win32')
|
|
2515
|
+
await exec(createFoldersCommand.replaceAll('/', '\\'))
|
|
2516
|
+
else await exec(createFoldersCommand)
|
|
2517
|
+
|
|
2518
|
+
const network = {
|
|
2519
|
+
index: {
|
|
2520
|
+
content: `export * from './routes'
|
|
2521
|
+
export * from './server'
|
|
2522
|
+
`,
|
|
2523
|
+
file: `${projectName}/src/network/index.ts`
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
await writeFile(network.index.file, network.index.content)
|
|
2528
|
+
|
|
2529
|
+
if (fastify && graphql) return await fastifyGraphQLNetwork({ projectName })
|
|
2530
|
+
|
|
2531
|
+
if (fastify) return await fastifyRestNetwork({ projectName })
|
|
2532
|
+
|
|
2533
|
+
if (graphql) return await expressGraphQLNetwork({ projectName })
|
|
2534
|
+
|
|
2535
|
+
return await expressRestNetwork({ projectName })
|
|
2536
|
+
}
|