@anthonylzq/simba.js 4.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,25 +75,33 @@ export { applyRoutes }
75
75
  server: {
76
76
  content: `import express from 'express'
77
77
  import mongoose from 'mongoose'
78
- import morgan from 'morgan'
79
78
  import cors from 'cors'
79
+ import pino, { HttpLogger } from 'express-pino-logger'
80
80
 
81
81
  import { applyRoutes } from './router'
82
82
 
83
- const PORT = (process.env.PORT as string) || '1996'
83
+ const PORT = (process.env.PORT as string) || 1996
84
84
 
85
85
  class Server {
86
86
  #app: express.Application
87
87
  #connection: mongoose.Connection | undefined
88
+ #log: HttpLogger
88
89
 
89
90
  constructor() {
90
91
  this.#app = express()
92
+ this.#log = pino({
93
+ transport: {
94
+ target: 'pino-pretty',
95
+ options: {
96
+ colorize: true
97
+ }
98
+ }
99
+ })
91
100
  this.#config()
92
101
  }
93
102
 
94
103
  #config() {
95
104
  this.#app.use(cors())
96
- this.#app.use(morgan('dev'))
97
105
  this.#app.use(express.json())
98
106
  this.#app.use(express.urlencoded({ extended: false }))
99
107
  this.#app.use(
@@ -108,10 +116,10 @@ class Server {
108
116
  'Access-Control-Allow-Headers',
109
117
  'Authorization, Content-Type'
110
118
  )
119
+ res.header('x-powered-by', 'Simba.js')
111
120
  next()
112
121
  }
113
122
  )
114
-
115
123
  applyRoutes(this.#app)
116
124
  }
117
125
 
@@ -123,14 +131,14 @@ class Server {
123
131
  useUnifiedTopology: true
124
132
  }
125
133
  this.#connection.on('connected', () => {
126
- console.log('Mongo connection established.')
134
+ this.#log.logger.info('Mongo connection established.')
127
135
  })
128
136
  this.#connection.on('reconnected', () => {
129
- console.log('Mongo connection reestablished')
137
+ this.#log.logger.info('Mongo connection reestablished')
130
138
  })
131
139
  this.#connection.on('disconnected', () => {
132
- console.log('Mongo connection disconnected')
133
- console.log('Trying to reconnected to Mongo...')
140
+ this.#log.logger.info('Mongo connection disconnected')
141
+ this.#log.logger.info('Trying to reconnected to Mongo...')
134
142
  setTimeout(() => {
135
143
  mongoose.connect(process.env.MONGO_URI as string, {
136
144
  ...connection,
@@ -140,24 +148,24 @@ class Server {
140
148
  }, 3000)
141
149
  })
142
150
  this.#connection.on('close', () => {
143
- console.log('Mongo connection closed')
151
+ this.#log.logger.info('Mongo connection closed')
144
152
  })
145
153
  this.#connection.on('error', (e: Error) => {
146
- console.log('Mongo connection error:')
147
- console.error(e)
154
+ this.#log.logger.info('Mongo connection error:')
155
+ this.#log.logger.error(e)
148
156
  })
149
157
  await mongoose.connect(process.env.MONGO_URI as string, connection)
150
158
  }
151
159
 
152
160
  public start(): void {
153
161
  this.#app.listen(PORT, () => {
154
- console.log(\`Server running at port \${PORT}\`)
162
+ this.#log.logger.info(\`Server running at port \${PORT}\`)
155
163
  })
156
164
 
157
165
  try {
158
166
  this.#mongo()
159
167
  } catch (e) {
160
- console.error(e)
168
+ this.#log.logger.error(e)
161
169
  }
162
170
  }
163
171
  }
@@ -378,45 +386,524 @@ export { validatorCompiler }
378
386
  }
379
387
  }
380
388
 
381
- await writeFile(network.response.file, network.response.content)
382
- await writeFile(network.router.file, network.router.content)
383
- await writeFile(network.server.file, network.server.content)
384
- await writeFile(network.routes.home.file, network.routes.home.content)
385
- await writeFile(network.routes.index.file, network.routes.index.content)
386
- await writeFile(network.routes.user.file, network.routes.user.content)
387
- await writeFile(
388
- network.routes.utils.index.file,
389
- network.routes.utils.index.content
390
- )
389
+ await Promise.all([
390
+ writeFile(network.response.file, network.response.content),
391
+ writeFile(network.router.file, network.router.content),
392
+ writeFile(network.server.file, network.server.content),
393
+ writeFile(network.routes.home.file, network.routes.home.content),
394
+ writeFile(network.routes.index.file, network.routes.index.content),
395
+ writeFile(network.routes.user.file, network.routes.user.content),
396
+ writeFile(
397
+ network.routes.utils.index.file,
398
+ network.routes.utils.index.content
399
+ )
400
+ ])
391
401
  }
392
402
 
393
403
  /**
394
404
  * @param {Object} args
395
405
  * @param {String} args.projectName
396
406
  */
397
- const fastifyRestNetwork = async ({ projectName }) => {
398
- const createFoldersCommand = `mkdir ${projectName}/src/network/utils`
407
+ const expressGraphQLNetwork = async ({ projectName }) => {
408
+ const createFoldersCommand = `mkdir ${projectName}/src/network/routes/utils \
409
+ ${projectName}/src/graphQL/models \
410
+ ${projectName}/src/graphQL/models/User \
411
+ ${projectName}/src/graphQL/models/utils \
412
+ ${projectName}/src/graphQL/models/utils/messages`
399
413
 
400
414
  if (platform() === 'win32')
401
415
  await exec(createFoldersCommand.replaceAll('/', '\\'))
402
416
  else await exec(createFoldersCommand)
403
417
 
404
- const network = {
405
- response: {
406
- content: `import { FastifyReply } from 'fastify'
418
+ const graphQL = {
419
+ index: {
420
+ content: "export * from './models'\n",
421
+ file: `${projectName}/src/graphQL/index.ts`
422
+ },
423
+ models: {
424
+ User: {
425
+ index: {
426
+ content: `import { makeExecutableSchema } from '@graphql-tools/schema'
407
427
 
408
- const response = ({
409
- error,
428
+ import { User as UserTD } from './typeDefs'
429
+ import { Query } from './queriesResolver'
430
+ import { Mutation } from './mutationsResolver'
431
+
432
+ const resolvers = {
433
+ Query,
434
+ Mutation
435
+ }
436
+
437
+ const User = makeExecutableSchema({
438
+ typeDefs: UserTD,
439
+ resolvers
440
+ })
441
+
442
+ export { User }
443
+ `,
444
+ file: `${projectName}/src/graphQL/models/User/index.ts`
445
+ },
446
+ mutations: {
447
+ content: `import { ApolloError } from 'apollo-server-core'
448
+
449
+ import { store, remove, update } from 'database'
450
+ import { UserDTO } from 'schemas'
451
+ import { EFU, MFU, GE, errorHandling } from '../utils'
452
+
453
+ const storeUser = async (
454
+ { user }: { user: UserDTO },
455
+ { log }: Context
456
+ ): Promise<UserDTO> => {
457
+ try {
458
+ const result = await store(user)
459
+
460
+ return result
461
+ } catch (e) {
462
+ return errorHandling({
463
+ e,
464
+ message: GE.INTERNAL_SERVER_ERROR,
465
+ code: 'INTERNAL_SERVER_ERROR',
466
+ log
467
+ })
468
+ }
469
+ }
470
+
471
+ const deleteAllUsers = async ({ log }: Context): Promise<string> => {
472
+ try {
473
+ const usersDeleted = (await remove()) as number
474
+
475
+ if (usersDeleted >= 1) return MFU.ALL_USERS_DELETED
476
+
477
+ if (usersDeleted === 0)
478
+ throw new ApolloError(EFU.NOTHING_TO_DELETE, 'NOTHING_TO_DELETE')
479
+
480
+ throw new ApolloError(GE.INTERNAL_SERVER_ERROR, 'INTERNAL_SERVER_ERROR')
481
+ } catch (e) {
482
+ return errorHandling({
483
+ e,
484
+ message: GE.INTERNAL_SERVER_ERROR,
485
+ code: 'INTERNAL_SERVER_ERROR',
486
+ log
487
+ })
488
+ }
489
+ }
490
+
491
+ const updateUser = async (
492
+ { user }: { user: UserDTO },
493
+ { log }: Context
494
+ ): Promise<UserDTO> => {
495
+ try {
496
+ const updatedUser = await update(user)
497
+
498
+ if (!updatedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
499
+
500
+ return updatedUser
501
+ } catch (e) {
502
+ return errorHandling({
503
+ e,
504
+ message: GE.INTERNAL_SERVER_ERROR,
505
+ code: 'INTERNAL_SERVER_ERROR',
506
+ log
507
+ })
508
+ }
509
+ }
510
+
511
+ const deleteUser = async (
512
+ { id }: { id: string },
513
+ { log }: Context
514
+ ): Promise<string> => {
515
+ try {
516
+ const deletedUser = await remove(id)
517
+
518
+ if (!deletedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
519
+
520
+ return MFU.USER_DELETED
521
+ } catch (e) {
522
+ return errorHandling({
523
+ e,
524
+ message: GE.INTERNAL_SERVER_ERROR,
525
+ code: 'INTERNAL_SERVER_ERROR',
526
+ log
527
+ })
528
+ }
529
+ }
530
+
531
+ export { storeUser, deleteAllUsers, updateUser, deleteUser }
532
+ `,
533
+ file: `${projectName}/src/graphQL/models/User/mutations.ts`
534
+ },
535
+ mutationsResolver: {
536
+ content: `import { DefinedError } from 'ajv'
537
+ import { ApolloError } from 'apollo-server-core'
538
+
539
+ import { ajv, idSchema, UserDTO } from 'schemas'
540
+ import { storeUserSchema, updateUserSchema } from './schemas'
541
+ import { storeUser, updateUser, deleteUser, deleteAllUsers } from './mutations'
542
+ import { errorHandling, GE } from '../utils'
543
+
544
+ const Mutation = {
545
+ storeUser: async (
546
+ parent: unknown,
547
+ { user }: { user: UserDTO },
548
+ context: Context
549
+ ): Promise<UserDTO> => {
550
+ const { log } = context
551
+ const validate = ajv.compile(storeUserSchema)
552
+
553
+ try {
554
+ const ok = validate(user)
555
+
556
+ if (!ok)
557
+ throw new ApolloError(
558
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
559
+ '/',
560
+ ''
561
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
562
+ 'UNPROCESSABLE_ENTITY'
563
+ )
564
+
565
+ return await storeUser({ user }, context)
566
+ } catch (e) {
567
+ log.error(validate.errors)
568
+
569
+ return errorHandling({
570
+ e,
571
+ message: GE.INTERNAL_SERVER_ERROR,
572
+ code: 'INTERNAL_SERVER_ERROR',
573
+ log
574
+ })
575
+ }
576
+ },
577
+ updateUser: async (
578
+ parent: unknown,
579
+ { user }: { user: UserDTO },
580
+ context: Context
581
+ ): Promise<UserDTO> => {
582
+ const validate = ajv.compile(updateUserSchema)
583
+ const { log } = context
584
+
585
+ try {
586
+ const ok = validate(user)
587
+
588
+ if (!ok)
589
+ throw new ApolloError(
590
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
591
+ '/',
592
+ ''
593
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
594
+ 'UNPROCESSABLE_ENTITY'
595
+ )
596
+
597
+ return await updateUser({ user }, context)
598
+ } catch (e) {
599
+ log.error(validate.errors)
600
+
601
+ return errorHandling({
602
+ e,
603
+ message: GE.INTERNAL_SERVER_ERROR,
604
+ code: 'INTERNAL_SERVER_ERROR',
605
+ log
606
+ })
607
+ }
608
+ },
609
+ deleteUser: async (
610
+ parent: unknown,
611
+ { id }: { id: string },
612
+ context: Context
613
+ ): Promise<string> => {
614
+ const validate = ajv.compile(idSchema)
615
+ const { log } = context
616
+
617
+ try {
618
+ const ok = validate({ id })
619
+
620
+ if (!ok)
621
+ throw new ApolloError(
622
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
623
+ '/',
624
+ ''
625
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
626
+ 'UNPROCESSABLE_ENTITY'
627
+ )
628
+
629
+ return await deleteUser({ id }, context)
630
+ } catch (e) {
631
+ log.error(validate.errors)
632
+
633
+ return errorHandling({
634
+ e,
635
+ message: GE.INTERNAL_SERVER_ERROR,
636
+ code: 'INTERNAL_SERVER_ERROR',
637
+ log
638
+ })
639
+ }
640
+ },
641
+ deleteAllUsers: async (
642
+ parent: unknown,
643
+ args: unknown,
644
+ context: Context
645
+ ): Promise<string> => {
646
+ const { log } = context
647
+ try {
648
+ return await deleteAllUsers(context)
649
+ } catch (e) {
650
+ return errorHandling({
651
+ e,
652
+ message: GE.INTERNAL_SERVER_ERROR,
653
+ code: 'INTERNAL_SERVER_ERROR',
654
+ log
655
+ })
656
+ }
657
+ }
658
+ }
659
+
660
+ export { Mutation }
661
+ `,
662
+ file: `${projectName}/src/graphQL/models/User/mutationsResolver.ts`
663
+ },
664
+ queries: {
665
+ content: `import { ApolloError } from 'apollo-server-core'
666
+
667
+ import { get } from 'database'
668
+ import { UserDTO } from 'schemas'
669
+ import { EFU, GE, errorHandling } from '../utils'
670
+
671
+ const getUsers = async (
672
+ parent: unknown,
673
+ args: unknown,
674
+ { log }: Context
675
+ ): Promise<UserDTO[]> => {
676
+ try {
677
+ const users = (await get()) as UserDTO[]
678
+
679
+ return users
680
+ } catch (e) {
681
+ return errorHandling({
682
+ e,
683
+ message: GE.INTERNAL_SERVER_ERROR,
684
+ code: 'INTERNAL_SERVER_ERROR',
685
+ log
686
+ })
687
+ }
688
+ }
689
+
690
+ const getUser = async (
691
+ { id }: { id: string },
692
+ { log }: Context
693
+ ): Promise<UserDTO> => {
694
+ try {
695
+ const user = (await get(id as string)) as UserDTO | null
696
+
697
+ if (!user) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
698
+
699
+ return user
700
+ } catch (e) {
701
+ return errorHandling({
702
+ e,
703
+ message: GE.INTERNAL_SERVER_ERROR,
704
+ code: 'INTERNAL_SERVER_ERROR',
705
+ log
706
+ })
707
+ }
708
+ }
709
+
710
+ export { getUsers, getUser }
711
+ `,
712
+ file: `${projectName}/src/graphQL/models/User/queries.ts`
713
+ },
714
+ queriesResolver: {
715
+ content: `import { DefinedError } from 'ajv'
716
+ import { ApolloError } from 'apollo-server-core'
717
+
718
+ import { ajv, idSchema, UserDTO } from 'schemas'
719
+ import { getUser, getUsers } from './queries'
720
+ import { errorHandling, GE } from '../utils'
721
+
722
+ const Query = {
723
+ getUser: async (
724
+ parent: unknown,
725
+ { id }: { id: string },
726
+ context: Context
727
+ ): Promise<UserDTO> => {
728
+ const { log } = context
729
+ const validate = ajv.compile(idSchema)
730
+
731
+ try {
732
+ const ok = validate({ id })
733
+
734
+ if (!ok)
735
+ throw new ApolloError(
736
+ \`id \${(validate.errors as DefinedError[])[0].message as string}\`,
737
+ 'UNPROCESSABLE_ENTITY'
738
+ )
739
+
740
+ return await getUser({ id }, context)
741
+ } catch (e) {
742
+ log.error(validate.errors)
743
+
744
+ return errorHandling({
745
+ e,
746
+ message: GE.INTERNAL_SERVER_ERROR,
747
+ code: 'INTERNAL_SERVER_ERROR',
748
+ log
749
+ })
750
+ }
751
+ },
752
+ getUsers
753
+ }
754
+
755
+ export { Query }
756
+ `,
757
+ file: `${projectName}/src/graphQL/models/User/queriesResolver.ts`
758
+ },
759
+ schemas: {
760
+ content: `import { Type } from '@sinclair/typebox'
761
+
762
+ const updateUserSchema = Type.Object(
763
+ {
764
+ id: Type.String({ minLength: 24, maxLength: 24 }),
765
+ lastName: Type.String(),
766
+ name: Type.String()
767
+ },
768
+ { additionalProperties: false }
769
+ )
770
+
771
+ const storeUserSchema = Type.Object(
772
+ {
773
+ lastName: Type.String({ minLength: 1 }),
774
+ name: Type.String({ minLength: 1 })
775
+ },
776
+ { additionalProperties: false }
777
+ )
778
+
779
+ export { updateUserSchema, storeUserSchema }
780
+ `,
781
+ file: `${projectName}/src/graphQL/models/User/schemas.ts`
782
+ },
783
+ typeDefs: {
784
+ content: `import { gql } from 'apollo-server-core'
785
+
786
+ const User = gql\`
787
+ type User {
788
+ id: ID!
789
+ name: String!
790
+ lastName: String!
791
+ updatedAt: String!
792
+ }
793
+
794
+ type Query {
795
+ getUsers: [User!]!
796
+ getUser(id: ID!): User!
797
+ }
798
+
799
+ input StoreUserInput {
800
+ lastName: String!
801
+ name: String!
802
+ }
803
+
804
+ input UpdateUserInput {
805
+ id: String!
806
+ lastName: String!
807
+ name: String!
808
+ }
809
+
810
+ type Mutation {
811
+ storeUser(user: StoreUserInput!): User!
812
+ deleteAllUsers: String
813
+ updateUser(user: UpdateUserInput!): User!
814
+ deleteUser(id: ID!): String
815
+ }
816
+ \`
817
+
818
+ export { User }
819
+ `,
820
+ file: `${projectName}/src/graphQL/models/User/typeDefs.ts`
821
+ }
822
+ },
823
+ utils: {
824
+ messages: {
825
+ index: {
826
+ content: `enum GenericErrors {
827
+ INTERNAL_SERVER_ERROR = 'Something went wrong'
828
+ }
829
+
830
+ export { GenericErrors as GE }
831
+ export * from './user'
832
+ `,
833
+ file: `${projectName}/src/graphQL/models/utils/messages/index.ts`
834
+ },
835
+ user: {
836
+ content: `enum ErrorForUser {
837
+ NOT_FOUND = 'The requested user does not exists',
838
+ NOTHING_TO_DELETE = 'There is no user to be deleted'
839
+ }
840
+
841
+ enum MessageForUser {
842
+ ALL_USERS_DELETED = 'All the users were deleted successfully',
843
+ USER_DELETED = 'The requested user was successfully deleted'
844
+ }
845
+
846
+ export { ErrorForUser as EFU, MessageForUser as MFU }
847
+ `,
848
+ file: `${projectName}/src/graphQL/models/utils/messages/user.ts`
849
+ }
850
+ },
851
+ index: {
852
+ content: `import { ApolloError } from 'apollo-server-core'
853
+ import { FastifyLoggerInstance } from 'fastify'
854
+
855
+ const errorHandling = ({
856
+ e,
410
857
  message,
411
- reply,
412
- status
858
+ code,
859
+ log
413
860
  }: {
861
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
862
+ e: any
863
+ message: string
864
+ code: string
865
+ log: FastifyLoggerInstance
866
+ }): never => {
867
+ log.error(e)
868
+
869
+ if (e instanceof ApolloError) throw e
870
+
871
+ throw new ApolloError(message ?? e.message, code)
872
+ }
873
+
874
+ export { errorHandling }
875
+ export * from './messages'
876
+ `,
877
+ file: `${projectName}/src/graphQL/models/utils/index.ts`
878
+ }
879
+ },
880
+ index: {
881
+ content: `import { mergeSchemas } from '@graphql-tools/schema'
882
+
883
+ import { User } from './User'
884
+
885
+ const mergedSchema = mergeSchemas({
886
+ schemas: [User]
887
+ })
888
+
889
+ export { mergedSchema }
890
+ `,
891
+ file: `${projectName}/src/graphQL/models/index.ts`
892
+ }
893
+ }
894
+ }
895
+
896
+ const network = {
897
+ response: {
898
+ content: `interface ResponseProps {
414
899
  error: boolean
415
900
  message: unknown
416
- reply: FastifyReply
901
+ res: CustomResponse
417
902
  status: number
418
- }): void => {
419
- reply.code(status).send({ error, message })
903
+ }
904
+
905
+ const response = ({ error, message, res, status }: ResponseProps): void => {
906
+ res.status(status).send({ error, message })
420
907
  }
421
908
 
422
909
  export { response }
@@ -424,35 +911,40 @@ export { response }
424
911
  file: `${projectName}/src/network/response.ts`
425
912
  },
426
913
  router: {
427
- content: `import { FastifyInstance } from 'fastify'
428
- import { HttpError } from 'http-errors'
914
+ content: `import { Application, Response, Request, Router, NextFunction } from 'express'
915
+ import swaggerUi from 'swagger-ui-express'
916
+ import httpErrors from 'http-errors'
429
917
 
430
918
  import { response } from './response'
431
- import { Home, User, Docs } from './routes'
432
-
433
- const routers = [Docs, User]
919
+ import { Home } from './routes'
920
+ import { docs } from 'utils'
434
921
 
435
- const applyRoutes = (app: FastifyInstance): void => {
436
- Home(app)
437
- routers.forEach(router => router(app))
922
+ const routers: Router[] = []
923
+ const applyRoutes = (app: Application): void => {
924
+ app.use('/', Home)
925
+ app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(docs))
926
+ routers.forEach((router: Router): Application => app.use('/api', router))
438
927
 
439
928
  // Handling 404 error
440
- app.setNotFoundHandler((request, reply) => {
441
- response({
442
- error: true,
443
- message: 'This route does not exists',
444
- reply,
445
- status: 404
446
- })
447
- })
448
- app.setErrorHandler<HttpError>((error, request, reply) => {
449
- response({
450
- error: true,
451
- message: error.message,
452
- reply,
453
- status: error.status ?? 500
454
- })
929
+ app.use((req, res, next) => {
930
+ next(new httpErrors.NotFound('This route does not exists'))
455
931
  })
932
+ app.use(
933
+ (
934
+ error: httpErrors.HttpError,
935
+ req: Request,
936
+ res: Response,
937
+ next: NextFunction
938
+ ) => {
939
+ response({
940
+ error: true,
941
+ message: error.message,
942
+ res,
943
+ status: error.status
944
+ })
945
+ next()
946
+ }
947
+ )
456
948
  }
457
949
 
458
950
  export { applyRoutes }
@@ -460,36 +952,64 @@ export { applyRoutes }
460
952
  file: `${projectName}/src/network/router.ts`
461
953
  },
462
954
  server: {
463
- content: `import fastify, { FastifyInstance } from 'fastify'
955
+ content: `import http from 'http'
956
+ import express from 'express'
464
957
  import mongoose from 'mongoose'
958
+ import cors from 'cors'
959
+ import pino, { HttpLogger } from 'express-pino-logger'
960
+ import { ApolloServer } from 'apollo-server-express'
961
+ import {
962
+ ApolloServerPluginDrainHttpServer,
963
+ ApolloServerPluginLandingPageDisabled,
964
+ ApolloServerPluginLandingPageGraphQLPlayground
965
+ } from 'apollo-server-core'
465
966
 
967
+ import { mergedSchema as schema } from 'graphQL'
466
968
  import { applyRoutes } from './router'
467
- import { validatorCompiler } from './utils'
468
969
 
469
- const PORT = process.env.PORT ?? '1996'
970
+ const PORT = (process.env.PORT as string) || 1996
470
971
 
471
972
  class Server {
472
- #app: FastifyInstance
973
+ #app: express.Application
473
974
  #connection: mongoose.Connection | undefined
975
+ #httpServer: http.Server
976
+ #log: HttpLogger
474
977
 
475
978
  constructor() {
476
- this.#app = fastify({ logger: { prettyPrint: true } })
979
+ this.#app = express()
980
+ this.#httpServer = http.createServer(this.#app)
981
+ this.#log = pino({
982
+ transport: {
983
+ target: 'pino-pretty',
984
+ options: {
985
+ colorize: true
986
+ }
987
+ }
988
+ })
477
989
  this.#config()
478
990
  }
479
991
 
480
992
  #config() {
481
- this.#app.register(require('fastify-cors'), {})
482
- this.#app.addHook('preHandler', (req, reply, done) => {
483
- reply.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
484
- reply.header('Access-Control-Allow-Origin', '*')
485
- reply.header(
486
- 'Access-Control-Allow-Headers',
487
- 'Authorization, Content-Type'
488
- )
489
- done()
490
- })
491
- this.#app.setValidatorCompiler(validatorCompiler)
492
- applyRoutes(this.#app)
993
+ this.#app.use(cors())
994
+ this.#app.use(this.#log)
995
+ this.#app.use(express.json())
996
+ this.#app.use(express.urlencoded({ extended: false }))
997
+ this.#app.use(
998
+ (
999
+ req: express.Request,
1000
+ res: express.Response,
1001
+ next: express.NextFunction
1002
+ ) => {
1003
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
1004
+ res.header('Access-Control-Allow-Origin', '*')
1005
+ res.header(
1006
+ 'Access-Control-Allow-Headers',
1007
+ 'Authorization, Content-Type'
1008
+ )
1009
+ res.header('x-powered-by', 'Simba.js')
1010
+ next()
1011
+ }
1012
+ )
493
1013
  }
494
1014
 
495
1015
  async #mongo(): Promise<void> {
@@ -500,10 +1020,313 @@ class Server {
500
1020
  useUnifiedTopology: true
501
1021
  }
502
1022
  this.#connection.on('connected', () => {
503
- this.#app.log.info('Mongo connection established.')
1023
+ this.#log.logger.info('Mongo connection established.')
504
1024
  })
505
1025
  this.#connection.on('reconnected', () => {
506
- this.#app.log.info('Mongo connection reestablished')
1026
+ this.#log.logger.info('Mongo connection reestablished')
1027
+ })
1028
+ this.#connection.on('disconnected', () => {
1029
+ this.#log.logger.info('Mongo connection disconnected')
1030
+ this.#log.logger.info('Trying to reconnected to Mongo...')
1031
+ setTimeout(() => {
1032
+ mongoose.connect(process.env.MONGO_URI as string, {
1033
+ ...connection,
1034
+ connectTimeoutMS: 3000,
1035
+ socketTimeoutMS: 3000
1036
+ })
1037
+ }, 3000)
1038
+ })
1039
+ this.#connection.on('close', () => {
1040
+ this.#log.logger.info('Mongo connection closed')
1041
+ })
1042
+ this.#connection.on('error', (e: Error) => {
1043
+ this.#log.logger.info('Mongo connection error:')
1044
+ this.#log.logger.error(e)
1045
+ })
1046
+ await mongoose.connect(process.env.MONGO_URI as string, connection)
1047
+ }
1048
+
1049
+ public async start(): Promise<void> {
1050
+ const server = new ApolloServer({
1051
+ schema,
1052
+ plugins: [
1053
+ ApolloServerPluginDrainHttpServer({ httpServer: this.#httpServer }),
1054
+ process.env.NODE_ENV === 'production'
1055
+ ? ApolloServerPluginLandingPageDisabled()
1056
+ : ApolloServerPluginLandingPageGraphQLPlayground()
1057
+ ],
1058
+ context: (): Context => ({
1059
+ log: this.#log.logger
1060
+ })
1061
+ })
1062
+
1063
+ try {
1064
+ await server.start()
1065
+ server.applyMiddleware({
1066
+ app: this.#app,
1067
+ path: '/api'
1068
+ })
1069
+ applyRoutes(this.#app)
1070
+ await this.#mongo()
1071
+ this.#httpServer.listen(PORT, () => {
1072
+ this.#log.logger.info(\`Server listening at port: \${PORT}\`)
1073
+ this.#log.logger.info(
1074
+ \`GraphQL server listening at: http://localhost:\${PORT}\${server.graphqlPath}\`
1075
+ )
1076
+ })
1077
+ } catch (e) {
1078
+ console.error(e)
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ const server = new Server()
1084
+
1085
+ export { server as Server }
1086
+ `,
1087
+ file: `${projectName}/src/network/server.ts`
1088
+ },
1089
+ routes: {
1090
+ home: {
1091
+ content: `import { Response, Request, Router } from 'express'
1092
+
1093
+ import { response } from 'network/response'
1094
+
1095
+ const Home = Router()
1096
+
1097
+ Home.route('').get((req: Request, res: Response) => {
1098
+ response({
1099
+ error: false,
1100
+ message: 'Welcome to your GraphQL Express Backend!',
1101
+ res,
1102
+ status: 200
1103
+ })
1104
+ })
1105
+
1106
+ export { Home }
1107
+ `,
1108
+ file: `${projectName}/src/network/routes/home.ts`
1109
+ },
1110
+ index: {
1111
+ content: "export * from './home'\n",
1112
+ file: `${projectName}/src/network/routes/index.ts`
1113
+ },
1114
+ utils: {
1115
+ index: {
1116
+ content: `import { NextFunction } from 'express'
1117
+ import httpErrors from 'http-errors'
1118
+ import { TObject, TProperties } from '@sinclair/typebox'
1119
+ import Ajv from 'ajv'
1120
+
1121
+ const ajv = new Ajv({
1122
+ removeAdditional: true,
1123
+ useDefaults: true,
1124
+ coerceTypes: true,
1125
+ nullable: true
1126
+ })
1127
+
1128
+ type Middleware = (
1129
+ req: CustomRequest,
1130
+ res: CustomResponse,
1131
+ next: NextFunction
1132
+ ) => void
1133
+
1134
+ const validatorCompiler = <T extends TProperties>(
1135
+ schema: TObject<T>,
1136
+ value: 'body' | 'params'
1137
+ ): Middleware => {
1138
+ return (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
1139
+ const validate = ajv.compile(schema)
1140
+ const ok = validate(req[value])
1141
+
1142
+ if (!ok && validate.errors) {
1143
+ const [error] = validate.errors
1144
+ const errorMessage = \`\${error.dataPath.replace('.', '')} \${error.message}\`
1145
+
1146
+ return next(new httpErrors.UnprocessableEntity(errorMessage))
1147
+ }
1148
+
1149
+ next()
1150
+ }
1151
+ }
1152
+
1153
+ export { validatorCompiler }
1154
+ `,
1155
+ file: `${projectName}/src/network/routes/utils/index.ts`
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ await Promise.all([
1162
+ writeFile(graphQL.index.file, graphQL.index.content),
1163
+ writeFile(
1164
+ graphQL.models.User.index.file,
1165
+ graphQL.models.User.index.content
1166
+ ),
1167
+ writeFile(
1168
+ graphQL.models.User.mutations.file,
1169
+ graphQL.models.User.mutations.content
1170
+ ),
1171
+ writeFile(
1172
+ graphQL.models.User.mutationsResolver.file,
1173
+ graphQL.models.User.mutationsResolver.content
1174
+ ),
1175
+ writeFile(
1176
+ graphQL.models.User.queries.file,
1177
+ graphQL.models.User.queries.content
1178
+ ),
1179
+ writeFile(
1180
+ graphQL.models.User.queriesResolver.file,
1181
+ graphQL.models.User.queriesResolver.content
1182
+ ),
1183
+ writeFile(
1184
+ graphQL.models.User.schemas.file,
1185
+ graphQL.models.User.schemas.content
1186
+ ),
1187
+ writeFile(
1188
+ graphQL.models.User.typeDefs.file,
1189
+ graphQL.models.User.typeDefs.content
1190
+ ),
1191
+ writeFile(
1192
+ graphQL.models.utils.messages.index.file,
1193
+ graphQL.models.utils.messages.index.content
1194
+ ),
1195
+ writeFile(
1196
+ graphQL.models.utils.messages.user.file,
1197
+ graphQL.models.utils.messages.user.content
1198
+ ),
1199
+ writeFile(
1200
+ graphQL.models.utils.index.file,
1201
+ graphQL.models.utils.index.content
1202
+ ),
1203
+ writeFile(graphQL.models.index.file, graphQL.models.index.content),
1204
+ writeFile(network.response.file, network.response.content),
1205
+ writeFile(network.router.file, network.router.content),
1206
+ writeFile(network.server.file, network.server.content),
1207
+ writeFile(network.routes.home.file, network.routes.home.content),
1208
+ writeFile(network.routes.index.file, network.routes.index.content),
1209
+ writeFile(
1210
+ network.routes.utils.index.file,
1211
+ network.routes.utils.index.content
1212
+ )
1213
+ ])
1214
+ }
1215
+
1216
+ /**
1217
+ * @param {Object} args
1218
+ * @param {String} args.projectName
1219
+ */
1220
+ const fastifyRestNetwork = async ({ projectName }) => {
1221
+ const createFoldersCommand = `mkdir ${projectName}/src/network/utils`
1222
+
1223
+ if (platform() === 'win32')
1224
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
1225
+ else await exec(createFoldersCommand)
1226
+
1227
+ const network = {
1228
+ response: {
1229
+ content: `import { FastifyReply } from 'fastify'
1230
+
1231
+ const response = ({
1232
+ error,
1233
+ message,
1234
+ reply,
1235
+ status
1236
+ }: {
1237
+ error: boolean
1238
+ message: unknown
1239
+ reply: FastifyReply
1240
+ status: number
1241
+ }): void => {
1242
+ reply.code(status).send({ error, message })
1243
+ }
1244
+
1245
+ export { response }
1246
+ `,
1247
+ file: `${projectName}/src/network/response.ts`
1248
+ },
1249
+ router: {
1250
+ content: `import { FastifyInstance } from 'fastify'
1251
+ import { HttpError } from 'http-errors'
1252
+
1253
+ import { response } from './response'
1254
+ import { Home, User, Docs } from './routes'
1255
+
1256
+ const routers = [Docs, User]
1257
+ const applyRoutes = (app: FastifyInstance): void => {
1258
+ Home(app)
1259
+ routers.forEach(router => router(app))
1260
+
1261
+ // Handling 404 error
1262
+ app.setNotFoundHandler((request, reply) => {
1263
+ response({
1264
+ error: true,
1265
+ message: 'This route does not exists',
1266
+ reply,
1267
+ status: 404
1268
+ })
1269
+ })
1270
+ app.setErrorHandler<HttpError>((error, request, reply) => {
1271
+ response({
1272
+ error: true,
1273
+ message: error.message,
1274
+ reply,
1275
+ status: error.status ?? 500
1276
+ })
1277
+ })
1278
+ }
1279
+
1280
+ export { applyRoutes }
1281
+ `,
1282
+ file: `${projectName}/src/network/router.ts`
1283
+ },
1284
+ server: {
1285
+ content: `import fastify, { FastifyInstance } from 'fastify'
1286
+ import mongoose from 'mongoose'
1287
+
1288
+ import { applyRoutes } from './router'
1289
+ import { validatorCompiler } from './utils'
1290
+
1291
+ const PORT = process.env.PORT ?? 1996
1292
+
1293
+ class Server {
1294
+ #app: FastifyInstance
1295
+ #connection: mongoose.Connection | undefined
1296
+
1297
+ constructor() {
1298
+ this.#app = fastify({ logger: { prettyPrint: true } })
1299
+ this.#config()
1300
+ }
1301
+
1302
+ #config() {
1303
+ this.#app.register(require('fastify-cors'), {})
1304
+ this.#app.addHook('preHandler', (req, reply, done) => {
1305
+ reply.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
1306
+ reply.header('Access-Control-Allow-Origin', '*')
1307
+ reply.header(
1308
+ 'Access-Control-Allow-Headers',
1309
+ 'Authorization, Content-Type'
1310
+ )
1311
+ reply.header('x-powered-by', 'Simba.js')
1312
+ done()
1313
+ })
1314
+ this.#app.setValidatorCompiler(validatorCompiler)
1315
+ applyRoutes(this.#app)
1316
+ }
1317
+
1318
+ async #mongo(): Promise<void> {
1319
+ this.#connection = mongoose.connection
1320
+ const connection = {
1321
+ keepAlive: true,
1322
+ useNewUrlParser: true,
1323
+ useUnifiedTopology: true
1324
+ }
1325
+ this.#connection.on('connected', () => {
1326
+ this.#app.log.info('Mongo connection established.')
1327
+ })
1328
+ this.#connection.on('reconnected', () => {
1329
+ this.#app.log.info('Mongo connection reestablished')
507
1330
  })
508
1331
  this.#connection.on('disconnected', () => {
509
1332
  this.#app.log.info('Mongo connection disconnected')
@@ -868,39 +1691,846 @@ export { validatorCompiler }
868
1691
  }
869
1692
  }
870
1693
 
871
- await writeFile(network.response.file, network.response.content)
872
- await writeFile(network.router.file, network.router.content)
873
- await writeFile(network.server.file, network.server.content)
874
- await writeFile(network.routes.docs.file, network.routes.docs.content)
875
- await writeFile(network.routes.home.file, network.routes.home.content)
876
- await writeFile(network.routes.index.file, network.routes.index.content)
877
- await writeFile(network.routes.user.file, network.routes.user.content)
878
- await writeFile(network.utils.index.file, network.utils.index.content)
1694
+ await Promise.all([
1695
+ writeFile(network.response.file, network.response.content),
1696
+ writeFile(network.router.file, network.router.content),
1697
+ writeFile(network.server.file, network.server.content),
1698
+ writeFile(network.routes.docs.file, network.routes.docs.content),
1699
+ writeFile(network.routes.home.file, network.routes.home.content),
1700
+ writeFile(network.routes.index.file, network.routes.index.content),
1701
+ writeFile(network.routes.user.file, network.routes.user.content),
1702
+ writeFile(network.utils.index.file, network.utils.index.content)
1703
+ ])
879
1704
  }
1705
+
880
1706
  /**
881
1707
  * @param {Object} args
882
- * @param {Boolean} args.fastify
883
1708
  * @param {String} args.projectName
884
1709
  */
885
- module.exports = async ({ fastify, projectName }) => {
886
- const createFoldersCommand = `mkdir ${projectName}/src/network \
887
- ${projectName}/src/network/routes`
1710
+ const fastifyGraphQLNetwork = async ({ projectName }) => {
1711
+ const createFoldersCommand = `mkdir ${projectName}/src/graphQL/models \
1712
+ ${projectName}/src/graphQL/models/User \
1713
+ ${projectName}/src/graphQL/models/utils \
1714
+ ${projectName}/src/graphQL/models/utils/messages`
888
1715
 
889
1716
  if (platform() === 'win32')
890
1717
  await exec(createFoldersCommand.replaceAll('/', '\\'))
891
1718
  else await exec(createFoldersCommand)
892
1719
 
893
- const network = {
1720
+ const graphQL = {
894
1721
  index: {
895
- content: `export * from './routes'
896
- export * from './server'
1722
+ content: "export * from './models'\n",
1723
+ file: `${projectName}/src/graphQL/index.ts`
1724
+ },
1725
+ models: {
1726
+ User: {
1727
+ index: {
1728
+ content: `import { makeExecutableSchema } from '@graphql-tools/schema'
1729
+
1730
+ import { User as UserTD } from './typeDefs'
1731
+ import { Query } from './queriesResolver'
1732
+ import { Mutation } from './mutationsResolver'
1733
+
1734
+ const resolvers = {
1735
+ Query,
1736
+ Mutation
1737
+ }
1738
+
1739
+ const User = makeExecutableSchema({
1740
+ typeDefs: UserTD,
1741
+ resolvers
1742
+ })
1743
+
1744
+ export { User }
897
1745
  `,
898
- file: `${projectName}/src/network/index.ts`
899
- }
1746
+ file: `${projectName}/src/graphQL/models/User/index.ts`
1747
+ },
1748
+ mutations: {
1749
+ content: `import { ApolloError } from 'apollo-server-core'
1750
+
1751
+ import { store, remove, update } from 'database'
1752
+ import { UserDTO } from 'schemas'
1753
+ import { EFU, MFU, GE, errorHandling } from '../utils'
1754
+
1755
+ const storeUser = async (
1756
+ { user }: { user: UserDTO },
1757
+ { log }: Context
1758
+ ): Promise<UserDTO> => {
1759
+ try {
1760
+ const result = await store(user)
1761
+
1762
+ return result
1763
+ } catch (e) {
1764
+ return errorHandling({
1765
+ e,
1766
+ message: GE.INTERNAL_SERVER_ERROR,
1767
+ code: 'INTERNAL_SERVER_ERROR',
1768
+ log
1769
+ })
900
1770
  }
1771
+ }
901
1772
 
902
- await writeFile(network.index.file, network.index.content)
1773
+ const deleteAllUsers = async ({ log }: Context): Promise<string> => {
1774
+ try {
1775
+ const usersDeleted = (await remove()) as number
1776
+
1777
+ if (usersDeleted >= 1) return MFU.ALL_USERS_DELETED
1778
+
1779
+ if (usersDeleted === 0)
1780
+ throw new ApolloError(EFU.NOTHING_TO_DELETE, 'NOTHING_TO_DELETE')
1781
+
1782
+ throw new ApolloError(GE.INTERNAL_SERVER_ERROR, 'INTERNAL_SERVER_ERROR')
1783
+ } catch (e) {
1784
+ return errorHandling({
1785
+ e,
1786
+ message: GE.INTERNAL_SERVER_ERROR,
1787
+ code: 'INTERNAL_SERVER_ERROR',
1788
+ log
1789
+ })
1790
+ }
1791
+ }
1792
+
1793
+ const updateUser = async (
1794
+ { user }: { user: UserDTO },
1795
+ { log }: Context
1796
+ ): Promise<UserDTO> => {
1797
+ try {
1798
+ const updatedUser = await update(user)
1799
+
1800
+ if (!updatedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
1801
+
1802
+ return updatedUser
1803
+ } catch (e) {
1804
+ return errorHandling({
1805
+ e,
1806
+ message: GE.INTERNAL_SERVER_ERROR,
1807
+ code: 'INTERNAL_SERVER_ERROR',
1808
+ log
1809
+ })
1810
+ }
1811
+ }
1812
+
1813
+ const deleteUser = async (
1814
+ { id }: { id: string },
1815
+ { log }: Context
1816
+ ): Promise<string> => {
1817
+ try {
1818
+ const deletedUser = await remove(id)
1819
+
1820
+ if (!deletedUser) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
1821
+
1822
+ return MFU.USER_DELETED
1823
+ } catch (e) {
1824
+ return errorHandling({
1825
+ e,
1826
+ message: GE.INTERNAL_SERVER_ERROR,
1827
+ code: 'INTERNAL_SERVER_ERROR',
1828
+ log
1829
+ })
1830
+ }
1831
+ }
1832
+
1833
+ export { storeUser, deleteAllUsers, updateUser, deleteUser }
1834
+ `,
1835
+ file: `${projectName}/src/graphQL/models/User/mutations.ts`
1836
+ },
1837
+ mutationsResolver: {
1838
+ content: `import { DefinedError } from 'ajv'
1839
+ import { ApolloError } from 'apollo-server-core'
1840
+
1841
+ import { ajv, idSchema, UserDTO } from 'schemas'
1842
+ import { storeUserSchema, updateUserSchema } from './schemas'
1843
+ import { storeUser, updateUser, deleteUser, deleteAllUsers } from './mutations'
1844
+ import { errorHandling, GE } from '../utils'
1845
+
1846
+ const Mutation = {
1847
+ storeUser: async (
1848
+ parent: unknown,
1849
+ { user }: { user: UserDTO },
1850
+ context: Context
1851
+ ): Promise<UserDTO> => {
1852
+ const { log } = context
1853
+ const validate = ajv.compile(storeUserSchema)
1854
+
1855
+ try {
1856
+ const ok = validate(user)
1857
+
1858
+ if (!ok)
1859
+ throw new ApolloError(
1860
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
1861
+ '/',
1862
+ ''
1863
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
1864
+ 'UNPROCESSABLE_ENTITY'
1865
+ )
1866
+
1867
+ return await storeUser({ user }, context)
1868
+ } catch (e) {
1869
+ log.error(validate.errors)
1870
+
1871
+ return errorHandling({
1872
+ e,
1873
+ message: GE.INTERNAL_SERVER_ERROR,
1874
+ code: 'INTERNAL_SERVER_ERROR',
1875
+ log
1876
+ })
1877
+ }
1878
+ },
1879
+ updateUser: async (
1880
+ parent: unknown,
1881
+ { user }: { user: UserDTO },
1882
+ context: Context
1883
+ ): Promise<UserDTO> => {
1884
+ const validate = ajv.compile(updateUserSchema)
1885
+ const { log } = context
1886
+
1887
+ try {
1888
+ const ok = validate(user)
1889
+
1890
+ if (!ok)
1891
+ throw new ApolloError(
1892
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
1893
+ '/',
1894
+ ''
1895
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
1896
+ 'UNPROCESSABLE_ENTITY'
1897
+ )
1898
+
1899
+ return await updateUser({ user }, context)
1900
+ } catch (e) {
1901
+ log.error(validate.errors)
1902
+
1903
+ return errorHandling({
1904
+ e,
1905
+ message: GE.INTERNAL_SERVER_ERROR,
1906
+ code: 'INTERNAL_SERVER_ERROR',
1907
+ log
1908
+ })
1909
+ }
1910
+ },
1911
+ deleteUser: async (
1912
+ parent: unknown,
1913
+ { id }: { id: string },
1914
+ context: Context
1915
+ ): Promise<string> => {
1916
+ const validate = ajv.compile(idSchema)
1917
+ const { log } = context
1918
+
1919
+ try {
1920
+ const ok = validate({ id })
1921
+
1922
+ if (!ok)
1923
+ throw new ApolloError(
1924
+ \`\${(validate.errors as DefinedError[])[0].instancePath.replace(
1925
+ '/',
1926
+ ''
1927
+ )} \${(validate.errors as DefinedError[])[0].message as string}\`,
1928
+ 'UNPROCESSABLE_ENTITY'
1929
+ )
1930
+
1931
+ return await deleteUser({ id }, context)
1932
+ } catch (e) {
1933
+ log.error(validate.errors)
1934
+
1935
+ return errorHandling({
1936
+ e,
1937
+ message: GE.INTERNAL_SERVER_ERROR,
1938
+ code: 'INTERNAL_SERVER_ERROR',
1939
+ log
1940
+ })
1941
+ }
1942
+ },
1943
+ deleteAllUsers: async (
1944
+ parent: unknown,
1945
+ args: unknown,
1946
+ context: Context
1947
+ ): Promise<string> => {
1948
+ const { log } = context
1949
+ try {
1950
+ return await deleteAllUsers(context)
1951
+ } catch (e) {
1952
+ return errorHandling({
1953
+ e,
1954
+ message: GE.INTERNAL_SERVER_ERROR,
1955
+ code: 'INTERNAL_SERVER_ERROR',
1956
+ log
1957
+ })
1958
+ }
1959
+ }
1960
+ }
1961
+
1962
+ export { Mutation }
1963
+ `,
1964
+ file: `${projectName}/src/graphQL/models/User/mutationsResolver.ts`
1965
+ },
1966
+ queries: {
1967
+ content: `import { ApolloError } from 'apollo-server-core'
1968
+
1969
+ import { get } from 'database'
1970
+ import { UserDTO } from 'schemas'
1971
+ import { EFU, GE, errorHandling } from '../utils'
1972
+
1973
+ const getUsers = async (
1974
+ parent: unknown,
1975
+ args: unknown,
1976
+ { log }: Context
1977
+ ): Promise<UserDTO[]> => {
1978
+ try {
1979
+ const users = (await get()) as UserDTO[]
1980
+
1981
+ return users
1982
+ } catch (e) {
1983
+ return errorHandling({
1984
+ e,
1985
+ message: GE.INTERNAL_SERVER_ERROR,
1986
+ code: 'INTERNAL_SERVER_ERROR',
1987
+ log
1988
+ })
1989
+ }
1990
+ }
1991
+
1992
+ const getUser = async (
1993
+ { id }: { id: string },
1994
+ { log }: Context
1995
+ ): Promise<UserDTO> => {
1996
+ try {
1997
+ const user = (await get(id as string)) as UserDTO | null
1998
+
1999
+ if (!user) throw new ApolloError(EFU.NOT_FOUND, 'NOT_FOUND')
2000
+
2001
+ return user
2002
+ } catch (e) {
2003
+ return errorHandling({
2004
+ e,
2005
+ message: GE.INTERNAL_SERVER_ERROR,
2006
+ code: 'INTERNAL_SERVER_ERROR',
2007
+ log
2008
+ })
2009
+ }
2010
+ }
2011
+
2012
+ export { getUsers, getUser }
2013
+ `,
2014
+ file: `${projectName}/src/graphQL/models/User/queries.ts`
2015
+ },
2016
+ queriesResolver: {
2017
+ content: `import { DefinedError } from 'ajv'
2018
+ import { ApolloError } from 'apollo-server-core'
2019
+
2020
+ import { ajv, idSchema, UserDTO } from 'schemas'
2021
+ import { getUser, getUsers } from './queries'
2022
+ import { errorHandling, GE } from '../utils'
2023
+
2024
+ const Query = {
2025
+ getUser: async (
2026
+ parent: unknown,
2027
+ { id }: { id: string },
2028
+ context: Context
2029
+ ): Promise<UserDTO> => {
2030
+ const { log } = context
2031
+ const validate = ajv.compile(idSchema)
2032
+
2033
+ try {
2034
+ const ok = validate({ id })
2035
+
2036
+ if (!ok)
2037
+ throw new ApolloError(
2038
+ \`id \${(validate.errors as DefinedError[])[0].message as string}\`,
2039
+ 'UNPROCESSABLE_ENTITY'
2040
+ )
2041
+
2042
+ return await getUser({ id }, context)
2043
+ } catch (e) {
2044
+ log.error(validate.errors)
2045
+
2046
+ return errorHandling({
2047
+ e,
2048
+ message: GE.INTERNAL_SERVER_ERROR,
2049
+ code: 'INTERNAL_SERVER_ERROR',
2050
+ log
2051
+ })
2052
+ }
2053
+ },
2054
+ getUsers
2055
+ }
2056
+
2057
+ export { Query }
2058
+ `,
2059
+ file: `${projectName}/src/graphQL/models/User/queriesResolver.ts`
2060
+ },
2061
+ schemas: {
2062
+ content: `import { Type } from '@sinclair/typebox'
2063
+
2064
+ const updateUserSchema = Type.Object(
2065
+ {
2066
+ id: Type.String({ minLength: 24, maxLength: 24 }),
2067
+ lastName: Type.String(),
2068
+ name: Type.String()
2069
+ },
2070
+ { additionalProperties: false }
2071
+ )
2072
+
2073
+ const storeUserSchema = Type.Object(
2074
+ {
2075
+ lastName: Type.String({ minLength: 1 }),
2076
+ name: Type.String({ minLength: 1 })
2077
+ },
2078
+ { additionalProperties: false }
2079
+ )
2080
+
2081
+ export { updateUserSchema, storeUserSchema }
2082
+ `,
2083
+ file: `${projectName}/src/graphQL/models/User/schemas.ts`
2084
+ },
2085
+ typeDefs: {
2086
+ content: `import { gql } from 'apollo-server-core'
2087
+
2088
+ const User = gql\`
2089
+ type User {
2090
+ id: ID!
2091
+ name: String!
2092
+ lastName: String!
2093
+ updatedAt: String!
2094
+ }
2095
+
2096
+ type Query {
2097
+ getUsers: [User!]!
2098
+ getUser(id: ID!): User!
2099
+ }
2100
+
2101
+ input StoreUserInput {
2102
+ lastName: String!
2103
+ name: String!
2104
+ }
2105
+
2106
+ input UpdateUserInput {
2107
+ id: String!
2108
+ lastName: String!
2109
+ name: String!
2110
+ }
2111
+
2112
+ type Mutation {
2113
+ storeUser(user: StoreUserInput!): User!
2114
+ deleteAllUsers: String
2115
+ updateUser(user: UpdateUserInput!): User!
2116
+ deleteUser(id: ID!): String
2117
+ }
2118
+ \`
2119
+
2120
+ export { User }
2121
+ `,
2122
+ file: `${projectName}/src/graphQL/models/User/typeDefs.ts`
2123
+ }
2124
+ },
2125
+ utils: {
2126
+ messages: {
2127
+ index: {
2128
+ content: `enum GenericErrors {
2129
+ INTERNAL_SERVER_ERROR = 'Something went wrong'
2130
+ }
2131
+
2132
+ export { GenericErrors as GE }
2133
+ export * from './user'
2134
+ `,
2135
+ file: `${projectName}/src/graphQL/models/utils/messages/index.ts`
2136
+ },
2137
+ user: {
2138
+ content: `enum ErrorForUser {
2139
+ NOT_FOUND = 'The requested user does not exists',
2140
+ NOTHING_TO_DELETE = 'There is no user to be deleted'
2141
+ }
2142
+
2143
+ enum MessageForUser {
2144
+ ALL_USERS_DELETED = 'All the users were deleted successfully',
2145
+ USER_DELETED = 'The requested user was successfully deleted'
2146
+ }
2147
+
2148
+ export { ErrorForUser as EFU, MessageForUser as MFU }
2149
+ `,
2150
+ file: `${projectName}/src/graphQL/models/utils/messages/user.ts`
2151
+ }
2152
+ },
2153
+ index: {
2154
+ content: `import { ApolloError } from 'apollo-server-core'
2155
+ import { FastifyLoggerInstance } from 'fastify'
2156
+
2157
+ const errorHandling = ({
2158
+ e,
2159
+ message,
2160
+ code,
2161
+ log
2162
+ }: {
2163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2164
+ e: any
2165
+ message: string
2166
+ code: string
2167
+ log: FastifyLoggerInstance
2168
+ }): never => {
2169
+ log.error(e)
2170
+
2171
+ if (e instanceof ApolloError) throw e
2172
+
2173
+ throw new ApolloError(message ?? e.message, code)
2174
+ }
2175
+
2176
+ export { errorHandling }
2177
+ export * from './messages'
2178
+ `,
2179
+ file: `${projectName}/src/graphQL/models/utils/index.ts`
2180
+ }
2181
+ },
2182
+ index: {
2183
+ content: `import { mergeSchemas } from '@graphql-tools/schema'
2184
+
2185
+ import { User } from './User'
2186
+
2187
+ const mergedSchema = mergeSchemas({
2188
+ schemas: [User]
2189
+ })
2190
+
2191
+ export { mergedSchema }
2192
+ `,
2193
+ file: `${projectName}/src/graphQL/models/index.ts`
2194
+ }
2195
+ }
2196
+ }
2197
+ const network = {
2198
+ response: {
2199
+ content: `import { FastifyReply } from 'fastify'
2200
+
2201
+ const response = ({
2202
+ error,
2203
+ message,
2204
+ reply,
2205
+ status
2206
+ }: {
2207
+ error: boolean
2208
+ message: unknown
2209
+ reply: FastifyReply
2210
+ status: number
2211
+ }): void => {
2212
+ reply.code(status).send({ error, message })
2213
+ }
2214
+
2215
+ export { response }
2216
+ `,
2217
+ file: `${projectName}/src/network/response.ts`
2218
+ },
2219
+ router: {
2220
+ content: `import { FastifyInstance } from 'fastify'
2221
+ import { HttpError } from 'http-errors'
2222
+
2223
+ import { response } from './response'
2224
+ import { Home, Docs } from './routes'
2225
+
2226
+ const routers = [Docs]
2227
+ const applyRoutes = (app: FastifyInstance): void => {
2228
+ Home(app)
2229
+ routers.forEach(router => router(app))
2230
+
2231
+ // Handling 404 error
2232
+ app.setNotFoundHandler((request, reply) => {
2233
+ response({
2234
+ error: true,
2235
+ message: 'This route does not exists',
2236
+ reply,
2237
+ status: 404
2238
+ })
2239
+ })
2240
+ app.setErrorHandler<HttpError>((error, request, reply) => {
2241
+ response({
2242
+ error: true,
2243
+ message: error.message,
2244
+ reply,
2245
+ status: error.status ?? 500
2246
+ })
2247
+ })
2248
+ }
2249
+
2250
+ export { applyRoutes }
2251
+ `,
2252
+ file: `${projectName}/src/network/router.ts`
2253
+ },
2254
+ server: {
2255
+ content: `import fastify, { FastifyInstance } from 'fastify'
2256
+ import { ApolloServer } from 'apollo-server-fastify'
2257
+ import {
2258
+ ApolloServerPluginDrainHttpServer,
2259
+ ApolloServerPluginLandingPageGraphQLPlayground,
2260
+ ApolloServerPluginLandingPageDisabled
2261
+ } from 'apollo-server-core'
2262
+ import { ApolloServerPlugin } from 'apollo-server-plugin-base'
2263
+ import mongoose from 'mongoose'
2264
+
2265
+ import { mergedSchema as schema } from 'graphQL'
2266
+ import { applyRoutes } from './router'
2267
+
2268
+ const PORT = process.env.PORT ?? 1996
2269
+
2270
+ class Server {
2271
+ #app: FastifyInstance
2272
+ #connection: mongoose.Connection | undefined
2273
+
2274
+ constructor() {
2275
+ this.#app = fastify({ logger: { prettyPrint: true } })
2276
+ this.#config()
2277
+ }
2278
+
2279
+ #config() {
2280
+ this.#app.addHook('preHandler', (req, reply, done) => {
2281
+ reply.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
2282
+ reply.header('Access-Control-Allow-Origin', '*')
2283
+ reply.header(
2284
+ 'Access-Control-Allow-Headers',
2285
+ 'Authorization, Content-Type'
2286
+ )
2287
+ reply.header('x-powered-by', 'Simba.js')
2288
+ done()
2289
+ })
2290
+ applyRoutes(this.#app)
2291
+ }
2292
+
2293
+ async #mongo(): Promise<void> {
2294
+ this.#connection = mongoose.connection
2295
+ const connection = {
2296
+ keepAlive: true,
2297
+ useNewUrlParser: true,
2298
+ useUnifiedTopology: true
2299
+ }
2300
+ this.#connection.on('connected', () => {
2301
+ this.#app.log.info('Mongo connection established.')
2302
+ })
2303
+ this.#connection.on('reconnected', () => {
2304
+ this.#app.log.info('Mongo connection reestablished')
2305
+ })
2306
+ this.#connection.on('disconnected', () => {
2307
+ this.#app.log.info('Mongo connection disconnected')
2308
+ this.#app.log.info('Trying to reconnected to Mongo...')
2309
+ setTimeout(() => {
2310
+ mongoose.connect(process.env.MONGO_URI as string, {
2311
+ ...connection,
2312
+ connectTimeoutMS: 3000,
2313
+ socketTimeoutMS: 3000
2314
+ })
2315
+ }, 3000)
2316
+ })
2317
+ this.#connection.on('close', () => {
2318
+ this.#app.log.info('Mongo connection closed')
2319
+ })
2320
+ this.#connection.on('error', (e: Error) => {
2321
+ this.#app.log.info('Mongo connection error:')
2322
+ this.#app.log.error(e)
2323
+ })
2324
+ await mongoose.connect(process.env.MONGO_URI as string, connection)
2325
+ }
2326
+
2327
+ #fastifyAppClosePlugin(): ApolloServerPlugin {
2328
+ const app = this.#app
2329
+
2330
+ return {
2331
+ async serverWillStart() {
2332
+ return {
2333
+ async drainServer() {
2334
+ await app.close()
2335
+ }
2336
+ }
2337
+ }
2338
+ }
2339
+ }
2340
+
2341
+ public async start(): Promise<void> {
2342
+ const server = new ApolloServer({
2343
+ schema,
2344
+ plugins: [
2345
+ this.#fastifyAppClosePlugin(),
2346
+ ApolloServerPluginDrainHttpServer({ httpServer: this.#app.server }),
2347
+ process.env.NODE_ENV === 'production'
2348
+ ? ApolloServerPluginLandingPageDisabled()
2349
+ : ApolloServerPluginLandingPageGraphQLPlayground()
2350
+ ],
2351
+ context: (): Context => ({
2352
+ log: this.#app.log
2353
+ })
2354
+ })
2355
+
2356
+ try {
2357
+ await server.start()
2358
+ this.#app.register(
2359
+ server.createHandler({
2360
+ path: '/api'
2361
+ })
2362
+ )
2363
+ await this.#mongo()
2364
+ await this.#app.listen(PORT)
2365
+ this.#app.log.info(
2366
+ \`GraphQL server listening at: http://localhost:\${PORT}\${server.graphqlPath}\`
2367
+ )
2368
+ } catch (e) {
2369
+ console.error(e)
2370
+ }
2371
+ }
2372
+ }
2373
+
2374
+ const server = new Server()
2375
+
2376
+ export { server as Server }
2377
+ `,
2378
+ file: `${projectName}/src/network/server.ts`
2379
+ },
2380
+ routes: {
2381
+ docs: {
2382
+ content: `import { FastifyInstance } from 'fastify'
2383
+ import fastifySwagger from 'fastify-swagger'
2384
+
2385
+ const Docs = (app: FastifyInstance, prefix = '/api'): void => {
2386
+ app.register(fastifySwagger, {
2387
+ routePrefix: \`\${prefix}/docs\`,
2388
+ openapi: {
2389
+ info: {
2390
+ title: 'Test swagger',
2391
+ description: 'Testing the Fastify swagger API',
2392
+ version: '0.1.0',
2393
+ contact: {
2394
+ email: 'sluzquinosa@uni.pe'
2395
+ },
2396
+ license: {
2397
+ name: 'MIT',
2398
+ url: 'https://opensource.org/licenses/MIT'
2399
+ }
2400
+ },
2401
+ servers: [
2402
+ {
2403
+ url: 'http://localhost:1996/api',
2404
+ description: 'test-fastify local API'
2405
+ }
2406
+ ],
2407
+ tags: [
2408
+ {
2409
+ name: 'user',
2410
+ description: 'User related endpoints'
2411
+ }
2412
+ ]
2413
+ },
2414
+ exposeRoute: true
2415
+ })
2416
+ }
2417
+
2418
+ export { Docs }
2419
+ `,
2420
+ file: `${projectName}/src/network/routes/docs.ts`
2421
+ },
2422
+ home: {
2423
+ content: `import { FastifyInstance } from 'fastify'
2424
+ import { response } from 'network/response'
2425
+
2426
+ const Home = (app: FastifyInstance, prefix = '/'): void => {
2427
+ app.get(\`\${prefix}\`, (request, reply) => {
2428
+ response({
2429
+ error: false,
2430
+ message: 'Welcome to your Fastify GraphQL Backend!',
2431
+ reply,
2432
+ status: 200
2433
+ })
2434
+ })
2435
+ }
2436
+
2437
+ export { Home }
2438
+ `,
2439
+ file: `${projectName}/src/network/routes/home.ts`
2440
+ },
2441
+ index: {
2442
+ content: `export * from './home'
2443
+ export * from './docs'
2444
+ `,
2445
+ file: `${projectName}/src/network/routes/index.ts`
2446
+ }
2447
+ }
2448
+ }
2449
+
2450
+ await Promise.all([
2451
+ writeFile(graphQL.index.file, graphQL.index.content),
2452
+ writeFile(
2453
+ graphQL.models.User.index.file,
2454
+ graphQL.models.User.index.content
2455
+ ),
2456
+ writeFile(
2457
+ graphQL.models.User.mutations.file,
2458
+ graphQL.models.User.mutations.content
2459
+ ),
2460
+ writeFile(
2461
+ graphQL.models.User.mutationsResolver.file,
2462
+ graphQL.models.User.mutationsResolver.content
2463
+ ),
2464
+ writeFile(
2465
+ graphQL.models.User.queries.file,
2466
+ graphQL.models.User.queries.content
2467
+ ),
2468
+ writeFile(
2469
+ graphQL.models.User.queriesResolver.file,
2470
+ graphQL.models.User.queriesResolver.content
2471
+ ),
2472
+ writeFile(
2473
+ graphQL.models.User.schemas.file,
2474
+ graphQL.models.User.schemas.content
2475
+ ),
2476
+ writeFile(
2477
+ graphQL.models.User.typeDefs.file,
2478
+ graphQL.models.User.typeDefs.content
2479
+ ),
2480
+ writeFile(
2481
+ graphQL.models.utils.messages.index.file,
2482
+ graphQL.models.utils.messages.index.content
2483
+ ),
2484
+ writeFile(
2485
+ graphQL.models.utils.messages.user.file,
2486
+ graphQL.models.utils.messages.user.content
2487
+ ),
2488
+ writeFile(
2489
+ graphQL.models.utils.index.file,
2490
+ graphQL.models.utils.index.content
2491
+ ),
2492
+ writeFile(graphQL.models.index.file, graphQL.models.index.content),
2493
+ writeFile(network.response.file, network.response.content),
2494
+ writeFile(network.router.file, network.router.content),
2495
+ writeFile(network.server.file, network.server.content),
2496
+ writeFile(network.routes.docs.file, network.routes.docs.content),
2497
+ writeFile(network.routes.home.file, network.routes.home.content),
2498
+ writeFile(network.routes.index.file, network.routes.index.content)
2499
+ ])
2500
+ }
2501
+
2502
+ /**
2503
+ * @param {Object} args
2504
+ * @param {Boolean} args.fastify
2505
+ * @param {String} args.projectName
2506
+ * @param {Boolean} args.graphql
2507
+ */
2508
+ module.exports = async ({ fastify, projectName, graphql }) => {
2509
+ const createFoldersCommand = `mkdir ${projectName}/src/network \
2510
+ ${projectName}/src/network/routes ${
2511
+ graphql ? `${projectName}/src/graphQL` : ''
2512
+ }`
2513
+
2514
+ if (platform() === 'win32')
2515
+ await exec(createFoldersCommand.replaceAll('/', '\\'))
2516
+ else await exec(createFoldersCommand)
2517
+
2518
+ const network = {
2519
+ index: {
2520
+ content: `export * from './routes'
2521
+ export * from './server'
2522
+ `,
2523
+ file: `${projectName}/src/network/index.ts`
2524
+ }
2525
+ }
2526
+
2527
+ await writeFile(network.index.file, network.index.content)
2528
+
2529
+ if (fastify && graphql) return await fastifyGraphQLNetwork({ projectName })
2530
+
2531
+ if (fastify) return await fastifyRestNetwork({ projectName })
2532
+
2533
+ if (graphql) return await expressGraphQLNetwork({ projectName })
903
2534
 
904
- if (fastify) await fastifyRestNetwork({ projectName })
905
- else await expressRestNetwork({ projectName })
2535
+ return await expressRestNetwork({ projectName })
906
2536
  }