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