@anthonylzq/simba.js 6.2.2 → 7.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.
@@ -0,0 +1,1989 @@
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
+ * @param {Boolean} args.graphQL
10
+ * @param {Boolean} args.dbIsSQL
11
+ */
12
+ const types = async ({ projectName, graphQL, dbIsSQL }) => {
13
+ const createFoldersCommand = `mkdir ${projectName}/src/@types ${projectName}/src/@types/custom \
14
+ ${!dbIsSQL ? ` ${projectName}/src/@types/models` : ''} ${
15
+ graphQL ? ` ${projectName}/src/@types/graphQL` : ''
16
+ }`
17
+
18
+ if (platform() === 'win32')
19
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
20
+ else await exec(createFoldersCommand)
21
+
22
+ const types = {
23
+ index: {
24
+ content: `/* eslint-disable no-var */
25
+ declare global {}
26
+
27
+ export {}
28
+ `,
29
+ file: `${projectName}/src/@types/index.d.ts`
30
+ },
31
+ custom: {
32
+ request: {
33
+ content: `type ExpressRequest = import('express').Request
34
+
35
+ interface CustomRequest extends ExpressRequest {
36
+ log: import('express-pino-logger').HttpLogger['logger']
37
+ body: {
38
+ args?: import('schemas').UserDTO
39
+ }
40
+ // We can add custom headers via intersection, remember that for some reason
41
+ // headers must be in Snake-Pascal-Case
42
+ headers: import('http').IncomingHttpHeaders & {
43
+ 'Custom-Header'?: string
44
+ }
45
+ }
46
+ `,
47
+ file: `${projectName}/src/@types/custom/request.d.ts`
48
+ },
49
+ response: {
50
+ content: `type ExpressResponse = import('express').Response
51
+
52
+ interface CustomResponse extends ExpressResponse {
53
+ newValue?: string
54
+ }
55
+ `,
56
+ file: `${projectName}/src/@types/custom/response.d.ts`
57
+ }
58
+ },
59
+ ...(!dbIsSQL && {
60
+ models: {
61
+ user: {
62
+ content: `interface UserDBO {
63
+ name: string
64
+ lastName: string
65
+ createdAt: Date
66
+ updatedAt: Date
67
+ }
68
+ `,
69
+ file: `${projectName}/src/@types/models/user.d.ts`
70
+ }
71
+ }
72
+ }),
73
+ ...(graphQL && {
74
+ graphQL: {
75
+ context: {
76
+ content: `type Context = {
77
+ log: import('express-pino-logger').HttpLogger['logger']
78
+ }
79
+ `,
80
+ file: `${projectName}/src/@types/graphQL/context.d.ts`
81
+ }
82
+ }
83
+ })
84
+ }
85
+ const processes = [
86
+ writeFile(types.index.file, types.index.content),
87
+ writeFile(types.custom.request.file, types.custom.request.content),
88
+ writeFile(types.custom.response.file, types.custom.response.content)
89
+ ]
90
+
91
+ if (!dbIsSQL)
92
+ processes.push(writeFile(types.models.user.file, types.models.user.content))
93
+
94
+ if (graphQL)
95
+ processes.push(
96
+ writeFile(types.graphQL.context.file, types.graphQL.context.content)
97
+ )
98
+
99
+ await Promise.all(processes)
100
+ }
101
+
102
+ /**
103
+ * @param {Object} args
104
+ * @param {String} args.projectName
105
+ */
106
+ const mongo = async ({ projectName }) => {
107
+ const createFoldersCommand = `mkdir ${projectName}/src/database \
108
+ ${projectName}/src/database/mongo ${projectName}/src/database/mongo/models \
109
+ ${projectName}/src/database/mongo/queries`
110
+
111
+ if (platform() === 'win32')
112
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
113
+ else await exec(createFoldersCommand)
114
+
115
+ const database = {
116
+ index: {
117
+ content: "export * from './mongo'\n",
118
+ file: `${projectName}/src/database/index.ts`
119
+ },
120
+ mongo: {
121
+ connection: {
122
+ content: `import { connect, connection } from 'mongoose'
123
+ import { HttpLogger } from 'express-pino-logger'
124
+
125
+ const ENVIRONMENTS_WITHOUT_RECONNECTION = ['ci', 'local']
126
+ const dbConnection = async (
127
+ logger: HttpLogger['logger']
128
+ ): Promise<{
129
+ connect: () => Promise<typeof import('mongoose')>
130
+ disconnect: () => Promise<void>
131
+ }> => {
132
+ const connectionConfig = {
133
+ keepAlive: true,
134
+ useNewUrlParser: true,
135
+ useUnifiedTopology: true
136
+ }
137
+
138
+ connection.on('connected', () => {
139
+ logger.info('Mongo connection established.')
140
+ })
141
+ connection.on('reconnected', () => {
142
+ logger.info('Mongo connection reestablished')
143
+ })
144
+ connection.on('disconnected', () => {
145
+ if (
146
+ !ENVIRONMENTS_WITHOUT_RECONNECTION.includes(
147
+ process.env.NODE_ENV as string
148
+ )
149
+ ) {
150
+ logger.info(
151
+ 'Mongo connection disconnected. Trying to reconnected to Mongo...'
152
+ )
153
+ setTimeout(() => {
154
+ connect(process.env.DB_URI as string, {
155
+ ...connection,
156
+ connectTimeoutMS: 3000,
157
+ socketTimeoutMS: 3000
158
+ })
159
+ }, 3000)
160
+ }
161
+ })
162
+ connection.on('close', () => {
163
+ logger.info('Mongo connection closed')
164
+ })
165
+ connection.on('error', (e: Error) => {
166
+ logger.info('Mongo connection error:')
167
+ logger.error(e)
168
+ })
169
+
170
+ return {
171
+ connect: () => connect(process.env.DB_URI as string, connectionConfig),
172
+ disconnect: () => connection.close()
173
+ }
174
+ }
175
+
176
+ export { dbConnection }
177
+ `,
178
+ file: `${projectName}/src/database/mongo/connection.ts`
179
+ },
180
+ index: {
181
+ content: `export * from './models'
182
+ export * from './queries'
183
+ export * from './connection'
184
+ `,
185
+ file: `${projectName}/src/database/mongo/index.ts`
186
+ },
187
+ models: {
188
+ index: {
189
+ content: "export * from './user'\n",
190
+ file: `${projectName}/src/database/mongo/models/index.ts`
191
+ },
192
+ user: {
193
+ content: `import { model, Schema } from 'mongoose'
194
+
195
+ const UserSchema = new Schema<UserDBO>(
196
+ {
197
+ lastName: {
198
+ required: true,
199
+ type: String
200
+ },
201
+ name: {
202
+ required: true,
203
+ type: String
204
+ }
205
+ },
206
+ {
207
+ timestamps: true,
208
+ versionKey: false,
209
+ toObject: {
210
+ transform: (_, ret) => {
211
+ ret.id = ret._id.toString()
212
+ delete ret._id
213
+ }
214
+ }
215
+ }
216
+ )
217
+
218
+ const UserModel = model<UserDBO>('users', UserSchema)
219
+
220
+ export { UserModel }
221
+ `,
222
+ file: `${projectName}/src/database/mongo/models/user.ts`
223
+ }
224
+ },
225
+ queries: {
226
+ index: {
227
+ content: "export * from './user'\n",
228
+ file: `${projectName}/src/database/mongo/queries/index.ts`
229
+ },
230
+ user: {
231
+ content: `import { Document, MergeType, Types } from 'mongoose'
232
+
233
+ import { UserModel } from '..'
234
+ import { User, UserDTO, UserWithId } from 'schemas'
235
+
236
+ const userDBOtoDTO = (
237
+ userDBO: Document<unknown, unknown, MergeType<UserDBO, UserDBO>> &
238
+ Omit<UserDBO, keyof UserDBO> &
239
+ UserDBO & {
240
+ _id: Types.ObjectId
241
+ }
242
+ ): UserDTO => ({
243
+ ...userDBO.toObject(),
244
+ createdAt: userDBO.createdAt.toISOString(),
245
+ updatedAt: userDBO.updatedAt.toISOString()
246
+ })
247
+
248
+ const store = async (userData: User): Promise<UserDTO> => {
249
+ const user = new UserModel(userData)
250
+
251
+ await user.save()
252
+
253
+ return userDBOtoDTO(user)
254
+ }
255
+
256
+ const remove = async (
257
+ id: string | null = null
258
+ ): Promise<UserDTO | number | null> => {
259
+ if (id) {
260
+ const removedUser = await UserModel.findByIdAndRemove(id)
261
+
262
+ if (!removedUser) return null
263
+
264
+ return userDBOtoDTO(removedUser)
265
+ }
266
+
267
+ return (await UserModel.deleteMany({})).deletedCount
268
+ }
269
+
270
+ const get = async (
271
+ id: string | null = null
272
+ ): Promise<UserDTO[] | UserDTO | null> => {
273
+ if (id) {
274
+ const user = await UserModel.findById(id)
275
+
276
+ return user ? userDBOtoDTO(user) : null
277
+ }
278
+
279
+ const users = await UserModel.find({})
280
+
281
+ return users.map(u => userDBOtoDTO(u))
282
+ }
283
+
284
+ const update = async (userData: UserWithId): Promise<UserDTO | null> => {
285
+ const { id, ...rest } = userData
286
+ const user = await UserModel.findByIdAndUpdate(id, rest, { new: true })
287
+
288
+ return user ? userDBOtoDTO(user) : null
289
+ }
290
+
291
+ export { store, remove, get, update }
292
+ `,
293
+ file: `${projectName}/src/database/mongo/queries/user.ts`
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ await Promise.all([
300
+ writeFile(database.index.file, database.index.content),
301
+ writeFile(
302
+ database.mongo.connection.file,
303
+ database.mongo.connection.content
304
+ ),
305
+ writeFile(database.mongo.index.file, database.mongo.index.content),
306
+ writeFile(
307
+ database.mongo.models.index.file,
308
+ database.mongo.models.index.content
309
+ ),
310
+ writeFile(
311
+ database.mongo.models.user.file,
312
+ database.mongo.models.user.content
313
+ ),
314
+ writeFile(
315
+ database.mongo.queries.index.file,
316
+ database.mongo.queries.index.content
317
+ ),
318
+ writeFile(
319
+ database.mongo.queries.user.file,
320
+ database.mongo.queries.user.content
321
+ )
322
+ ])
323
+ }
324
+
325
+ /**
326
+ * @param {Object} args
327
+ * @param {String} args.projectName
328
+ * @param {import('../../../../').Config['database']} args.db
329
+ */
330
+ const sql = async ({ projectName, db }) => {
331
+ const createFoldersCommand = `mkdir ${projectName}/src/scripts \
332
+ ${projectName}/src/database ${projectName}/src/database/${db} \
333
+ ${projectName}/src/database/${db}/models ${projectName}/src/database/${db}/queries \
334
+ ${projectName}/src/database/migrations`
335
+
336
+ if (platform() === 'win32')
337
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
338
+ else await exec(createFoldersCommand)
339
+
340
+ const database = {
341
+ index: {
342
+ content: `export * from './${db}'\n`,
343
+ file: `${projectName}/src/database/index.ts`
344
+ },
345
+ [db]: {
346
+ config: {
347
+ content: `require('dotenv').config()
348
+
349
+ module.exports = {
350
+ development: {
351
+ url: process.env.DB_URI,
352
+ dialect: 'postgres'
353
+ },
354
+ production: {
355
+ url: process.env.DB_URI,
356
+ dialect: 'postgres'
357
+ }
358
+ }
359
+ `,
360
+ file: `${projectName}/src/database/${db}/config.js`
361
+ },
362
+ connection: {
363
+ content: `import { join } from 'path'
364
+ import { Sequelize } from 'sequelize-typescript'
365
+ import { HttpLogger } from 'express-pino-logger'
366
+
367
+ import * as models from './models'
368
+
369
+ let sequelize: Sequelize
370
+
371
+ const dbConnection = async (
372
+ logger?: HttpLogger['logger']
373
+ ): Promise<{
374
+ connect: () => Promise<Sequelize>
375
+ disconnect: () => Promise<void>
376
+ createMigration: (migrationName: string) => Promise<void>
377
+ }> => {
378
+ return {
379
+ connect: async () => {
380
+ if (!sequelize) {
381
+ sequelize = new Sequelize(process.env.DB_URI as string, {
382
+ models: Object.values(models)
383
+ })
384
+ logger?.info('Postgres connection established.')
385
+ }
386
+
387
+ return sequelize
388
+ },
389
+ disconnect: () => {
390
+ logger?.info('Postgres connection closed.')
391
+
392
+ return sequelize?.close()
393
+ },
394
+ createMigration: async (migrationName: string) => {
395
+ const { SequelizeTypescriptMigration } = await import(
396
+ 'sequelize-typescript-migration-lts'
397
+ )
398
+
399
+ await SequelizeTypescriptMigration.makeMigration(sequelize, {
400
+ outDir: join(__dirname, './migrations'),
401
+ migrationName,
402
+ preview: false
403
+ })
404
+ }
405
+ }
406
+ }
407
+
408
+ export { dbConnection }
409
+ `,
410
+ file: `${projectName}/src/database/${db}/connection.ts`
411
+ },
412
+ index: {
413
+ content: `export * from './connection'
414
+ export * from './models'
415
+ export * from './queries'
416
+ `,
417
+ file: `${projectName}/src/database/${db}/index.ts`
418
+ },
419
+ models: {
420
+ index: {
421
+ content: "export * from './user'\n",
422
+ file: `${projectName}/src/database/${db}/models/index.ts`
423
+ },
424
+ user: {
425
+ content: `import { Model, Column, Table, DataType } from 'sequelize-typescript'
426
+
427
+ @Table({
428
+ paranoid: true,
429
+ tableName: 'users'
430
+ })
431
+ class User extends Model {
432
+ @Column({
433
+ type: DataType.STRING
434
+ })
435
+ name!: string
436
+
437
+ @Column({
438
+ type: DataType.STRING
439
+ })
440
+ lastName!: string
441
+
442
+ @Column({
443
+ type: DataType.STRING
444
+ })
445
+ email!: string
446
+ }
447
+
448
+ export { User }
449
+ `,
450
+ file: `${projectName}/src/database/${db}/models/user.ts`
451
+ }
452
+ },
453
+ queries: {
454
+ index: {
455
+ content: "export * from './user'\n",
456
+ file: `${projectName}/src/database/${db}/queries/index.ts`
457
+ },
458
+ user: {
459
+ content: `import { User } from '..'
460
+ import { User as UserSchema, UserDTO, UserWithId } from 'schemas'
461
+ import { Transaction } from 'sequelize/types'
462
+
463
+ const userDBOtoDTO = (userDBO: User): UserDTO => ({
464
+ ...userDBO.get(),
465
+ createdAt: userDBO.createdAt.toISOString(),
466
+ updatedAt: userDBO.updatedAt.toISOString()
467
+ })
468
+
469
+ const store = async (
470
+ userData: UserSchema,
471
+ transaction: Transaction | null = null
472
+ ): Promise<UserDTO> => {
473
+ const user = await User.create(userData, {
474
+ transaction
475
+ })
476
+
477
+ return userDBOtoDTO(user)
478
+ }
479
+
480
+ const remove = async (
481
+ id: number | null = null,
482
+ transaction: Transaction | null = null
483
+ ): Promise<number | null> => {
484
+ if (id) {
485
+ const removedUser = await User.destroy({
486
+ where: { id },
487
+ transaction
488
+ })
489
+
490
+ return removedUser
491
+ }
492
+
493
+ const w = await User.destroy({ truncate: true, transaction })
494
+
495
+ return w
496
+ }
497
+
498
+ const get = async (
499
+ id: number | null = null
500
+ ): Promise<UserDTO[] | UserDTO | null> => {
501
+ if (id) {
502
+ const user = await User.findByPk(id)
503
+
504
+ return user ? userDBOtoDTO(user) : null
505
+ }
506
+
507
+ const { rows: users } = await User.findAndCountAll()
508
+
509
+ return users.map(u => userDBOtoDTO(u))
510
+ }
511
+
512
+ const update = async (userData: UserWithId): Promise<UserDTO | null> => {
513
+ const { id, ...rest } = userData
514
+ const [, user] = await User.update(rest, {
515
+ where: { id },
516
+ returning: true,
517
+ limit: 1
518
+ })
519
+
520
+ return user[0] ? userDBOtoDTO(user[0]) : null
521
+ }
522
+
523
+ export { store, remove, get, update }
524
+ `,
525
+ file: `${projectName}/src/database/${db}/queries/user.ts`
526
+ }
527
+ }
528
+ },
529
+ scripts: {
530
+ migration: {
531
+ content: `import { dbConnection } from 'database'
532
+ import { promisify } from 'util'
533
+
534
+ const exec = promisify(require('child_process').exec)
535
+
536
+ const migration = async () => {
537
+ const connection = await dbConnection()
538
+
539
+ await connection.connect()
540
+
541
+ console.log('Creating migration')
542
+
543
+ if (process.env.MIGRATION)
544
+ await connection.createMigration(process.env.MIGRATION)
545
+
546
+ console.log('Executing migration')
547
+
548
+ await exec(
549
+ 'yarn migrations:run:last && eslint src/database/* --ext .js --fix'
550
+ )
551
+
552
+ console.log('Migration complete')
553
+ }
554
+
555
+ migration()
556
+ `,
557
+ file: `${projectName}/src/scripts/migration.ts`
558
+ }
559
+ },
560
+ sequelizerc: {
561
+ content: `module.exports = {
562
+ config: './src/database/postgres/config.js',
563
+ 'migrations-path': './src/database/postgres/migrations/'
564
+ }
565
+ `,
566
+ file: `${projectName}/.sequelizerc`
567
+ }
568
+ }
569
+
570
+ await Promise.all([
571
+ writeFile(database.index.file, database.index.content),
572
+ writeFile(database[db].config.file, database[db].config.content),
573
+ writeFile(database[db].connection.file, database[db].connection.content),
574
+ writeFile(database[db].index.file, database[db].index.content),
575
+ writeFile(
576
+ database[db].models.index.file,
577
+ database[db].models.index.content
578
+ ),
579
+ writeFile(database[db].models.user.file, database[db].models.user.content),
580
+ writeFile(
581
+ database[db].queries.index.file,
582
+ database[db].queries.index.content
583
+ ),
584
+ writeFile(
585
+ database[db].queries.user.file,
586
+ database[db].queries.user.content
587
+ ),
588
+ writeFile(
589
+ database.scripts.migration.file,
590
+ database.scripts.migration.content
591
+ ),
592
+ writeFile(database.sequelizerc.file, database.sequelizerc.content)
593
+ ])
594
+ }
595
+
596
+ /**
597
+ * @param {Object} args
598
+ * @param {String} args.projectName
599
+ * @param {String} args.graphQL
600
+ * @param {String} args.dbIsSQL
601
+ */
602
+ const network = async ({ projectName, graphQL, dbIsSQL }) => {
603
+ const createFoldersCommand = `mkdir ${projectName}/src/network \
604
+ ${projectName}/src/network/routes ${projectName}/src/network/routes/utils ${
605
+ graphQL
606
+ ? ` ${projectName}/src/graphQL ${projectName}/src/graphQL/models \
607
+ ${projectName}/src/graphQL/models/User ${projectName}/src/graphQL/models/utils \
608
+ ${projectName}/src/graphQL/models/utils/messages`
609
+ : ''
610
+ }`
611
+
612
+ if (platform() === 'win32')
613
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
614
+ else await exec(createFoldersCommand)
615
+
616
+ const network = {
617
+ index: {
618
+ content: `export * from './routes'
619
+ export * from './server'
620
+ `,
621
+ file: `${projectName}/src/network/index.ts`
622
+ },
623
+ response: {
624
+ content: `interface ResponseProps {
625
+ error: boolean
626
+ message: unknown
627
+ res: CustomResponse
628
+ status: number
629
+ }
630
+
631
+ const response = ({ error, message, res, status }: ResponseProps): void => {
632
+ res.status(status).send({ error, message })
633
+ }
634
+
635
+ export { response }
636
+ `,
637
+ file: `${projectName}/src/network/response.ts`
638
+ },
639
+ router: {
640
+ content: `import { Application, Response, Request, Router, NextFunction } from 'express'
641
+ import swaggerUi from 'swagger-ui-express'
642
+ import httpErrors from 'http-errors'
643
+
644
+ import { response } from './response'
645
+ ${
646
+ graphQL
647
+ ? "import { Home } from './routes'"
648
+ : "import { Home, User } from './routes'"
649
+ }
650
+ import { docs } from 'utils'
651
+
652
+ ${graphQL ? 'const routers: Router[] = []' : 'const routers = [User]'}
653
+ const applyRoutes = (app: Application): void => {
654
+ app.use('/', Home)
655
+ app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(docs))
656
+ routers.forEach((router: Router): Application => app.use('/api', router))
657
+
658
+ // Handling 404 error
659
+ app.use((req, res, next) => {
660
+ next(new httpErrors.NotFound('This route does not exists'))
661
+ })
662
+ app.use(
663
+ (
664
+ error: httpErrors.HttpError,
665
+ req: Request,
666
+ res: Response,
667
+ next: NextFunction
668
+ ) => {
669
+ response({
670
+ error: true,
671
+ message: error.message,
672
+ res,
673
+ status: error.status
674
+ })
675
+ next()
676
+ }
677
+ )
678
+ }
679
+
680
+ export { applyRoutes }
681
+ `,
682
+ file: `${projectName}/src/network/router.ts`
683
+ },
684
+ server: {
685
+ content: graphQL
686
+ ? `import { createServer, Server as HttpServer } from 'http'
687
+ import express from 'express'
688
+ import cors from 'cors'
689
+ import pino, { HttpLogger } from 'express-pino-logger'
690
+ import { ApolloServer } from 'apollo-server-express'
691
+ import {
692
+ ApolloServerPluginDrainHttpServer,
693
+ ApolloServerPluginLandingPageDisabled,
694
+ ApolloServerPluginLandingPageGraphQLPlayground
695
+ } from 'apollo-server-core'
696
+
697
+ import { dbConnection } from 'database'
698
+ import { mergedSchema as schema } from 'graphQL'
699
+ import { applyRoutes } from './router'
700
+
701
+ const PORT = (process.env.PORT as string) || 1996
702
+ const ENVIRONMENTS_WITHOUT_PRETTY_PRINT = ['production', 'ci']
703
+
704
+ class Server {
705
+ #app: express.Application
706
+ #log: HttpLogger
707
+ #server: HttpServer
708
+ #connection: Awaited<ReturnType<typeof dbConnection>> | undefined
709
+
710
+ constructor() {
711
+ this.#app = express()
712
+ this.#server = createServer(this.#app)
713
+ this.#log = pino({
714
+ transport: !ENVIRONMENTS_WITHOUT_PRETTY_PRINT.includes(
715
+ process.env.NODE_ENV as string
716
+ )
717
+ ? {
718
+ target: 'pino-pretty',
719
+ options: {
720
+ translateTime: 'HH:MM:ss Z',
721
+ ignore: 'pid,hostname'
722
+ }
723
+ }
724
+ : undefined
725
+ })
726
+ this.#config()
727
+ }
728
+
729
+ #config() {
730
+ this.#app.use(cors())
731
+ this.#app.use(express.json())
732
+ this.#app.use(express.urlencoded({ extended: false }))
733
+ this.#app.use(this.#log)
734
+ this.#app.use(
735
+ (
736
+ req: express.Request,
737
+ res: express.Response,
738
+ next: express.NextFunction
739
+ ) => {
740
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
741
+ res.header('Access-Control-Allow-Origin', '*')
742
+ res.header(
743
+ 'Access-Control-Allow-Headers',
744
+ 'Authorization, Content-Type'
745
+ )
746
+ res.header('x-powered-by', 'Simba.js')
747
+ next()
748
+ }
749
+ )
750
+ }
751
+
752
+ async #dbConnection() {
753
+ this.#connection = await dbConnection(this.#log.logger)
754
+ }
755
+
756
+ public async start(): Promise<void> {
757
+ const server = new ApolloServer({
758
+ schema,
759
+ plugins: [
760
+ ApolloServerPluginDrainHttpServer({ httpServer: this.#server }),
761
+ process.env.NODE_ENV === 'production'
762
+ ? ApolloServerPluginLandingPageDisabled()
763
+ : ApolloServerPluginLandingPageGraphQLPlayground()
764
+ ],
765
+ context: (): Context => ({
766
+ log: this.#log.logger
767
+ })
768
+ })
769
+
770
+ try {
771
+ await server.start()
772
+ server.applyMiddleware({
773
+ app: this.#app,
774
+ path: '/api'
775
+ })
776
+ applyRoutes(this.#app)
777
+ await this.#dbConnection()
778
+ await this.#connection?.connect()
779
+ this.#server.listen(PORT, () => {
780
+ this.#log.logger.info(\`Server listening at port: \${PORT}\`)
781
+ this.#log.logger.info(
782
+ \`GraphQL server listening at: http://localhost:\${PORT}\${server.graphqlPath}\`
783
+ )
784
+ })
785
+ } catch (e) {
786
+ console.error(e)
787
+ }
788
+ }
789
+
790
+ public async stop(): Promise<void> {
791
+ try {
792
+ await this.#connection?.disconnect()
793
+ this.#server?.close()
794
+ } catch (e) {
795
+ this.#log.logger.error(e)
796
+ }
797
+ }
798
+ }
799
+
800
+ const server = new Server()
801
+
802
+ export { server as Server }
803
+ `
804
+ : `import { Server as HttpServer } from 'http'
805
+ import express from 'express'
806
+ import cors from 'cors'
807
+ import pino, { HttpLogger } from 'express-pino-logger'
808
+
809
+ import { dbConnection } from 'database'
810
+ import { applyRoutes } from './router'
811
+
812
+ const PORT = (process.env.PORT as string) || 1996
813
+ const ENVIRONMENTS_WITHOUT_PRETTY_PRINT = ['production', 'ci']
814
+
815
+ class Server {
816
+ #app: express.Application
817
+ #log: HttpLogger
818
+ #server: HttpServer | undefined
819
+ #connection: Awaited<ReturnType<typeof dbConnection>> | undefined
820
+
821
+ constructor() {
822
+ this.#app = express()
823
+ this.#log = pino({
824
+ transport: !ENVIRONMENTS_WITHOUT_PRETTY_PRINT.includes(
825
+ process.env.NODE_ENV as string
826
+ )
827
+ ? {
828
+ target: 'pino-pretty',
829
+ options: {
830
+ translateTime: 'HH:MM:ss Z',
831
+ ignore: 'pid,hostname'
832
+ }
833
+ }
834
+ : undefined
835
+ })
836
+ this.#config()
837
+ }
838
+
839
+ #config() {
840
+ this.#app.use(cors())
841
+ this.#app.use(express.json())
842
+ this.#app.use(express.urlencoded({ extended: false }))
843
+ this.#app.use(this.#log)
844
+ this.#app.use(
845
+ (
846
+ req: express.Request,
847
+ res: express.Response,
848
+ next: express.NextFunction
849
+ ) => {
850
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
851
+ res.header('Access-Control-Allow-Origin', '*')
852
+ res.header(
853
+ 'Access-Control-Allow-Headers',
854
+ 'Authorization, Content-Type'
855
+ )
856
+ res.header('x-powered-by', 'Simba.js')
857
+ next()
858
+ }
859
+ )
860
+ applyRoutes(this.#app)
861
+ }
862
+
863
+ async #dbConnection() {
864
+ this.#connection = await dbConnection(this.#log.logger)
865
+ }
866
+
867
+ public async start(): Promise<void> {
868
+ try {
869
+ await this.#dbConnection()
870
+ await this.#connection?.connect()
871
+ this.#server = this.#app.listen(PORT, () => {
872
+ this.#log.logger.info(\`Server running at port \${PORT}\`)
873
+ })
874
+ } catch (e) {
875
+ this.#log.logger.error(e)
876
+ }
877
+ }
878
+
879
+ public async stop(): Promise<void> {
880
+ try {
881
+ await this.#connection?.disconnect()
882
+ this.#server?.close()
883
+ } catch (e) {
884
+ this.#log.logger.error(e)
885
+ }
886
+ }
887
+ }
888
+
889
+ const server = new Server()
890
+
891
+ export { server as Server }
892
+ `,
893
+ file: `${projectName}/src/network/server.ts`
894
+ },
895
+ routes: {
896
+ home: {
897
+ content: `import { Response, Request, Router } from 'express'
898
+
899
+ import { response } from 'network/response'
900
+
901
+ const Home = Router()
902
+
903
+ Home.route('').get((req: Request, res: Response) => {
904
+ response({
905
+ error: false,
906
+ message: 'Welcome to your Express Backend!',
907
+ res,
908
+ status: 200
909
+ })
910
+ })
911
+
912
+ export { Home }
913
+ `,
914
+ file: `${projectName}/src/network/routes/home.ts`
915
+ },
916
+ index: {
917
+ content: `export * from './home'
918
+ ${graphQL ? '' : "export * from './user'\n"}`,
919
+ file: `${projectName}/src/network/routes/index.ts`
920
+ },
921
+ ...(!graphQL && {
922
+ user: {
923
+ content: `import { NextFunction, Router } from 'express'
924
+
925
+ import { response } from 'network/response'
926
+ import { UserService } from 'services'
927
+ import { idSchema, storeUserDto, UserWithId } from 'schemas'
928
+ import { validatorCompiler } from './utils'
929
+
930
+ const User = Router()
931
+
932
+ User.route('/users')
933
+ .post(
934
+ validatorCompiler(storeUserDto, 'body'),
935
+ async (
936
+ req: CustomRequest,
937
+ res: CustomResponse,
938
+ next: NextFunction
939
+ ): Promise<void> => {
940
+ try {
941
+ const {
942
+ body: { args: user }
943
+ } = req
944
+ const us = new UserService({ user })
945
+ const result = await us.process({ type: 'store' })
946
+
947
+ response({ error: false, message: result, res, status: 201 })
948
+ } catch (error) {
949
+ next(error)
950
+ }
951
+ }
952
+ )
953
+ .get(
954
+ async (
955
+ req: CustomRequest,
956
+ res: CustomResponse,
957
+ next: NextFunction
958
+ ): Promise<void> => {
959
+ try {
960
+ const us = new UserService()
961
+ const result = await us.process({ type: 'getAll' })
962
+
963
+ response({ error: false, message: result, res, status: 200 })
964
+ } catch (error) {
965
+ next(error)
966
+ }
967
+ }
968
+ )
969
+ .delete(
970
+ async (
971
+ req: CustomRequest,
972
+ res: CustomResponse,
973
+ next: NextFunction
974
+ ): Promise<void> => {
975
+ try {
976
+ const us = new UserService()
977
+ const result = await us.process({ type: 'deleteAll' })
978
+
979
+ response({ error: false, message: result, res, status: 200 })
980
+ } catch (error) {
981
+ next(error)
982
+ }
983
+ }
984
+ )
985
+
986
+ User.route('/user/:id')
987
+ .get(
988
+ validatorCompiler(idSchema, 'params'),
989
+ async (
990
+ req: CustomRequest,
991
+ res: CustomResponse,
992
+ next: NextFunction
993
+ ): Promise<void> => {
994
+ try {
995
+ const {
996
+ params: { id }
997
+ } = req
998
+ ${
999
+ dbIsSQL
1000
+ ? 'const us = new UserService({ id: parseInt(id) })'
1001
+ : 'const us = new UserService({ id })'
1002
+ }
1003
+ const result = await us.process({ type: 'getOne' })
1004
+
1005
+ response({ error: false, message: result, res, status: 200 })
1006
+ } catch (error) {
1007
+ next(error)
1008
+ }
1009
+ }
1010
+ )
1011
+ .patch(
1012
+ validatorCompiler(idSchema, 'params'),
1013
+ validatorCompiler(storeUserDto, 'body'),
1014
+ async (
1015
+ req: CustomRequest,
1016
+ res: CustomResponse,
1017
+ next: NextFunction
1018
+ ): Promise<void> => {
1019
+ try {
1020
+ const {
1021
+ body: { args },
1022
+ params: { id }
1023
+ } = req
1024
+ const userWithId = {
1025
+ id,
1026
+ ...args
1027
+ } as UserWithId
1028
+ const us = new UserService({ userWithId })
1029
+ const result = await us.process({ type: 'update' })
1030
+
1031
+ response({ error: false, message: result, res, status: 200 })
1032
+ } catch (error) {
1033
+ next(error)
1034
+ }
1035
+ }
1036
+ )
1037
+ .delete(
1038
+ validatorCompiler(idSchema, 'params'),
1039
+ async (
1040
+ req: CustomRequest,
1041
+ res: CustomResponse,
1042
+ next: NextFunction
1043
+ ): Promise<void> => {
1044
+ try {
1045
+ const {
1046
+ params: { id }
1047
+ } = req
1048
+ ${
1049
+ dbIsSQL
1050
+ ? 'const us = new UserService({ id: parseInt(id) })'
1051
+ : 'const us = new UserService({ id })'
1052
+ }
1053
+ const result = await us.process({ type: 'delete' })
1054
+
1055
+ response({ error: false, message: result, res, status: 200 })
1056
+ } catch (error) {
1057
+ next(error)
1058
+ }
1059
+ }
1060
+ )
1061
+
1062
+ export { User }
1063
+ `,
1064
+ file: `${projectName}/src/network/routes/user.ts`
1065
+ }
1066
+ }),
1067
+ utils: {
1068
+ index: {
1069
+ content: `import { NextFunction } from 'express'
1070
+ import httpErrors from 'http-errors'
1071
+ import { TObject, TProperties } from '@sinclair/typebox'
1072
+ import Ajv from 'ajv'
1073
+
1074
+ const ajv = new Ajv({
1075
+ removeAdditional: true,
1076
+ useDefaults: true,
1077
+ coerceTypes: true,
1078
+ nullable: true
1079
+ })
1080
+
1081
+ type Middleware = (
1082
+ req: CustomRequest,
1083
+ res: CustomResponse,
1084
+ next: NextFunction
1085
+ ) => void
1086
+
1087
+ const validatorCompiler = <T extends TProperties>(
1088
+ schema: TObject<T>,
1089
+ value: 'body' | 'params'
1090
+ ): Middleware => {
1091
+ return (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
1092
+ const validate = ajv.compile(schema)
1093
+ const ok = validate(req[value])
1094
+
1095
+ if (!ok && validate.errors) {
1096
+ const [error] = validate.errors
1097
+ const errorMessage = \`\${error.dataPath.replace('.', '')} \${error.message}\`
1098
+
1099
+ return next(new httpErrors.UnprocessableEntity(errorMessage))
1100
+ }
1101
+
1102
+ next()
1103
+ }
1104
+ }
1105
+
1106
+ export { validatorCompiler }
1107
+ `,
1108
+ file: `${projectName}/src/network/routes/utils/index.ts`
1109
+ }
1110
+ }
1111
+ },
1112
+ ...(graphQL && {
1113
+ graphQL: {
1114
+ index: {
1115
+ content: "export * from './models'\n",
1116
+ file: `${projectName}/src/graphQL/index.ts`
1117
+ },
1118
+ models: {
1119
+ User: {
1120
+ index: {
1121
+ content: `import { makeExecutableSchema } from '@graphql-tools/schema'
1122
+
1123
+ import { User as UserTD } from './typeDefs'
1124
+ import { Query } from './queriesResolver'
1125
+ import { Mutation } from './mutationsResolver'
1126
+
1127
+ const resolvers = {
1128
+ Query,
1129
+ Mutation
1130
+ }
1131
+
1132
+ const User = makeExecutableSchema({
1133
+ typeDefs: UserTD,
1134
+ resolvers
1135
+ })
1136
+
1137
+ export { User }
1138
+ `,
1139
+ file: `${projectName}/src/graphQL/models/User/index.ts`
1140
+ },
1141
+ mutations: {
1142
+ content: `import { ApolloError } from 'apollo-server-core'
1143
+
1144
+ import { store, remove, update } from 'database'
1145
+ import { User, UserDTO, UserWithId } from 'schemas'
1146
+ import { EFU, MFU, GE, errorHandling } from '../utils'
1147
+
1148
+ const storeUser = async (
1149
+ { user }: { user: User },
1150
+ { log }: Context
1151
+ ): Promise<UserDTO> => {
1152
+ try {
1153
+ const result = await store(user)
1154
+
1155
+ return result
1156
+ } catch (e) {
1157
+ return errorHandling({
1158
+ e,
1159
+ message: GE.INTERNAL_SERVER_ERROR,
1160
+ code: 'INTERNAL_SERVER_ERROR',
1161
+ log
1162
+ })
1163
+ }
1164
+ }
1165
+
1166
+ const deleteAllUsers = async ({ log }: Context): Promise<string> => {
1167
+ try {
1168
+ const usersDeleted = (await remove()) as number
1169
+
1170
+ if (usersDeleted >= 1) return MFU.ALL_USERS_DELETED
1171
+
1172
+ if (usersDeleted === 0)
1173
+ throw new ApolloError(EFU.NOTHING_TO_DELETE, 'NOTHING_TO_DELETE')
1174
+
1175
+ throw new ApolloError(GE.INTERNAL_SERVER_ERROR, 'INTERNAL_SERVER_ERROR')
1176
+ } catch (e) {
1177
+ return errorHandling({
1178
+ e,
1179
+ message: GE.INTERNAL_SERVER_ERROR,
1180
+ code: 'INTERNAL_SERVER_ERROR',
1181
+ log
1182
+ })
1183
+ }
1184
+ }
1185
+
1186
+ const updateUser = async (
1187
+ { user }: { user: UserWithId },
1188
+ { log }: Context
1189
+ ): Promise<UserDTO> => {
1190
+ try {
1191
+ const updatedUser = await update(user)
1192
+
1193
+ if (!updatedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
1194
+
1195
+ return updatedUser
1196
+ } catch (e) {
1197
+ return errorHandling({
1198
+ e,
1199
+ message: GE.INTERNAL_SERVER_ERROR,
1200
+ code: 'INTERNAL_SERVER_ERROR',
1201
+ log
1202
+ })
1203
+ }
1204
+ }
1205
+
1206
+ const deleteUser = async (
1207
+ { id }: { id: ${dbIsSQL ? 'number' : 'string'} },
1208
+ { log }: Context
1209
+ ): Promise<string> => {
1210
+ try {
1211
+ const deletedUser = await remove(id)
1212
+
1213
+ if (!deletedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
1214
+
1215
+ return MFU.USER_DELETED
1216
+ } catch (e) {
1217
+ return errorHandling({
1218
+ e,
1219
+ message: GE.INTERNAL_SERVER_ERROR,
1220
+ code: 'INTERNAL_SERVER_ERROR',
1221
+ log
1222
+ })
1223
+ }
1224
+ }
1225
+
1226
+ export { storeUser, deleteAllUsers, updateUser, deleteUser }
1227
+ `,
1228
+ file: `${projectName}/src/graphQL/models/User/mutations.ts`
1229
+ },
1230
+ mutationsResolver: {
1231
+ content: `import { DefinedError } from 'ajv'
1232
+ import { ApolloError } from 'apollo-server-core'
1233
+
1234
+ import {
1235
+ ajv,
1236
+ idSchema,
1237
+ User,
1238
+ user as storeUserSchema,
1239
+ UserDTO,
1240
+ UserWithId,
1241
+ userWithId as updateUserSchema
1242
+ } from 'schemas'
1243
+ import { storeUser, updateUser, deleteUser, deleteAllUsers } from './mutations'
1244
+ import { errorHandling, GE } from '../utils'
1245
+
1246
+ const Mutation = {
1247
+ storeUser: async (
1248
+ parent: unknown,
1249
+ { user }: { user: User },
1250
+ context: Context
1251
+ ): Promise<UserDTO> => {
1252
+ const { log } = context
1253
+ const validate = ajv.compile(storeUserSchema)
1254
+
1255
+ try {
1256
+ const ok = validate(user)
1257
+
1258
+ if (!ok)
1259
+ throw new ApolloError(
1260
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
1261
+ '/',
1262
+ ''
1263
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
1264
+ 'UNPROCESSABLE_ENTITY'
1265
+ )
1266
+
1267
+ return await storeUser({ user }, context)
1268
+ } catch (e) {
1269
+ log.error(validate.errors)
1270
+
1271
+ return errorHandling({
1272
+ e,
1273
+ message: GE.INTERNAL_SERVER_ERROR,
1274
+ code: 'INTERNAL_SERVER_ERROR',
1275
+ log
1276
+ })
1277
+ }
1278
+ },
1279
+ updateUser: async (
1280
+ parent: unknown,
1281
+ { user }: { user: UserWithId },
1282
+ context: Context
1283
+ ): Promise<UserDTO> => {
1284
+ const validate = ajv.compile(updateUserSchema)
1285
+ const { log } = context
1286
+
1287
+ try {
1288
+ const ok = validate(user)
1289
+
1290
+ if (!ok)
1291
+ throw new ApolloError(
1292
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
1293
+ '/',
1294
+ ''
1295
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
1296
+ 'UNPROCESSABLE_ENTITY'
1297
+ )
1298
+
1299
+ return await updateUser({ user }, context)
1300
+ } catch (e) {
1301
+ log.error(validate.errors)
1302
+
1303
+ return errorHandling({
1304
+ e,
1305
+ message: GE.INTERNAL_SERVER_ERROR,
1306
+ code: 'INTERNAL_SERVER_ERROR',
1307
+ log
1308
+ })
1309
+ }
1310
+ },
1311
+ deleteUser: async (
1312
+ parent: unknown,
1313
+ { id }: { id: ${dbIsSQL ? 'number' : 'string'} },
1314
+ context: Context
1315
+ ): Promise<string> => {
1316
+ const validate = ajv.compile(idSchema)
1317
+ const { log } = context
1318
+
1319
+ try {
1320
+ const ok = validate({ id })
1321
+
1322
+ if (!ok)
1323
+ throw new ApolloError(
1324
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
1325
+ '/',
1326
+ ''
1327
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
1328
+ 'UNPROCESSABLE_ENTITY'
1329
+ )
1330
+
1331
+ return await deleteUser({ id }, context)
1332
+ } catch (e) {
1333
+ log.error(validate.errors)
1334
+
1335
+ return errorHandling({
1336
+ e,
1337
+ message: GE.INTERNAL_SERVER_ERROR,
1338
+ code: 'INTERNAL_SERVER_ERROR',
1339
+ log
1340
+ })
1341
+ }
1342
+ },
1343
+ deleteAllUsers: async (
1344
+ parent: unknown,
1345
+ args: unknown,
1346
+ context: Context
1347
+ ): Promise<string> => {
1348
+ const { log } = context
1349
+ try {
1350
+ return await deleteAllUsers(context)
1351
+ } catch (e) {
1352
+ return errorHandling({
1353
+ e,
1354
+ message: GE.INTERNAL_SERVER_ERROR,
1355
+ code: 'INTERNAL_SERVER_ERROR',
1356
+ log
1357
+ })
1358
+ }
1359
+ }
1360
+ }
1361
+
1362
+ export { Mutation }
1363
+ `,
1364
+ file: `${projectName}/src/graphQL/models/User/mutationsResolver.ts`
1365
+ },
1366
+ queries: {
1367
+ content: `import { ApolloError } from 'apollo-server-core'
1368
+
1369
+ import { get } from 'database'
1370
+ import { UserDTO } from 'schemas'
1371
+ import { EFU, GE, errorHandling } from '../utils'
1372
+
1373
+ const getUsers = async (
1374
+ parent: unknown,
1375
+ args: unknown,
1376
+ { log }: Context
1377
+ ): Promise<UserDTO[]> => {
1378
+ try {
1379
+ const users = (await get()) as UserDTO[]
1380
+
1381
+ return users
1382
+ } catch (e) {
1383
+ return errorHandling({
1384
+ e,
1385
+ message: GE.INTERNAL_SERVER_ERROR,
1386
+ code: 'INTERNAL_SERVER_ERROR',
1387
+ log
1388
+ })
1389
+ }
1390
+ }
1391
+
1392
+ const getUser = async (
1393
+ { id }: { id: ${dbIsSQL ? 'number' : 'string'} },
1394
+ { log }: Context
1395
+ ): Promise<UserDTO> => {
1396
+ try {
1397
+ const user = (await get(id${dbIsSQL ? '' : ' as string'})) as UserDTO | null
1398
+
1399
+ if (!user) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
1400
+
1401
+ return user
1402
+ } catch (e) {
1403
+ return errorHandling({
1404
+ e,
1405
+ message: GE.INTERNAL_SERVER_ERROR,
1406
+ code: 'INTERNAL_SERVER_ERROR',
1407
+ log
1408
+ })
1409
+ }
1410
+ }
1411
+
1412
+ export { getUsers, getUser }
1413
+ `,
1414
+ file: `${projectName}/src/graphQL/models/User/queries.ts`
1415
+ },
1416
+ queriesResolver: {
1417
+ content: `import { DefinedError } from 'ajv'
1418
+ import { ApolloError } from 'apollo-server-core'
1419
+
1420
+ import { ajv, idSchema, UserDTO } from 'schemas'
1421
+ import { getUser, getUsers } from './queries'
1422
+ import { errorHandling, GE } from '../utils'
1423
+
1424
+ const Query = {
1425
+ getUser: async (
1426
+ parent: unknown,
1427
+ { id }: { id: ${dbIsSQL ? 'number' : 'string'} },
1428
+ context: Context
1429
+ ): Promise<UserDTO> => {
1430
+ const { log } = context
1431
+ const validate = ajv.compile(idSchema)
1432
+
1433
+ try {
1434
+ const ok = validate({ id })
1435
+
1436
+ if (!ok)
1437
+ throw new ApolloError(
1438
+ \`id \${(validate.errors as DefinedError[])[0].message as string}\`,
1439
+ 'UNPROCESSABLE_ENTITY'
1440
+ )
1441
+
1442
+ return await getUser({ id }, context)
1443
+ } catch (e) {
1444
+ log.error(validate.errors)
1445
+
1446
+ return errorHandling({
1447
+ e,
1448
+ message: GE.INTERNAL_SERVER_ERROR,
1449
+ code: 'INTERNAL_SERVER_ERROR',
1450
+ log
1451
+ })
1452
+ }
1453
+ },
1454
+ getUsers
1455
+ }
1456
+
1457
+ export { Query }
1458
+ `,
1459
+ file: `${projectName}/src/graphQL/models/User/queriesResolver.ts`
1460
+ },
1461
+ typeDefs: {
1462
+ content: `import { gql } from 'apollo-server-core'
1463
+
1464
+ const User = gql\`
1465
+ type User {
1466
+ id: ${dbIsSQL ? 'Int' : 'ID'}!
1467
+ name: String!
1468
+ lastName: String!
1469
+ createdAt: String!
1470
+ updatedAt: String!
1471
+ }
1472
+
1473
+ type Query {
1474
+ getUsers: [User!]!
1475
+ getUser(id: ${dbIsSQL ? 'Int' : 'ID'}!): User!
1476
+ }
1477
+
1478
+ input StoreUserInput {
1479
+ lastName: String!
1480
+ name: String!
1481
+ }
1482
+
1483
+ input UpdateUserInput {
1484
+ id: ${dbIsSQL ? 'Int' : 'String'}!
1485
+ lastName: String!
1486
+ name: String!
1487
+ }
1488
+
1489
+ type Mutation {
1490
+ storeUser(user: StoreUserInput!): User!
1491
+ deleteAllUsers: String
1492
+ updateUser(user: UpdateUserInput!): User!
1493
+ deleteUser(id: ${dbIsSQL ? 'Int' : 'ID'}!): String
1494
+ }
1495
+ \`
1496
+
1497
+ export { User }
1498
+ `,
1499
+ file: `${projectName}/src/graphQL/models/User/typeDefs.ts`
1500
+ }
1501
+ },
1502
+ utils: {
1503
+ messages: {
1504
+ index: {
1505
+ content: `enum GenericErrors {
1506
+ INTERNAL_SERVER_ERROR = 'Something went wrong'
1507
+ }
1508
+
1509
+ export { GenericErrors as GE }
1510
+ export * from './user'
1511
+ `,
1512
+ file: `${projectName}/src/graphQL/models/utils/messages/index.ts`
1513
+ },
1514
+ user: {
1515
+ content: `enum ErrorForUser {
1516
+ NOT_FOUND = 'The requested user does not exists',
1517
+ NOTHING_TO_DELETE = 'There is no user to be deleted'
1518
+ }
1519
+
1520
+ enum MessageForUser {
1521
+ ALL_USERS_DELETED = 'All the users were deleted successfully',
1522
+ USER_DELETED = 'The requested user was successfully deleted'
1523
+ }
1524
+
1525
+ export { ErrorForUser as EFU, MessageForUser as MFU }
1526
+ `,
1527
+ file: `${projectName}/src/graphQL/models/utils/messages/user.ts`
1528
+ }
1529
+ },
1530
+ index: {
1531
+ content: `import { ApolloError } from 'apollo-server-core'
1532
+ import { HttpLogger } from 'express-pino-logger'
1533
+
1534
+ const errorHandling = ({
1535
+ e,
1536
+ message,
1537
+ code,
1538
+ log
1539
+ }: {
1540
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1541
+ e: any
1542
+ message: string
1543
+ code: string
1544
+ log: HttpLogger['logger']
1545
+ }): never => {
1546
+ log.error(e)
1547
+
1548
+ if (e instanceof ApolloError) throw e
1549
+
1550
+ throw new ApolloError(message ?? e.message, code)
1551
+ }
1552
+
1553
+ export { errorHandling }
1554
+ export * from './messages'
1555
+ `,
1556
+ file: `${projectName}/src/graphQL/models/utils/index.ts`
1557
+ }
1558
+ },
1559
+ index: {
1560
+ content: `import { mergeSchemas } from '@graphql-tools/schema'
1561
+
1562
+ import { User } from './User'
1563
+
1564
+ const mergedSchema = mergeSchemas({
1565
+ schemas: [User]
1566
+ })
1567
+
1568
+ export { mergedSchema }
1569
+ `,
1570
+ file: `${projectName}/src/graphQL/models/index.ts`
1571
+ }
1572
+ }
1573
+ }
1574
+ })
1575
+ }
1576
+ const processes = [
1577
+ writeFile(network.index.file, network.index.content),
1578
+ writeFile(network.response.file, network.response.content),
1579
+ writeFile(network.router.file, network.router.content),
1580
+ writeFile(network.server.file, network.server.content),
1581
+ writeFile(network.routes.home.file, network.routes.home.content),
1582
+ writeFile(network.routes.index.file, network.routes.index.content),
1583
+ writeFile(
1584
+ network.routes.utils.index.file,
1585
+ network.routes.utils.index.content
1586
+ )
1587
+ ]
1588
+
1589
+ if (graphQL)
1590
+ processes.concat([
1591
+ writeFile(network.graphQL?.index.file, network.graphQL?.index.content),
1592
+ writeFile(
1593
+ network.graphQL?.models.User.index.file,
1594
+ network.graphQL?.models.User.index.content
1595
+ ),
1596
+ writeFile(
1597
+ network.graphQL?.models.User.mutations.file,
1598
+ network.graphQL?.models.User.mutations.content
1599
+ ),
1600
+ writeFile(
1601
+ network.graphQL?.models.User.mutationsResolver.file,
1602
+ network.graphQL?.models.User.mutationsResolver.content
1603
+ ),
1604
+ writeFile(
1605
+ network.graphQL?.models.User.queries.file,
1606
+ network.graphQL?.models.User.queries.content
1607
+ ),
1608
+ writeFile(
1609
+ network.graphQL?.models.User.queriesResolver.file,
1610
+ network.graphQL?.models.User.queriesResolver.content
1611
+ ),
1612
+ writeFile(
1613
+ network.graphQL?.models.User.typeDefs.file,
1614
+ network.graphQL?.models.User.typeDefs.content
1615
+ ),
1616
+ writeFile(
1617
+ network.graphQL?.models.utils.messages.index.file,
1618
+ network.graphQL?.models.utils.messages.index.content
1619
+ ),
1620
+ writeFile(
1621
+ network.graphQL?.models.utils.messages.user.file,
1622
+ network.graphQL?.models.utils.messages.user.content
1623
+ ),
1624
+ writeFile(
1625
+ network.graphQL?.models.utils.index.file,
1626
+ network.graphQL?.models.utils.index.content
1627
+ ),
1628
+ writeFile(
1629
+ network.graphQL?.models.index.file,
1630
+ network.graphQL?.models.index.content
1631
+ )
1632
+ ])
1633
+ else
1634
+ processes.push(
1635
+ writeFile(network.routes.user.file, network.routes.user.content)
1636
+ )
1637
+
1638
+ await Promise.all(processes)
1639
+ }
1640
+
1641
+ /**
1642
+ * @param {Object} args
1643
+ * @param {String} args.projectName
1644
+ * @param {Boolean} args.dbIsSQL
1645
+ * @param {Boolean} args.graphQL
1646
+ */
1647
+ const schemas = async ({ projectName, dbIsSQL, graphQL }) => {
1648
+ const createFoldersCommand = `mkdir ${projectName}/src/schemas`
1649
+
1650
+ if (platform() === 'win32')
1651
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
1652
+ else await exec(createFoldersCommand)
1653
+
1654
+ const schemas = {
1655
+ index: {
1656
+ content: graphQL
1657
+ ? `import { Static, Type } from '@sinclair/typebox'
1658
+ import Ajv from 'ajv/dist/2019.js'
1659
+ import addFormats from 'ajv-formats'
1660
+
1661
+ const id = ${
1662
+ dbIsSQL
1663
+ ? `Type.Number()`
1664
+ : `Type.String({ pattern: '^[a-zA-Z0-9]{24,}$' })`
1665
+ }
1666
+ type ID = Static<typeof id>
1667
+
1668
+ const idSchema = Type.Object({ id })
1669
+
1670
+ type IDSchema = Static<typeof idSchema>
1671
+
1672
+ const ajv = addFormats(new Ajv(), ['email'])
1673
+ .addKeyword('kind')
1674
+ .addKeyword('modifier')
1675
+
1676
+ export { id, ID, idSchema, IDSchema, ajv }
1677
+ export * from './user'
1678
+ `
1679
+ : `import { Static, Type } from '@sinclair/typebox'
1680
+
1681
+ const id = ${
1682
+ dbIsSQL
1683
+ ? `Type.Number()`
1684
+ : `Type.String({ pattern: '^[a-zA-Z0-9]{24,}$' })`
1685
+ }
1686
+
1687
+ type Id = Static<typeof id>
1688
+
1689
+ const idSchema = Type.Object({ id })
1690
+
1691
+ type IdSchema = Static<typeof idSchema>
1692
+
1693
+ export { id, Id, idSchema, IdSchema }
1694
+ export * from './user'
1695
+ `,
1696
+ file: `${projectName}/src/schemas/index.ts`
1697
+ },
1698
+ user: {
1699
+ content: `import { Static, Type } from '@sinclair/typebox'
1700
+
1701
+ import { id } from '.'
1702
+
1703
+ const user = Type.Object({
1704
+ lastName: Type.String(),
1705
+ name: Type.String()
1706
+ })
1707
+
1708
+ type User = Static<typeof user>
1709
+
1710
+ const userWithId = Type.Intersect([user, Type.Object({ id })])
1711
+
1712
+ type UserWithId = Static<typeof userWithId>
1713
+
1714
+ const userDto = Type.Object({
1715
+ id: Type.Optional(id),
1716
+ lastName: Type.String(),
1717
+ name: Type.String(),
1718
+ createdAt: Type.Optional(Type.String()),
1719
+ updatedAt: Type.Optional(Type.String())
1720
+ })
1721
+
1722
+ type UserDTO = Static<typeof userDto>
1723
+
1724
+ const storeUserDto = Type.Object({
1725
+ args: user
1726
+ })
1727
+
1728
+ type StoreUserDTO = Static<typeof storeUserDto>
1729
+
1730
+ export {
1731
+ userDto,
1732
+ UserDTO,
1733
+ userWithId,
1734
+ UserWithId,
1735
+ user,
1736
+ User,
1737
+ storeUserDto,
1738
+ StoreUserDTO
1739
+ }
1740
+ `,
1741
+ file: `${projectName}/src/schemas/user.ts`
1742
+ }
1743
+ }
1744
+
1745
+ await Promise.all([
1746
+ writeFile(schemas.index.file, schemas.index.content),
1747
+ writeFile(schemas.user.file, schemas.user.content)
1748
+ ])
1749
+ }
1750
+
1751
+ /**
1752
+ * @param {Object} args
1753
+ * @param {String} args.projectName
1754
+ * @param {Boolean} args.dbIsSQL
1755
+ */
1756
+ const services = async ({ projectName, dbIsSQL }) => {
1757
+ const createFoldersCommand = `mkdir ${projectName}/src/services \
1758
+ ${projectName}/src/services/utils \
1759
+ ${projectName}/src/services/utils/messages`
1760
+
1761
+ if (platform() === 'win32')
1762
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
1763
+ else await exec(createFoldersCommand)
1764
+
1765
+ const services = {
1766
+ index: {
1767
+ content: "export * from './user'\n",
1768
+ file: `${projectName}/src/services/index.ts`
1769
+ },
1770
+ user: {
1771
+ content: `import httpErrors from 'http-errors'
1772
+
1773
+ import { store, remove, get, update } from 'database'
1774
+ import { User, UserDTO, UserWithId } from 'schemas'
1775
+ import { EFU, MFU, GE, errorHandling } from './utils'
1776
+
1777
+ type Process = {
1778
+ type: 'store' | 'getAll' | 'deleteAll' | 'getOne' | 'update' | 'delete'
1779
+ }
1780
+
1781
+ type Arguments = {
1782
+ id?: ${dbIsSQL ? 'number' : 'string'}
1783
+ user?: User
1784
+ userWithId?: UserWithId
1785
+ }
1786
+
1787
+ class UserService {
1788
+ #args: Arguments
1789
+
1790
+ constructor(args: Arguments = {}) {
1791
+ this.#args = args
1792
+ }
1793
+
1794
+ public process({ type }: Process): Promise<string | UserDTO | UserDTO[]> {
1795
+ switch (type) {
1796
+ case 'store':
1797
+ return this.#store()
1798
+ case 'getAll':
1799
+ return this.#getAll()
1800
+ case 'deleteAll':
1801
+ return this.#deleteAll()
1802
+ case 'getOne':
1803
+ return this.#getOne()
1804
+ case 'update':
1805
+ return this.#update()
1806
+ case 'delete':
1807
+ return this.#delete()
1808
+ default:
1809
+ throw new httpErrors.InternalServerError(GE.INTERNAL_SERVER_ERROR)
1810
+ }
1811
+ }
1812
+
1813
+ async #store(): Promise<UserDTO> {
1814
+ try {
1815
+ if (!this.#args.user)
1816
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
1817
+
1818
+ const result = await store(this.#args.user)
1819
+
1820
+ return result
1821
+ } catch (e) {
1822
+ return errorHandling(e, GE.INTERNAL_SERVER_ERROR)
1823
+ }
1824
+ }
1825
+
1826
+ async #getAll(): Promise<UserDTO[]> {
1827
+ try {
1828
+ const users = (await get()) as UserDTO[]
1829
+
1830
+ return users
1831
+ } catch (e) {
1832
+ return errorHandling(e, GE.INTERNAL_SERVER_ERROR)
1833
+ }
1834
+ }
1835
+
1836
+ async #deleteAll(): Promise<string> {
1837
+ try {
1838
+ const usersDeleted = (await remove()) as number
1839
+
1840
+ ${
1841
+ dbIsSQL
1842
+ ? 'if (usersDeleted !== 0) return MFU.ALL_USERS_DELETED'
1843
+ : `if (usersDeleted >= 1) return MFU.ALL_USERS_DELETED
1844
+
1845
+ if (usersDeleted === 0)
1846
+ throw new httpErrors.Conflict(EFU.NOTHING_TO_DELETE)`
1847
+ }
1848
+
1849
+ throw new httpErrors.InternalServerError(GE.INTERNAL_SERVER_ERROR)
1850
+ } catch (e) {
1851
+ return errorHandling(e, GE.INTERNAL_SERVER_ERROR)
1852
+ }
1853
+ }
1854
+
1855
+ async #getOne(): Promise<UserDTO> {
1856
+ try {
1857
+ if (!this.#args.id)
1858
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
1859
+
1860
+ const { id } = this.#args
1861
+ const user = (await get(id)) as UserDTO | null
1862
+
1863
+ if (!user) throw new httpErrors.NotFound(EFU.NOT_FOUND)
1864
+
1865
+ return user
1866
+ } catch (e) {
1867
+ return errorHandling(e, GE.INTERNAL_SERVER_ERROR)
1868
+ }
1869
+ }
1870
+
1871
+ async #update(): Promise<UserDTO> {
1872
+ try {
1873
+ if (!this.#args.userWithId || !this.#args.userWithId.id)
1874
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
1875
+
1876
+ const updatedUser = await update(this.#args.userWithId)
1877
+
1878
+ if (!updatedUser) throw new httpErrors.NotFound(EFU.NOT_FOUND)
1879
+
1880
+ return updatedUser
1881
+ } catch (e) {
1882
+ return errorHandling(e, GE.INTERNAL_SERVER_ERROR)
1883
+ }
1884
+ }
1885
+
1886
+ async #delete(): Promise<string> {
1887
+ try {
1888
+ if (!this.#args.id)
1889
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
1890
+
1891
+ const { id } = this.#args
1892
+ const deletedUser = await remove(id)
1893
+
1894
+ if (!deletedUser) throw new httpErrors.NotFound(EFU.NOT_FOUND)
1895
+
1896
+ return MFU.USER_DELETED
1897
+ } catch (e) {
1898
+ return errorHandling(e, GE.INTERNAL_SERVER_ERROR)
1899
+ }
1900
+ }
1901
+ }
1902
+
1903
+ export { UserService }
1904
+ `,
1905
+ file: `${projectName}/src/services/user.ts`
1906
+ },
1907
+ utils: {
1908
+ index: {
1909
+ content: `import httpErrors from 'http-errors'
1910
+
1911
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1912
+ const errorHandling = (e: any, message?: string): never => {
1913
+ console.error(e)
1914
+
1915
+ if (e instanceof httpErrors.HttpError) throw e
1916
+
1917
+ throw new httpErrors.InternalServerError(message ?? e.message)
1918
+ }
1919
+
1920
+ export { errorHandling }
1921
+ export * from './messages'
1922
+ `,
1923
+ file: `${projectName}/src/services/utils/index.ts`
1924
+ }
1925
+ },
1926
+ 'utils/messages': {
1927
+ index: {
1928
+ content: `enum GenericErrors {
1929
+ INTERNAL_SERVER_ERROR = 'Something went wrong'
1930
+ }
1931
+
1932
+ export { GenericErrors as GE }
1933
+ export * from './user'
1934
+ `,
1935
+ file: `${projectName}/src/services/utils/messages/index.ts`
1936
+ },
1937
+ user: {
1938
+ content: `enum ErrorForUser {
1939
+ NOT_FOUND = 'The requested user does not exists',
1940
+ NOTHING_TO_DELETE = 'There is no user to be deleted'
1941
+ }
1942
+
1943
+ enum MessageForUser {
1944
+ ALL_USERS_DELETED = 'All the users were deleted successfully',
1945
+ USER_DELETED = 'The requested user was successfully deleted'
1946
+ }
1947
+
1948
+ export { ErrorForUser as EFU, MessageForUser as MFU }
1949
+ `,
1950
+ file: `${projectName}/src/services/utils/messages/user.ts`
1951
+ }
1952
+ }
1953
+ }
1954
+
1955
+ await Promise.all([
1956
+ writeFile(services.index.file, services.index.content),
1957
+ writeFile(services.user.file, services.user.content),
1958
+ writeFile(services.utils.index.file, services.utils.index.content),
1959
+ writeFile(
1960
+ services['utils/messages'].index.file,
1961
+ services['utils/messages'].index.content
1962
+ ),
1963
+ writeFile(
1964
+ services['utils/messages'].user.file,
1965
+ services['utils/messages'].user.content
1966
+ )
1967
+ ])
1968
+ }
1969
+
1970
+ /**
1971
+ * @param {Object} args
1972
+ * @param {String} args.projectName
1973
+ * @param {Boolean} args.graphQL
1974
+ * @param {import('../../../../').Config['database']} args.database
1975
+ */
1976
+ const main = async ({ projectName, graphQL, database }) => {
1977
+ const dbIsSQL = database !== 'mongo'
1978
+
1979
+ await types({ projectName, graphQL, dbIsSQL })
1980
+ await network({ projectName, graphQL, dbIsSQL })
1981
+ await schemas({ projectName, dbIsSQL, graphQL })
1982
+
1983
+ if (!graphQL) await services({ projectName, dbIsSQL })
1984
+
1985
+ if (dbIsSQL) await sql({ projectName, db: database })
1986
+ else await mongo({ projectName })
1987
+ }
1988
+
1989
+ module.exports = main