@anthonylzq/simba.js 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Simba.js
2
2
 
3
3
  [![NPM version](https://img.shields.io/npm/v/@anthonylzq/simba.js.svg?style=flat)](https://www.npmjs.com/package/@anthonylzq/simba.js)
4
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/AnthonyLzq/simba.js/blob/master/LICENSE)
4
5
  [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
6
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
5
7
 
6
8
  Set up a modern backend app by running one command. This project has the goal to create a complete setup for a backend application using `TypeScript` and `Express` or `Fastify`. It will create many files that are usually created manually. Think about Simba.js like a [CRA](https://create-react-app.dev/), but for backend development. Check the [**project structure**](#project-structure) for more information.
7
9
 
@@ -269,7 +271,7 @@ If you want to check the content of the files, please check the [example](https:
269
271
 
270
272
  - The provided project structure is inspired in my personal experience as [`Node.js`](https://nodejs.org/en/) developer and the [`Nest`](https://nestjs.com/) framework. It follows a layered architecture:
271
273
 
272
- 1. Presentation layer (network layer): it is represented by the network folder, which contains the routes and the necessary schemas for each route.
274
+ 1. Presentation layer (network layer): it is represented by the network and schemas folders, which contains the routes and the schemas necessary for each route.
273
275
  2. Business layer (services layer): it is represented by the services folder, which contains all the code related to the business logic of your application.
274
276
  3. Persistance layer (database layer): it is represented by the database folder, which contains the database connection, models and queries (that will be used by the services). Multiple database connection are possible and should be implemented here.
275
277
 
@@ -282,7 +284,14 @@ If you want to check the content of the files, please check the [example](https:
282
284
 
283
285
  ## What is new?
284
286
 
285
- Please check the [`changelog.md`](https://github.com/AnthonyLzq/simba.js/blob/master/CHANGELOG.md) file.
287
+ Please check the [`changelog.md`](https://github.com/AnthonyLzq/simba.js/blob/master/CHANGELOG.md) file. Also, if you want to check what is coming, check the [road map](https://simbajs.notion.site/simbajs/783092dc7d444067b4c56a25d671f658?v=31060f3d17524ca58870e86c2960a6df).
288
+
289
+ ### Version 4.x.x
290
+
291
+ In this major version I would be focusing on adding new possible configurations according to the road map. The major changes of this version will be described here:
292
+
293
+ - Replaced [`joi`](https://www.npmjs.com/package/joi) in favor of [`ajv`](https://www.npmjs.com/package/ajv) + [`@sinclair/typebox`](https://www.npmjs.com/package/@sinclair/typebox) in the Express case. [Why did I do this?](https://simbajs.notion.site/TypeBox-support-for-Express-f4e3cf8dd06f4c7ba4d8e4051a52688c)
294
+ - Using more descriptive nouns, now every database object is represented with a DBO at the end, like: _UserDBO_. Also the objects that are sent and received will have a DTO at the end, like: _UserDTO_.
286
295
 
287
296
  ## <a name="notes"></a>Notes
288
297
 
@@ -312,6 +321,8 @@ Here is the list of the packages that are being installed, as `devDependencies`:
312
321
 
313
322
  As `dependencies`:
314
323
 
324
+ - [`@sinclair/typebox`](https://www.npmjs.com/package/@sinclair/typebox)
325
+ - [`ajv`](https://www.npmjs.com/package/ajv)
315
326
  - [`http-errors`](https://www.npmjs.com/package/http-errors)
316
327
  - [`mongoose`](https://mongoosejs.com/)
317
328
 
@@ -326,7 +337,6 @@ As `devDependencies`:
326
337
  As `dependencies`:
327
338
 
328
339
  - [`express`](https://expressjs.com/)
329
- - [`joi`](https://joi.dev/api/?v=17.4.2)
330
340
  - [`morgan`](https://www.npmjs.com/package/morgan)
331
341
  - [`swagger-ui-express`](https://www.npmjs.com/package/swagger-ui-express)
332
342
 
@@ -334,7 +344,6 @@ As `dependencies`:
334
344
 
335
345
  As `dependencies`:
336
346
 
337
- - [`@sinclair/typebox`](https://www.npmjs.com/package/@sinclair/typebox)
338
347
  - [`fastify`](https://www.npmjs.com/package/fastify)
339
348
  - [`fastify-swagger`](https://www.npmjs.com/package/fastify-swagger)
340
349
 
package/lib/index.js CHANGED
@@ -287,8 +287,6 @@ const main = async () => {
287
287
  else config.mainFile = argv.mainFile
288
288
  }
289
289
 
290
- console.log(config)
291
-
292
290
  await installation(config)
293
291
  }
294
292
 
@@ -123,7 +123,7 @@ export {}
123
123
  },
124
124
  '@types/dto': {
125
125
  user: {
126
- content: `interface DtoUser {
126
+ content: `interface UserDTO {
127
127
  id: string
128
128
  lastName: string
129
129
  name: string
@@ -134,10 +134,11 @@ export {}
134
134
  },
135
135
  '@types/models': {
136
136
  user: {
137
- content: `interface IUser {
137
+ content: `interface UserDBO {
138
138
  id: string
139
139
  name: string
140
140
  lastName: string
141
+ createdAt: Date
141
142
  updatedAt: Date
142
143
  }
143
144
  `,
@@ -166,7 +167,7 @@ export * from './queries'
166
167
  user: {
167
168
  content: `import { model, Schema } from 'mongoose'
168
169
 
169
- const User = new Schema<IUser>(
170
+ const UserSchema = new Schema<UserDBO>(
170
171
  {
171
172
  lastName: {
172
173
  required: true,
@@ -178,24 +179,18 @@ const User = new Schema<IUser>(
178
179
  }
179
180
  },
180
181
  {
181
- timestamps: {
182
- createdAt: false,
183
- updatedAt: true
184
- },
182
+ timestamps: true,
185
183
  versionKey: false,
186
- toJSON: {
187
- transform(_, ret) {
184
+ toObject: {
185
+ transform: (_, ret) => {
188
186
  ret.id = ret._id.toString()
189
- ret.updatedAt = ret.updatedAt.toISOString()
190
187
  delete ret._id
191
- delete ret.__v
192
- },
193
- virtuals: true
188
+ }
194
189
  }
195
190
  }
196
191
  )
197
192
 
198
- const UserModel = model<IUser>('users', User)
193
+ const UserModel = model<UserDBO>('users', UserSchema)
199
194
 
200
195
  export { UserModel }
201
196
  `,
@@ -208,42 +203,63 @@ export { UserModel }
208
203
  file: `${projectName}/src/database/mongo/queries/index.ts`
209
204
  },
210
205
  user: {
211
- content: `import { UserModel } from '..'
206
+ content: `import { Document, Types } from 'mongoose'
207
+
208
+ import { UserModel } from '..'
209
+ import { UserDTO } from 'schemas'
210
+
211
+ const userDBOtoDTO = (
212
+ userDBO: Document<unknown, unknown, UserDBO> &
213
+ UserDBO & {
214
+ _id: Types.ObjectId
215
+ }
216
+ ): UserDTO => ({
217
+ ...userDBO.toObject(),
218
+ createdAt: userDBO.createdAt.toISOString(),
219
+ updatedAt: userDBO.updatedAt.toISOString()
220
+ })
212
221
 
213
- const store = async (userData: DtoUser): Promise<IUser> => {
222
+ const store = async (userData: UserDTO): Promise<UserDTO> => {
214
223
  const user = new UserModel(userData)
224
+
215
225
  await user.save()
216
226
 
217
- return user.toJSON()
227
+ return userDBOtoDTO(user)
218
228
  }
219
229
 
220
230
  const remove = async (
221
231
  id: string | null = null
222
- ): Promise<IUser | number | null> => {
223
- if (id) return await UserModel.findByIdAndRemove(id)
232
+ ): Promise<UserDTO | number | null> => {
233
+ if (id) {
234
+ const removedUser = await UserModel.findByIdAndRemove(id)
235
+
236
+ if (!removedUser) return null
237
+
238
+ return userDBOtoDTO(removedUser)
239
+ }
224
240
 
225
241
  return (await UserModel.deleteMany({})).deletedCount
226
242
  }
227
243
 
228
244
  const get = async (
229
245
  id: string | null = null
230
- ): Promise<IUser[] | IUser | null> => {
246
+ ): Promise<UserDTO[] | UserDTO | null> => {
231
247
  if (id) {
232
248
  const user = await UserModel.findById(id)
233
249
 
234
- return user ? user.toJSON() : null
250
+ return user ? userDBOtoDTO(user) : null
235
251
  }
236
252
 
237
253
  const users = await UserModel.find({})
238
254
 
239
- return users.map(u => u.toJSON())
255
+ return users.map(u => userDBOtoDTO(u))
240
256
  }
241
257
 
242
- const update = async (userData: DtoUser): Promise<IUser | null> => {
258
+ const update = async (userData: UserDTO): Promise<UserDTO | null> => {
243
259
  const { id, ...rest } = userData
244
260
  const user = await UserModel.findByIdAndUpdate(id, rest, { new: true })
245
261
 
246
- return user ? user.toJSON() : null
262
+ return user ? userDBOtoDTO(user) : null
247
263
  }
248
264
 
249
265
  export { store, remove, get, update }
@@ -259,6 +275,58 @@ export * from './server'
259
275
  file: `${projectName}/src/network/index.ts`
260
276
  }
261
277
  },
278
+ schemas: {
279
+ index: {
280
+ content: `import { Static, Type } from '@sinclair/typebox'
281
+
282
+ const id = Type.String({
283
+ pattern: '^[a-zA-Z0-9]{24,}$'
284
+ })
285
+
286
+ type Id = Static<typeof id>
287
+
288
+ const idSchema = Type.Object({ id })
289
+
290
+ type IdSchema = Static<typeof idSchema>
291
+
292
+ export { id, Id, idSchema, IdSchema }
293
+ export * from './user'
294
+ `,
295
+ file: `${projectName}/src/schemas/index.ts`
296
+ },
297
+ user: {
298
+ content: `import { Static, Type } from '@sinclair/typebox'
299
+
300
+ import { id } from '.'
301
+
302
+ const user = Type.Object({
303
+ lastName: Type.String(),
304
+ name: Type.String()
305
+ })
306
+
307
+ type User = Static<typeof user>
308
+
309
+ const userDto = Type.Object({
310
+ id: Type.Optional(id),
311
+ lastName: Type.String(),
312
+ name: Type.String(),
313
+ createdAt: Type.Optional(Type.String()),
314
+ updatedAt: Type.Optional(Type.String())
315
+ })
316
+
317
+ type UserDTO = Static<typeof userDto>
318
+
319
+ const storeUserSchema = Type.Object({
320
+ args: user
321
+ })
322
+
323
+ type StoreUser = Static<typeof storeUserSchema>
324
+
325
+ export { userDto, UserDTO, user, User, storeUserSchema, StoreUser }
326
+ `,
327
+ file: `${projectName}/src/schemas/user.ts`
328
+ }
329
+ },
262
330
  services: {
263
331
  index: {
264
332
  content: "export * from './user'\n",
@@ -268,41 +336,53 @@ export * from './server'
268
336
  content: `import httpErrors from 'http-errors'
269
337
 
270
338
  import { store, remove, get, update } from 'database'
339
+ import { UserDTO } from 'schemas'
271
340
  import { EFU, MFU, GE, errorHandling } from './utils'
272
341
 
273
342
  type Process = {
274
343
  type: 'store' | 'getAll' | 'deleteAll' | 'getOne' | 'update' | 'delete'
275
344
  }
276
345
 
346
+ type Arguments = {
347
+ id?: string
348
+ userDto?: UserDTO
349
+ userDtoWithoutId?: Omit<UserDTO, 'id'>
350
+ }
351
+
277
352
  class UserService {
278
- private _args: Partial<DtoUser> | null
353
+ #args: Arguments
279
354
 
280
- constructor(args: Partial<DtoUser> | null = null) {
281
- this._args = args
355
+ constructor(args: Arguments = {}) {
356
+ this.#args = args
282
357
  }
283
358
 
284
- public process({ type }: Process): Promise<string | IUser[] | IUser> {
359
+ public process({ type }: Process): Promise<string | UserDTO | UserDTO[]> {
285
360
  switch (type) {
286
361
  case 'store':
287
- return this._store()
362
+ return this.#store()
288
363
  case 'getAll':
289
- return this._getAll()
364
+ return this.#getAll()
290
365
  case 'deleteAll':
291
- return this._deleteAll()
366
+ return this.#deleteAll()
292
367
  case 'getOne':
293
- return this._getOne()
368
+ return this.#getOne()
294
369
  case 'update':
295
- return this._update()
370
+ return this.#update()
296
371
  case 'delete':
297
- return this._delete()
372
+ return this.#delete()
298
373
  default:
299
374
  throw new httpErrors.InternalServerError(GE.INTERNAL_SERVER_ERROR)
300
375
  }
301
376
  }
302
377
 
303
- private async _store(): Promise<IUser> {
378
+ async #store(): Promise<UserDTO> {
304
379
  try {
305
- const result = await store(this._args as DtoUser)
380
+ if (!this.#args.userDtoWithoutId)
381
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
382
+
383
+ const result = await store({
384
+ ...this.#args.userDtoWithoutId
385
+ })
306
386
 
307
387
  return result
308
388
  } catch (e) {
@@ -310,9 +390,9 @@ class UserService {
310
390
  }
311
391
  }
312
392
 
313
- private async _getAll(): Promise<IUser[]> {
393
+ async #getAll(): Promise<UserDTO[]> {
314
394
  try {
315
- const users = (await get()) as IUser[]
395
+ const users = (await get()) as UserDTO[]
316
396
 
317
397
  return users
318
398
  } catch (e) {
@@ -320,7 +400,7 @@ class UserService {
320
400
  }
321
401
  }
322
402
 
323
- private async _deleteAll(): Promise<string> {
403
+ async #deleteAll(): Promise<string> {
324
404
  try {
325
405
  const usersDeleted = (await remove()) as number
326
406
 
@@ -335,11 +415,13 @@ class UserService {
335
415
  }
336
416
  }
337
417
 
338
- private async _getOne(): Promise<IUser> {
339
- const { id } = this._args as DtoUser
340
-
418
+ async #getOne(): Promise<UserDTO> {
341
419
  try {
342
- const user = (await get(id as string)) as IUser | null
420
+ if (!this.#args.id)
421
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
422
+
423
+ const { id } = this.#args
424
+ const user = (await get(id)) as UserDTO | null
343
425
 
344
426
  if (!user) throw new httpErrors.NotFound(EFU.NOT_FOUND)
345
427
 
@@ -349,9 +431,12 @@ class UserService {
349
431
  }
350
432
  }
351
433
 
352
- private async _update(): Promise<IUser> {
434
+ async #update(): Promise<UserDTO> {
353
435
  try {
354
- const updatedUser = await update(this._args as DtoUser)
436
+ if (!this.#args.userDto || !this.#args.userDto.id)
437
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
438
+
439
+ const updatedUser = await update(this.#args.userDto)
355
440
 
356
441
  if (!updatedUser) throw new httpErrors.NotFound(EFU.NOT_FOUND)
357
442
 
@@ -361,10 +446,12 @@ class UserService {
361
446
  }
362
447
  }
363
448
 
364
- private async _delete(): Promise<string> {
365
- const { id } = this._args as DtoUser
366
-
449
+ async #delete(): Promise<string> {
367
450
  try {
451
+ if (!this.#args.id)
452
+ throw new httpErrors.UnprocessableEntity(GE.INTERNAL_SERVER_ERROR)
453
+
454
+ const { id } = this.#args
368
455
  const deletedUser = await remove(id)
369
456
 
370
457
  if (!deletedUser) throw new httpErrors.NotFound(EFU.NOT_FOUND)
@@ -484,6 +571,33 @@ Server.start()
484
571
  }
485
572
 
486
573
  const expressData = {
574
+ '@types/custom': {
575
+ request: {
576
+ content: `type ExpressRequest = import('express').Request
577
+
578
+ interface CustomRequest extends ExpressRequest {
579
+ body: {
580
+ args?: import('schemas').UserDTO
581
+ }
582
+ // We can add custom headers via intersection, remember that for some reason
583
+ // headers must be in Snake-Pascal-Case
584
+ headers: import('http').IncomingHttpHeaders & {
585
+ 'Custom-Header'?: string
586
+ }
587
+ }
588
+ `,
589
+ file: `${projectName}/src/@types/custom/request.d.ts`
590
+ },
591
+ response: {
592
+ content: `type ExpressResponse = import('express').Response
593
+
594
+ interface CustomResponse extends ExpressResponse {
595
+ newValue?: string
596
+ }
597
+ `,
598
+ file: `${projectName}/src/@types/custom/response.d.ts`
599
+ }
600
+ },
487
601
  network: {
488
602
  response: {
489
603
  content: `interface ResponseProps {
@@ -639,33 +753,6 @@ export { server as Server }
639
753
  file: `${projectName}/src/network/server.ts`
640
754
  }
641
755
  },
642
- '@types/custom': {
643
- request: {
644
- content: `type ExpressRequest = import('express').Request
645
-
646
- interface CustomRequest extends ExpressRequest {
647
- body: {
648
- args?: DtoUser
649
- }
650
- // We can add custom headers via intersection, remember that for some reason
651
- // headers must be in Snake-Pascal-Case
652
- headers: import('http').IncomingHttpHeaders & {
653
- 'Custom-Header'?: string
654
- }
655
- }
656
- `,
657
- file: `${projectName}/src/@types/custom/request.d.ts`
658
- },
659
- response: {
660
- content: `type ExpressResponse = import('express').Response
661
-
662
- interface CustomResponse extends ExpressResponse {
663
- newValue?: string
664
- }
665
- `,
666
- file: `${projectName}/src/@types/custom/response.d.ts`
667
- }
668
- },
669
756
  'network/routes': {
670
757
  home: {
671
758
  content: `import { Response, Request, Router } from 'express'
@@ -694,146 +781,82 @@ export * from './user'
694
781
  file: `${projectName}/src/network/routes/index.ts`
695
782
  },
696
783
  user: {
697
- content: `import { Router, NextFunction } from 'express'
698
- import httpErrors from 'http-errors'
699
- import { ValidationError } from 'joi'
784
+ content: `import { Router } from 'express'
700
785
 
701
786
  import { response } from 'network/response'
702
- import { UserService } from 'services/user'
703
- import { idSchema, userSchema, storeUserSchema } from './schemas'
787
+ import { UserService } from 'services'
788
+ import { idSchema, storeUserSchema, UserDTO } from 'schemas'
789
+ import { validatorCompiler } from './utils'
704
790
 
705
791
  const User = Router()
706
792
 
707
793
  User.route('/users')
708
794
  .post(
709
- async (
710
- req: CustomRequest,
711
- res: CustomResponse,
712
- next: NextFunction
713
- ): Promise<void> => {
795
+ validatorCompiler(storeUserSchema, 'body'),
796
+ async (req: CustomRequest, res: CustomResponse): Promise<void> => {
714
797
  const {
715
798
  body: { args }
716
799
  } = req
800
+ const us = new UserService({ userDtoWithoutId: args })
801
+ const result = await us.process({ type: 'store' })
717
802
 
718
- try {
719
- await storeUserSchema.validateAsync(args)
720
- const us = new UserService(args)
721
- const result = await us.process({ type: 'store' })
722
- response({ error: false, message: result, res, status: 201 })
723
- } catch (e) {
724
- if (e instanceof ValidationError)
725
- return next(new httpErrors.UnprocessableEntity(e.message))
726
-
727
- next(e)
728
- }
803
+ response({ error: false, message: result, res, status: 201 })
729
804
  }
730
805
  )
731
- .get(
732
- async (
733
- req: CustomRequest,
734
- res: CustomResponse,
735
- next: NextFunction
736
- ): Promise<void> => {
737
- const us = new UserService()
738
-
739
- try {
740
- const result = await us.process({ type: 'getAll' })
741
- response({ error: false, message: result, res, status: 200 })
742
- } catch (e) {
743
- next(e)
744
- }
745
- }
746
- )
747
- .delete(
748
- async (
749
- req: CustomRequest,
750
- res: CustomResponse,
751
- next: NextFunction
752
- ): Promise<void> => {
753
- const us = new UserService()
806
+ .get(async (req: CustomRequest, res: CustomResponse): Promise<void> => {
807
+ const us = new UserService()
808
+ const result = await us.process({ type: 'getAll' })
754
809
 
755
- try {
756
- const result = await us.process({ type: 'deleteAll' })
757
- response({ error: false, message: result, res, status: 200 })
758
- } catch (e) {
759
- next(e)
760
- }
761
- }
762
- )
810
+ response({ error: false, message: result, res, status: 200 })
811
+ })
812
+ .delete(async (req: CustomRequest, res: CustomResponse): Promise<void> => {
813
+ const us = new UserService()
814
+ const result = await us.process({ type: 'deleteAll' })
815
+
816
+ response({ error: false, message: result, res, status: 200 })
817
+ })
763
818
 
764
819
  User.route('/user/:id')
765
820
  .get(
766
- async (
767
- req: CustomRequest,
768
- res: CustomResponse,
769
- next: NextFunction
770
- ): Promise<void> => {
821
+ validatorCompiler(idSchema, 'params'),
822
+ async (req: CustomRequest, res: CustomResponse): Promise<void> => {
771
823
  const {
772
824
  params: { id }
773
825
  } = req
826
+ const us = new UserService({ id })
827
+ const result = await us.process({ type: 'getOne' })
774
828
 
775
- try {
776
- await idSchema.validateAsync(id)
777
- const us = new UserService({ id })
778
- const result = await us.process({ type: 'getOne' })
779
- response({ error: false, message: result, res, status: 200 })
780
- } catch (e) {
781
- if (e instanceof ValidationError)
782
- return next(new httpErrors.UnprocessableEntity(e.message))
783
-
784
- next(e)
785
- }
829
+ response({ error: false, message: result, res, status: 200 })
786
830
  }
787
831
  )
788
832
  .patch(
789
- async (
790
- req: CustomRequest,
791
- res: CustomResponse,
792
- next: NextFunction
793
- ): Promise<void> => {
833
+ validatorCompiler(idSchema, 'params'),
834
+ validatorCompiler(storeUserSchema, 'body'),
835
+ async (req: CustomRequest, res: CustomResponse): Promise<void> => {
794
836
  const {
795
837
  body: { args },
796
838
  params: { id }
797
839
  } = req
798
- const user = {
840
+ const userDto = {
799
841
  id,
800
842
  ...args
801
- }
843
+ } as UserDTO
844
+ const us = new UserService({ userDto })
845
+ const result = await us.process({ type: 'update' })
802
846
 
803
- try {
804
- await userSchema.validateAsync(user)
805
- const us = new UserService(user)
806
- const result = await us.process({ type: 'update' })
807
- response({ error: false, message: result, res, status: 200 })
808
- } catch (e) {
809
- if (e instanceof ValidationError)
810
- return next(new httpErrors.UnprocessableEntity(e.message))
811
-
812
- next(e)
813
- }
847
+ response({ error: false, message: result, res, status: 200 })
814
848
  }
815
849
  )
816
850
  .delete(
817
- async (
818
- req: CustomRequest,
819
- res: CustomResponse,
820
- next: NextFunction
821
- ): Promise<void> => {
851
+ validatorCompiler(idSchema, 'params'),
852
+ async (req: CustomRequest, res: CustomResponse): Promise<void> => {
822
853
  const {
823
854
  params: { id }
824
855
  } = req
856
+ const us = new UserService({ id })
857
+ const result = await us.process({ type: 'delete' })
825
858
 
826
- try {
827
- await idSchema.validateAsync(id)
828
- const us = new UserService({ id })
829
- const result = await us.process({ type: 'delete' })
830
- response({ error: false, message: result, res, status: 200 })
831
- } catch (e) {
832
- if (e instanceof ValidationError)
833
- return next(new httpErrors.UnprocessableEntity(e.message))
834
-
835
- next(e)
836
- }
859
+ response({ error: false, message: result, res, status: 200 })
837
860
  }
838
861
  )
839
862
 
@@ -842,34 +865,48 @@ export { User }
842
865
  file: `${projectName}/src/network/routes/user.ts`
843
866
  }
844
867
  },
845
- 'network/routes/schemas': {
868
+ 'network/routes/utils': {
846
869
  index: {
847
- content: `import Joi from 'joi'
848
-
849
- const idSchema = Joi.string().length(24).required()
850
-
851
- export { idSchema }
852
- export * from './user'
853
- `,
854
- file: `${projectName}/src/network/routes/schemas/index.ts`
855
- },
856
- user: {
857
- content: `import Joi from 'joi'
858
-
859
- const userSchema = Joi.object().keys({
860
- id: Joi.string().length(24).required(),
861
- lastName: Joi.string().required(),
862
- name: Joi.string().required()
870
+ content: `import { NextFunction } from 'express'
871
+ import httpErrors from 'http-errors'
872
+ import { TObject, TProperties } from '@sinclair/typebox'
873
+ import Ajv from 'ajv'
874
+
875
+ const ajv = new Ajv({
876
+ removeAdditional: true,
877
+ useDefaults: true,
878
+ coerceTypes: true,
879
+ nullable: true
863
880
  })
864
881
 
865
- const storeUserSchema = Joi.object().keys({
866
- lastName: Joi.string().required(),
867
- name: Joi.string().required()
868
- })
882
+ type Middleware = (
883
+ req: CustomRequest,
884
+ res: CustomResponse,
885
+ next: NextFunction
886
+ ) => void
887
+
888
+ const validatorCompiler = <T extends TProperties>(
889
+ schema: TObject<T>,
890
+ value: 'body' | 'params'
891
+ ): Middleware => {
892
+ return (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
893
+ const validate = ajv.compile(schema)
894
+ const ok = validate(req[value])
895
+
896
+ if (!ok && validate.errors) {
897
+ const [error] = validate.errors
898
+ const errorMessage = \`\${error.dataPath.replace('.', '')} \${error.message}\`
899
+
900
+ return next(new httpErrors.UnprocessableEntity(errorMessage))
901
+ }
902
+
903
+ next()
904
+ }
905
+ }
869
906
 
870
- export { userSchema, storeUserSchema }
907
+ export { validatorCompiler }
871
908
  `,
872
- file: `${projectName}/src/network/routes/schemas/user.ts`
909
+ file: `${projectName}/src/network/routes/utils/index.ts`
873
910
  }
874
911
  },
875
912
  utils: {
@@ -909,7 +946,7 @@ export { userSchema, storeUserSchema }
909
946
  "summary": "Save a user in the database",
910
947
  "operationId": "store",
911
948
  "requestBody": {
912
- "$ref": "#/components/requestBodies/DtoUser"
949
+ "$ref": "#/components/requestBodies/UserDTO"
913
950
  },
914
951
  "responses": {
915
952
  "201": {
@@ -1103,7 +1140,7 @@ export { userSchema, storeUserSchema }
1103
1140
  }
1104
1141
  ],
1105
1142
  "requestBody": {
1106
- "$ref": "#/components/requestBodies/DtoUser"
1143
+ "$ref": "#/components/requestBodies/UserDTO"
1107
1144
  },
1108
1145
  "responses": {
1109
1146
  "200": {
@@ -1264,7 +1301,7 @@ export { userSchema, storeUserSchema }
1264
1301
  }
1265
1302
  },
1266
1303
  "requestBodies": {
1267
- "DtoUser": {
1304
+ "UserDTO": {
1268
1305
  "description": "User name and last name",
1269
1306
  "content": {
1270
1307
  "application/json": {
@@ -1479,7 +1516,7 @@ const Docs = (app: FastifyInstance, prefix = '/api'): void => {
1479
1516
  }
1480
1517
 
1481
1518
  export { Docs }
1482
- `,
1519
+ `,
1483
1520
  file: `${projectName}/src/network/routes/docs.ts`
1484
1521
  },
1485
1522
  home: {
@@ -1510,30 +1547,37 @@ export * from './docs'
1510
1547
  },
1511
1548
  user: {
1512
1549
  content: `import { FastifyInstance } from 'fastify'
1550
+ import { Type } from '@sinclair/typebox'
1513
1551
 
1514
1552
  import { response } from 'network/response'
1515
- import { userSchema, idSchema, storeUserSchema } from './schemas'
1553
+ import {
1554
+ userDto,
1555
+ idSchema,
1556
+ IdSchema,
1557
+ storeUserSchema,
1558
+ StoreUser
1559
+ } from 'schemas'
1516
1560
  import { UserService } from 'services'
1561
+ import { validatorCompiler } from './utils'
1517
1562
 
1518
1563
  const User = (app: FastifyInstance, prefix = '/api'): void => {
1519
1564
  app
1520
- .post<{ Body: { args: Omit<DtoUser, 'id'> } }>(
1565
+ .post<{ Body: StoreUser }>(
1521
1566
  \`\${prefix}/users\`,
1522
1567
  {
1523
1568
  schema: {
1524
- body: {
1525
- args: storeUserSchema
1526
- },
1569
+ body: storeUserSchema,
1527
1570
  response: {
1528
1571
  200: {
1529
1572
  error: {
1530
1573
  type: 'boolean'
1531
1574
  },
1532
- message: userSchema
1575
+ message: userDto
1533
1576
  }
1534
1577
  },
1535
1578
  tags: ['user']
1536
- }
1579
+ },
1580
+ validatorCompiler
1537
1581
  },
1538
1582
  async (request, reply) => {
1539
1583
  const {
@@ -1541,14 +1585,16 @@ const User = (app: FastifyInstance, prefix = '/api'): void => {
1541
1585
  args: { lastName, name }
1542
1586
  }
1543
1587
  } = request
1544
- const us = new UserService({ lastName, name })
1588
+ const us = new UserService({
1589
+ userDtoWithoutId: { lastName, name }
1590
+ })
1545
1591
  const user = await us.process({ type: 'store' })
1546
1592
 
1547
1593
  response({
1548
1594
  error: false,
1549
1595
  message: user,
1550
1596
  reply,
1551
- status: 200
1597
+ status: 201
1552
1598
  })
1553
1599
  }
1554
1600
  )
@@ -1561,10 +1607,7 @@ const User = (app: FastifyInstance, prefix = '/api'): void => {
1561
1607
  error: {
1562
1608
  type: 'boolean'
1563
1609
  },
1564
- message: {
1565
- type: 'array',
1566
- items: userSchema
1567
- }
1610
+ message: Type.Array(userDto)
1568
1611
  }
1569
1612
  },
1570
1613
  tags: ['user']
@@ -1611,23 +1654,22 @@ const User = (app: FastifyInstance, prefix = '/api'): void => {
1611
1654
  })
1612
1655
  }
1613
1656
  )
1614
- .get<{ Params: { id: string } }>(
1657
+ .get<{ Params: IdSchema }>(
1615
1658
  \`\${prefix}/user/:id\`,
1616
1659
  {
1617
1660
  schema: {
1618
- params: {
1619
- id: idSchema
1620
- },
1661
+ params: idSchema,
1621
1662
  response: {
1622
1663
  200: {
1623
1664
  error: {
1624
1665
  type: 'boolean'
1625
1666
  },
1626
- message: userSchema
1667
+ message: userDto
1627
1668
  }
1628
1669
  },
1629
1670
  tags: ['user']
1630
- }
1671
+ },
1672
+ validatorCompiler
1631
1673
  },
1632
1674
  async (request, reply) => {
1633
1675
  const {
@@ -1644,22 +1686,18 @@ const User = (app: FastifyInstance, prefix = '/api'): void => {
1644
1686
  })
1645
1687
  }
1646
1688
  )
1647
- .patch<{ Body: { args: Omit<DtoUser, 'id'> }; Params: { id: string } }>(
1689
+ .patch<{ Body: StoreUser; Params: IdSchema }>(
1648
1690
  \`\${prefix}/user/:id\`,
1649
1691
  {
1650
1692
  schema: {
1651
- body: {
1652
- args: userSchema
1653
- },
1654
- params: {
1655
- id: idSchema
1656
- },
1693
+ body: storeUserSchema,
1694
+ params: idSchema,
1657
1695
  response: {
1658
1696
  200: {
1659
1697
  error: {
1660
1698
  type: 'boolean'
1661
1699
  },
1662
- message: userSchema
1700
+ message: userDto
1663
1701
  }
1664
1702
  },
1665
1703
  tags: ['user']
@@ -1672,7 +1710,9 @@ const User = (app: FastifyInstance, prefix = '/api'): void => {
1672
1710
  },
1673
1711
  params: { id }
1674
1712
  } = request
1675
- const us = new UserService({ name, lastName, id })
1713
+ const us = new UserService({
1714
+ userDto: { name, lastName, id }
1715
+ })
1676
1716
  const user = await us.process({ type: 'update' })
1677
1717
 
1678
1718
  response({
@@ -1683,13 +1723,11 @@ const User = (app: FastifyInstance, prefix = '/api'): void => {
1683
1723
  })
1684
1724
  }
1685
1725
  )
1686
- .delete<{ Params: { id: string } }>(
1726
+ .delete<{ Params: IdSchema }>(
1687
1727
  \`\${prefix}/user/:id\`,
1688
1728
  {
1689
1729
  schema: {
1690
- params: {
1691
- id: idSchema
1692
- },
1730
+ params: idSchema,
1693
1731
  response: {
1694
1732
  200: {
1695
1733
  error: {
@@ -1725,45 +1763,55 @@ export { User }
1725
1763
  file: `${projectName}/src/network/routes/user.ts`
1726
1764
  }
1727
1765
  },
1728
- 'network/routes/schemas': {
1766
+ 'network/routes/utils': {
1729
1767
  index: {
1730
- content: `import { Type } from '@sinclair/typebox'
1768
+ content: `/* eslint-disable @typescript-eslint/no-explicit-any */
1769
+ import {
1770
+ FastifyRouteSchemaDef,
1771
+ FastifyValidationResult
1772
+ } from 'fastify/types/schema'
1773
+ import httpErrors from 'http-errors'
1774
+ import Ajv from 'ajv'
1731
1775
 
1732
- const idSchema = Type.String({ minLength: 24, maxLength: 24 })
1776
+ const ajv = new Ajv({
1777
+ removeAdditional: true,
1778
+ useDefaults: true,
1779
+ coerceTypes: true,
1780
+ nullable: true
1781
+ })
1733
1782
 
1734
- export { idSchema }
1735
- export * from './user'
1736
- `,
1737
- file: `${projectName}/src/network/routes/schemas/index.ts`
1738
- },
1739
- user: {
1740
- content: `import { Type } from '@sinclair/typebox'
1783
+ const validatorCompiler = ({
1784
+ schema
1785
+ }: FastifyRouteSchemaDef<any>): FastifyValidationResult => {
1786
+ const validate = ajv.compile(schema)
1741
1787
 
1742
- const userSchema = Type.Object({
1743
- id: Type.Optional(Type.String({ minLength: 24, maxLength: 24 })),
1744
- lastName: Type.Optional(Type.String()),
1745
- name: Type.Optional(Type.String()),
1746
- updatedAt: Type.Optional(Type.String())
1747
- })
1788
+ return (data: unknown): boolean => {
1789
+ const ok = validate(data)
1748
1790
 
1749
- const storeUserSchema = Type.Object({
1750
- lastName: Type.String(),
1751
- name: Type.String()
1752
- })
1791
+ if (!ok && validate.errors) {
1792
+ const [error] = validate.errors
1793
+ const errorMessage = \`\${error.dataPath.replace('.', '')} \${error.message}\`
1753
1794
 
1754
- export { userSchema, storeUserSchema }
1795
+ throw new httpErrors.UnprocessableEntity(errorMessage)
1796
+ }
1797
+
1798
+ return true
1799
+ }
1800
+ }
1801
+
1802
+ export { validatorCompiler }
1755
1803
  `,
1756
- file: `${projectName}/src/network/routes/schemas/user.ts`
1804
+ file: `${projectName}/src/network/routes/utils/index.ts`
1757
1805
  }
1758
1806
  }
1759
1807
  }
1760
1808
 
1761
- const expressFolders = `${projectName}/src/utils`
1809
+ const expressFolders = `${projectName}/src/utils \
1810
+ ${projectName}/src/@types/custom`
1762
1811
 
1763
1812
  const createFoldersCommands = `mkdir ${projectName}/src \
1764
1813
  ${projectName}/src/@types \
1765
1814
  ${projectName}/src/@types/dto \
1766
- ${projectName}/src/@types/custom \
1767
1815
  ${projectName}/src/@types/models \
1768
1816
  ${projectName}/src/database \
1769
1817
  ${projectName}/src/database/mongo \
@@ -1771,7 +1819,8 @@ ${projectName}/src/database/mongo/models \
1771
1819
  ${projectName}/src/database/mongo/queries \
1772
1820
  ${projectName}/src/network \
1773
1821
  ${projectName}/src/network/routes \
1774
- ${projectName}/src/network/routes/schemas \
1822
+ ${projectName}/src/network/routes/utils \
1823
+ ${projectName}/src/schemas \
1775
1824
  ${projectName}/src/services \
1776
1825
  ${projectName}/src/services/utils \
1777
1826
  ${projectName}/src/services/utils/messages \
@@ -1794,10 +1843,6 @@ ${fastify ? '' : `${expressFolders}`}
1794
1843
  data['@types/models'].user.content
1795
1844
  )
1796
1845
 
1797
- // /services
1798
- await writeFile(data.services.user.file, data.services.user.content)
1799
- await writeFile(data.services.index.file, data.services.index.content)
1800
-
1801
1846
  // /database
1802
1847
  await writeFile(data.database.index.file, data.database.index.content)
1803
1848
  await writeFile(
@@ -1821,6 +1866,14 @@ ${fastify ? '' : `${expressFolders}`}
1821
1866
  data['database/mongo/queries'].user.content
1822
1867
  )
1823
1868
 
1869
+ // /schemas
1870
+ await writeFile(data.schemas.user.file, data.schemas.user.content)
1871
+ await writeFile(data.schemas.index.file, data.schemas.index.content)
1872
+
1873
+ // /services
1874
+ await writeFile(data.services.user.file, data.services.user.content)
1875
+ await writeFile(data.services.index.file, data.services.index.content)
1876
+
1824
1877
  // /services/utils
1825
1878
  await writeFile(
1826
1879
  data['services/utils'].index.file,
@@ -1882,16 +1935,22 @@ ${fastify ? '' : `${expressFolders}`}
1882
1935
  fastifyData['network/routes'].index.content
1883
1936
  )
1884
1937
 
1885
- // /network/routes/schemas
1938
+ // /network/routes/utils
1886
1939
  await writeFile(
1887
- fastifyData['network/routes/schemas'].index.file,
1888
- fastifyData['network/routes/schemas'].index.content
1940
+ fastifyData['network/routes/utils'].index.file,
1941
+ fastifyData['network/routes/utils'].index.content
1889
1942
  )
1943
+ } else {
1944
+ // /@types/custom
1890
1945
  await writeFile(
1891
- fastifyData['network/routes/schemas'].user.file,
1892
- fastifyData['network/routes/schemas'].user.content
1946
+ expressData['@types/custom'].request.file,
1947
+ expressData['@types/custom'].request.content
1893
1948
  )
1894
- } else {
1949
+ await writeFile(
1950
+ expressData['@types/custom'].response.file,
1951
+ expressData['@types/custom'].response.content
1952
+ )
1953
+
1895
1954
  // /network
1896
1955
  await writeFile(
1897
1956
  expressData.network.response.file,
@@ -1920,14 +1979,10 @@ ${fastify ? '' : `${expressFolders}`}
1920
1979
  expressData['network/routes'].index.content
1921
1980
  )
1922
1981
 
1923
- // /network/routes/schemas
1924
- await writeFile(
1925
- expressData['network/routes/schemas'].index.file,
1926
- expressData['network/routes/schemas'].index.content
1927
- )
1982
+ // /network/routes/utils
1928
1983
  await writeFile(
1929
- expressData['network/routes/schemas'].user.file,
1930
- expressData['network/routes/schemas'].user.content
1984
+ expressData['network/routes/utils'].index.file,
1985
+ expressData['network/routes/utils'].index.content
1931
1986
  )
1932
1987
 
1933
1988
  // /utils
@@ -1936,15 +1991,5 @@ ${fastify ? '' : `${expressFolders}`}
1936
1991
  expressData.utils.index.file,
1937
1992
  expressData.utils.index.content
1938
1993
  )
1939
-
1940
- // /@types/custom
1941
- await writeFile(
1942
- expressData['@types/custom'].request.file,
1943
- expressData['@types/custom'].request.content
1944
- )
1945
- await writeFile(
1946
- expressData['@types/custom'].response.file,
1947
- expressData['@types/custom'].response.content
1948
- )
1949
1994
  }
1950
1995
  }
@@ -17,7 +17,7 @@ module.exports = async projectName => {
17
17
 
18
18
  /* Basic Options */
19
19
  // "incremental": true, /* Enable incremental compilation */
20
- "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
20
+ "target": "ES2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
21
21
  "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
22
22
  // "lib": [], /* Specify library files to be included in the compilation. */
23
23
  "allowJs": true /* Allow javascript files to be compiled. */,
package/lib/src/index.js CHANGED
@@ -57,9 +57,9 @@ module.exports = async ({
57
57
  cliProgress.Presets.shades_classic
58
58
  )
59
59
 
60
- const expressProdPackages = 'express joi morgan swagger-ui-express'
61
- const fastifyProdPackages = '@sinclair/typebox fastify fastify-swagger'
62
- const prodPackages = `${manager} http-errors mongoose ${
60
+ const expressProdPackages = 'express morgan swagger-ui-express'
61
+ const fastifyProdPackages = 'fastify fastify-swagger'
62
+ const prodPackages = `${manager} http-errors mongoose @sinclair/typebox ajv@^6 ${
63
63
  fastify ? fastifyProdPackages : expressProdPackages
64
64
  }`
65
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthonylzq/simba.js",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "description": "set up a modern backend app by running one command",
5
5
  "main": "lib/index.js",
6
6
  "directories": {
@@ -44,9 +44,9 @@
44
44
  "yargs": "^17.3.1"
45
45
  },
46
46
  "devDependencies": {
47
- "dotenv": "^14.3.2",
48
- "eslint": "^8.7.0",
49
- "eslint-config-prettier": "^8.3.0",
47
+ "dotenv": "^16.0.0",
48
+ "eslint": "^8.10.0",
49
+ "eslint-config-prettier": "^8.5.0",
50
50
  "eslint-config-standard": "^16.0.3",
51
51
  "eslint-plugin-import": "^2.25.4",
52
52
  "eslint-plugin-node": "^11.1.0",