@maioradv/nestjs-core 1.0.1

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.
Files changed (39) hide show
  1. package/README.md +1 -0
  2. package/index.ts +1 -0
  3. package/lib/decorators/api-response-with-rels.decorator.ts +75 -0
  4. package/lib/decorators/index.ts +1 -0
  5. package/lib/dto/clauses/index.ts +2 -0
  6. package/lib/dto/clauses/model.ts +15 -0
  7. package/lib/dto/clauses/standard-clauses.decorator.ts +80 -0
  8. package/lib/dto/index.ts +3 -0
  9. package/lib/dto/pagination/cursor-meta.dto.ts +38 -0
  10. package/lib/dto/pagination/cursor-paginated.dto.ts +68 -0
  11. package/lib/dto/pagination/cursor-query.dto.ts +64 -0
  12. package/lib/dto/pagination/index.ts +16 -0
  13. package/lib/dto/pagination/model.ts +7 -0
  14. package/lib/dto/pagination/paginated-meta.dto.ts +37 -0
  15. package/lib/dto/pagination/paginated-query.dto.ts +40 -0
  16. package/lib/dto/pagination/paginated-response.decorator.ts +49 -0
  17. package/lib/dto/pagination/paginated.dto.ts +15 -0
  18. package/lib/dto/pagination/pagination-params.decorator.ts +21 -0
  19. package/lib/dto/sorting/enums.ts +4 -0
  20. package/lib/dto/sorting/index.ts +3 -0
  21. package/lib/dto/sorting/models.ts +47 -0
  22. package/lib/dto/sorting/sorting-params.decorator.ts +35 -0
  23. package/lib/errors/index.ts +1 -0
  24. package/lib/errors/validation.error.ts +9 -0
  25. package/lib/filters/all-exception.filter.ts +33 -0
  26. package/lib/filters/http-exception.filter.ts +32 -0
  27. package/lib/filters/index.ts +3 -0
  28. package/lib/filters/validation-exception.filter.ts +21 -0
  29. package/lib/index.ts +6 -0
  30. package/lib/logger/index.ts +1 -0
  31. package/lib/logger/logger-factory.ts +42 -0
  32. package/lib/utils/chunk.helper.ts +4 -0
  33. package/lib/utils/index.ts +5 -0
  34. package/lib/utils/path.helper.ts +5 -0
  35. package/lib/utils/slug.helper.ts +22 -0
  36. package/lib/utils/storage.helper.ts +39 -0
  37. package/lib/utils/types.helper.ts +3 -0
  38. package/package.json +33 -0
  39. package/tsconfig.json +21 -0
package/README.md ADDED
@@ -0,0 +1 @@
1
+
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './dist';
@@ -0,0 +1,75 @@
1
+ import { Type, applyDecorators } from "@nestjs/common";
2
+ import { ApiExtraModels, ApiResponse, ApiResponseMetadata, getSchemaPath } from "@nestjs/swagger";
3
+ import { WithRequired } from "../utils/types.helper";
4
+ import { ReferenceObject, SchemaObject } from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
5
+
6
+ export type TModel = Type<any> | [string,Type<any>]
7
+ export type RelationDefs = {
8
+ oneToOne?: TModel[],
9
+ oneToMany?: TModel[],
10
+ manyToMany?: [TModel,TModel|TModel[]][],
11
+ }
12
+
13
+ export type ApiReponseWithRelsOptions = WithRequired<ApiResponseMetadata,'type'>
14
+
15
+ export const ApiResponseWithRels = (
16
+ options: ApiReponseWithRelsOptions,
17
+ relations: RelationDefs
18
+ ) => {
19
+ const {type, ...rest} = options
20
+ const Model = type as Type<any>
21
+ const {properties,extraModels} = handleRelationsForSwagger(relations)
22
+ return applyDecorators(
23
+ ApiExtraModels(...extraModels),
24
+ ApiResponse({
25
+ ...rest,
26
+ schema:{
27
+ title: `${Model.name}`,
28
+ allOf: [
29
+ { $ref: getSchemaPath(Model) },
30
+ {
31
+ properties:properties
32
+ }
33
+ ],
34
+ }
35
+ }),
36
+ );
37
+ };
38
+
39
+ export function handleRelationsForSwagger(relations: RelationDefs): {
40
+ properties:Record<string,SchemaObject | ReferenceObject>,
41
+ extraModels:Type<any>[]
42
+ } {
43
+ let properties:Record<string,SchemaObject | ReferenceObject> = {}
44
+ let extraModels:Type<any>[] = []
45
+ relations.oneToMany?.forEach((T) => {
46
+ const [name,schema] = Array.isArray(T) ? T : [T.name,T]
47
+ properties[`${name}`] = {
48
+ type:'array',
49
+ items: { $ref: getSchemaPath(schema) }
50
+ }
51
+ extraModels.push(schema)
52
+ })
53
+ relations.oneToOne?.forEach((T) => {
54
+ const [name,schema] = Array.isArray(T) ? T : [T.name,T]
55
+ properties[`${name}`] = { $ref: getSchemaPath(schema) }
56
+ extraModels.push(schema)
57
+ })
58
+ relations.manyToMany?.forEach((TCouple) => {
59
+ const [T,S] = TCouple
60
+ const [name,schema] = Array.isArray(T) ? T : [T.name,T]
61
+ const models = Array.isArray(S) ? S : [S]
62
+ let rels:Record<string,SchemaObject | ReferenceObject> = {}
63
+ models.forEach(M => {
64
+ const [name,schema] = Array.isArray(M) ? M : [M.name,M]
65
+ rels[`${name}`] = { $ref: getSchemaPath(schema) }
66
+ extraModels.push(schema)
67
+ })
68
+ properties[`${name}`] = {
69
+ type:'array',
70
+ items: { $ref: getSchemaPath(schema), properties:rels }
71
+ }
72
+ extraModels.push(schema)
73
+ })
74
+ return {properties,extraModels}
75
+ }
@@ -0,0 +1 @@
1
+ export * from './api-response-with-rels.decorator'
@@ -0,0 +1,2 @@
1
+ export * from './standard-clauses.decorator'
2
+ export * from './model'
@@ -0,0 +1,15 @@
1
+ //export type WhereClause = number | number[] | string | string[] | Date | boolean;
2
+
3
+ export interface DefaultClausesI {
4
+ id?:number[];
5
+ createdAt?:Date;
6
+ updatedAt?:Date;
7
+ minCreatedAt?:Date;
8
+ maxCreatedAt?:Date;
9
+ minUpdatedAt?:Date;
10
+ maxUpdatedAt?:Date;
11
+ }
12
+
13
+ export type WhereClausesOf<T, P extends keyof T = Exclude<keyof T,keyof DefaultClausesI | 'metafields' | 'translations'>> = {
14
+ [K in P]?: T[K] | T[K][]
15
+ }
@@ -0,0 +1,80 @@
1
+ import { applyDecorators, Type as TypeI } from "@nestjs/common";
2
+ import { ApiPropertyOptional } from "@nestjs/swagger";
3
+ import { Transform, Type } from "class-transformer";
4
+ import { IsArray, IsBoolean, IsDate, IsEnum, IsNumber, IsOptional, IsString } from "class-validator";
5
+
6
+ export const IsStringClause = (
7
+
8
+ ) => {
9
+ return applyDecorators(
10
+ ApiPropertyOptional({
11
+ type:String,
12
+ description:'Check if field contains the string'
13
+ }),
14
+ IsOptional(),
15
+ IsString()
16
+ );
17
+ };
18
+
19
+ export const IsEnumClause = (
20
+ model: object,
21
+ ) => {
22
+ return applyDecorators(
23
+ ApiPropertyOptional({
24
+ enum:model,
25
+ description:'A string or a comma-separated list of strings'
26
+ }),
27
+ Transform(({value}:{value:string}) => value.split(',')),
28
+ IsOptional(),
29
+ IsEnum(model,{each:true}),
30
+ IsArray()
31
+ );
32
+ };
33
+
34
+ export const IsNumberClause = (
35
+
36
+ ) => {
37
+ return applyDecorators(
38
+ ApiPropertyOptional({
39
+ type:Number,
40
+ description:'A number or a comma-separated list of numbers'
41
+ }),
42
+ Transform(({value}:{value:string}) => value.split(',').map(str => +str)),
43
+ IsOptional(),
44
+ IsNumber({allowNaN:false},{each: true}),
45
+ IsArray()
46
+ );
47
+ };
48
+
49
+ export const IsBooleanClause = (
50
+
51
+ ) => {
52
+ return applyDecorators(
53
+ ApiPropertyOptional({
54
+ type:Boolean,
55
+ }),
56
+ Transform(({value}:{value:string}) => {
57
+ return value === 'true' ? true :
58
+ value === 'false' ? false :
59
+ value === '1' ? true :
60
+ value === '0' ? false :
61
+ value ? true : false
62
+ }),
63
+ IsOptional(),
64
+ IsBoolean()
65
+ );
66
+ };
67
+
68
+ export const IsDateStringClause = (
69
+
70
+ ) => {
71
+ return applyDecorators(
72
+ ApiPropertyOptional({
73
+ type:Date,
74
+ description:'A date string in ISO8601 format',
75
+ }),
76
+ Type(() => Date),
77
+ IsOptional(),
78
+ IsDate()
79
+ );
80
+ };
@@ -0,0 +1,3 @@
1
+ export * from './clauses'
2
+ export * from './sorting'
3
+ export * from './pagination'
@@ -0,0 +1,38 @@
1
+ import { ApiProperty } from "@nestjs/swagger";
2
+ import { Field, ObjectType, Int } from '@nestjs/graphql'
3
+
4
+ @ObjectType()
5
+ export default class CursorMetaDto {
6
+ @ApiProperty()
7
+ @Field(() => Int,{ nullable: true })
8
+ readonly startCursor: number;
9
+
10
+ @ApiProperty()
11
+ @Field(() => Int,{ nullable: true })
12
+ readonly endCursor: number;
13
+
14
+ @ApiProperty()
15
+ @Field()
16
+ readonly hasPreviousPage: boolean;
17
+
18
+ @ApiProperty()
19
+ @Field()
20
+ readonly hasNextPage: boolean;
21
+
22
+ constructor({
23
+ first,
24
+ last,
25
+ start,
26
+ end
27
+ }:{
28
+ first: number|undefined,
29
+ last: number|undefined,
30
+ start: number|undefined,
31
+ end: number|undefined,
32
+ }) {
33
+ if(start) this.startCursor = start;
34
+ if(end) this.endCursor = end;
35
+ this.hasPreviousPage = start ? (start !== first) : false
36
+ this.hasNextPage = end ? (end !== last) : false
37
+ }
38
+ }
@@ -0,0 +1,68 @@
1
+ import { Field, ObjectType } from '@nestjs/graphql';
2
+ import { Type } from '@nestjs/common';
3
+ import CursorMetaDto from './cursor-meta.dto'
4
+
5
+ interface IEdgeType<T> {
6
+ cursor: string;
7
+ node: T;
8
+ }
9
+
10
+ export interface IPaginatedType<T> {
11
+ edges: IEdgeType<T>[];
12
+ nodes: T[];
13
+ meta: CursorMetaDto;
14
+ }
15
+
16
+ export function PaginatedGQL<T>(classRef: Type<T>): Type<IPaginatedType<T>> {
17
+ @ObjectType(`${classRef.name}Edge`)
18
+ abstract class EdgeType {
19
+ @Field(() => String)
20
+ cursor: string;
21
+
22
+ @Field(() => classRef)
23
+ node: T;
24
+ }
25
+
26
+ @ObjectType({ isAbstract: true })
27
+ abstract class PaginatedType implements IPaginatedType<T> {
28
+ @Field(() => [EdgeType], { nullable: true })
29
+ edges: EdgeType[];
30
+
31
+ @Field(() => [classRef], { nullable: true })
32
+ nodes: T[];
33
+
34
+ @Field(() => CursorMetaDto, { nullable: true })
35
+ meta: CursorMetaDto;
36
+ }
37
+ return PaginatedType as Type<IPaginatedType<T>>;
38
+ }
39
+
40
+ export type CursorMeta = {
41
+ first:number|undefined
42
+ last:number|undefined
43
+ }
44
+
45
+ export class PaginatedGQLDto<T> implements IPaginatedType<T>{
46
+ readonly edges: IEdgeType<T>[];
47
+ readonly nodes: T[];
48
+ readonly meta: CursorMetaDto;
49
+
50
+ constructor(data: T[], meta: CursorMeta) {
51
+ this.edges = this.getEdges(data)
52
+ this.nodes = data
53
+ this.meta = new CursorMetaDto({
54
+ ...meta,
55
+ start: +this.edges[0]?.cursor,
56
+ end: +this.edges[this.edges.length-1]?.cursor
57
+ })
58
+ }
59
+
60
+ private getEdges(data: T[]) {
61
+ return data.map((value) => {
62
+ return {
63
+ node: value,
64
+ cursor: (value as any).id,
65
+ };
66
+ });
67
+ }
68
+ }
@@ -0,0 +1,64 @@
1
+ import { ApiProperty } from "@nestjs/swagger";
2
+ import { Type } from "class-transformer";
3
+ import { IsInt, IsNumber, IsOptional, Max, Min } from "class-validator";
4
+ import { Int, Field, ArgsType } from '@nestjs/graphql';
5
+
6
+ @ArgsType()
7
+ export default class CursorQueryDto {
8
+ @ApiProperty({
9
+ required:false,
10
+ })
11
+ @Field(() => Int,{nullable:true})
12
+ @IsNumber()
13
+ @IsOptional()
14
+ @IsInt()
15
+ @Type(() => Number)
16
+ readonly after?:number;
17
+
18
+ @ApiProperty({
19
+ required:false,
20
+ })
21
+ @Field(() => Int,{nullable:true})
22
+ @IsNumber()
23
+ @IsOptional()
24
+ @IsInt()
25
+ @Type(() => Number)
26
+ readonly before?:number;
27
+
28
+ @ApiProperty({
29
+ required:false,
30
+ minimum: 1,
31
+ maximum: 250,
32
+ default: 50,
33
+ })
34
+ @Field(() => Int,{nullable:true})
35
+ @IsNumber()
36
+ @IsOptional()
37
+ @IsInt()
38
+ @Min(1)
39
+ @Max(250)
40
+ @Type(() => Number)
41
+ readonly limit?:number = 50;
42
+
43
+ get skip(): number {
44
+ if(this.after || this.before) return 1;
45
+ return 0;
46
+ }
47
+
48
+ get take(): number {
49
+ if(this.before) return -this.limit;
50
+ return this.limit;
51
+ }
52
+
53
+ get order():Record<string,any> {
54
+ return {
55
+ id: 'asc'
56
+ }
57
+ }
58
+
59
+ get cursor() {
60
+ return this.after || this.before ? {
61
+ id: this.after ?? this.before
62
+ } : undefined
63
+ }
64
+ }
@@ -0,0 +1,16 @@
1
+ import ApiPaginatedResponse from "./paginated-response.decorator";
2
+ import PaginatedMetaDto from "./paginated-meta.dto";
3
+ import PaginatedQueryDto from "./paginated-query.dto";
4
+ import PaginatedDto from "./paginated.dto";
5
+ import CursorQueryDto from "./cursor-query.dto";
6
+ export * from './cursor-paginated.dto'
7
+
8
+ export * from './model'
9
+
10
+ export {
11
+ ApiPaginatedResponse,
12
+ PaginatedMetaDto,
13
+ PaginatedQueryDto,
14
+ CursorQueryDto,
15
+ PaginatedDto
16
+ }
@@ -0,0 +1,7 @@
1
+ export interface PaginatedQueryI {
2
+ readonly page?:number;
3
+ readonly limit?:number;
4
+
5
+ get skip(): number;
6
+ get take(): number;
7
+ }
@@ -0,0 +1,37 @@
1
+ import { ApiProperty } from "@nestjs/swagger";
2
+ import PaginatedQueryDto from "./paginated-query.dto";
3
+
4
+ export default class PaginatedMetaDto {
5
+ @ApiProperty()
6
+ readonly page: number;
7
+
8
+ @ApiProperty()
9
+ readonly take: number;
10
+
11
+ @ApiProperty()
12
+ readonly itemCount: number;
13
+
14
+ @ApiProperty()
15
+ readonly pageCount: number;
16
+
17
+ @ApiProperty()
18
+ readonly hasPreviousPage: boolean;
19
+
20
+ @ApiProperty()
21
+ readonly hasNextPage: boolean;
22
+
23
+ constructor({
24
+ paginatedQueryDto,
25
+ itemCount
26
+ }:{
27
+ paginatedQueryDto: PaginatedQueryDto,
28
+ itemCount: number
29
+ }) {
30
+ this.page = paginatedQueryDto.page;
31
+ this.take = paginatedQueryDto.take;
32
+ this.itemCount = itemCount;
33
+ this.pageCount = Math.ceil(this.itemCount / this.take);
34
+ this.hasPreviousPage = this.page > 1;
35
+ this.hasNextPage = this.page < this.pageCount;
36
+ }
37
+ }
@@ -0,0 +1,40 @@
1
+ import { ApiProperty } from "@nestjs/swagger";
2
+ import { Type } from "class-transformer";
3
+ import { IsInt, IsNumber, IsOptional, Max, Min } from "class-validator";
4
+ import { PaginatedQueryI } from "./model";
5
+
6
+ export default class PaginatedQueryDto implements PaginatedQueryI {
7
+ @ApiProperty({
8
+ required:false,
9
+ minimum: 1,
10
+ default: 1,
11
+ })
12
+ @IsNumber()
13
+ @IsOptional()
14
+ @IsInt()
15
+ @Min(1)
16
+ @Type(() => Number)
17
+ readonly page?:number = 1;
18
+
19
+ @ApiProperty({
20
+ required:false,
21
+ minimum: 1,
22
+ maximum: 250,
23
+ default: 50,
24
+ })
25
+ @IsNumber()
26
+ @IsOptional()
27
+ @IsInt()
28
+ @Min(1)
29
+ @Max(250)
30
+ @Type(() => Number)
31
+ readonly limit?:number = 50;
32
+
33
+ get skip(): number {
34
+ return (this.page - 1) * this.limit;
35
+ }
36
+
37
+ get take(): number {
38
+ return this.limit;
39
+ }
40
+ }
@@ -0,0 +1,49 @@
1
+ import { HttpStatus, Type, applyDecorators } from "@nestjs/common";
2
+ import { ApiExtraModels, ApiOkResponse, ApiResponseOptions, getSchemaPath } from "@nestjs/swagger";
3
+ import PaginatedMetaDto from "./paginated-meta.dto";
4
+ import PaginatedDto from "./paginated.dto";
5
+ import { RelationDefs, handleRelationsForSwagger } from "@/decorators";
6
+
7
+ const ApiPaginatedResponse = <TModel extends Type<any>>(
8
+ model: TModel,
9
+ relations?:RelationDefs,
10
+ options?: ApiResponseOptions,
11
+ ) => {
12
+ const {properties,extraModels} = relations ? handleRelationsForSwagger(relations) : {properties:undefined,extraModels:[]}
13
+ return applyDecorators(
14
+ ApiExtraModels(PaginatedMetaDto, PaginatedDto, model, ...extraModels),
15
+ ApiOkResponse({
16
+ status:HttpStatus.OK,
17
+ description:`${model.name}s`,
18
+ schema: {
19
+ title: `Paginated${model.name}Dto`,
20
+ allOf: [
21
+ { $ref: getSchemaPath(PaginatedDto) },
22
+ {
23
+ properties: {
24
+ data: {
25
+
26
+ type: 'array',
27
+ items: {
28
+ title:`${model.name}s`,
29
+ allOf:[
30
+ { $ref: getSchemaPath(model) },
31
+ {
32
+ properties:properties
33
+ }
34
+ ]
35
+ },
36
+ },
37
+ meta: {
38
+ $ref: getSchemaPath(PaginatedMetaDto)
39
+ },
40
+ },
41
+ },
42
+ ],
43
+ },
44
+ ...options
45
+ }),
46
+ );
47
+ };
48
+
49
+ export default ApiPaginatedResponse
@@ -0,0 +1,15 @@
1
+ import { ApiProperty } from "@nestjs/swagger";
2
+ import PaginatedMetaDto from "./paginated-meta.dto";
3
+
4
+ export default class PaginatedDto<T> {
5
+ @ApiProperty()
6
+ readonly data: T[];
7
+
8
+ @ApiProperty()
9
+ readonly meta: PaginatedMetaDto;
10
+
11
+ constructor(data: T[], meta: PaginatedMetaDto) {
12
+ this.data = data
13
+ this.meta = meta
14
+ }
15
+ }
@@ -0,0 +1,21 @@
1
+ import { Type, applyDecorators } from "@nestjs/common";
2
+ import { ApiExtraModels, ApiQuery, getSchemaPath } from "@nestjs/swagger";
3
+ import PaginatedQueryDto from "./paginated-query.dto";
4
+
5
+ const ApiPaginationParams = <TModel extends Type<any>>() => {
6
+ return applyDecorators(
7
+ ApiExtraModels(PaginatedQueryDto),
8
+ ApiQuery({
9
+ required: false,
10
+ name: 'pagination',
11
+ style: 'deepObject',
12
+ explode: true,
13
+ type: 'object',
14
+ schema: {
15
+ $ref: getSchemaPath(PaginatedQueryDto),
16
+ },
17
+ })
18
+ );
19
+ };
20
+
21
+ export default ApiPaginationParams
@@ -0,0 +1,4 @@
1
+ export enum Sorting {
2
+ asc = 'asc',
3
+ desc = 'desc'
4
+ }
@@ -0,0 +1,3 @@
1
+ export * from './enums'
2
+ export * from './sorting-params.decorator'
3
+ export * from './models'
@@ -0,0 +1,47 @@
1
+ import { IsEnum, IsOptional } from 'class-validator';
2
+ import { Sorting } from '@/dto/sorting';
3
+ import { ApiPropertyOptional } from '@nestjs/swagger';
4
+
5
+ export class SortingDto implements DefaultSortingI {
6
+ @ApiPropertyOptional({enum: Sorting})
7
+ @IsEnum(Sorting)
8
+ @IsOptional()
9
+ id?: Sorting;
10
+
11
+ @ApiPropertyOptional({enum: Sorting})
12
+ @IsEnum(Sorting)
13
+ @IsOptional()
14
+ createdAt?: Sorting;
15
+
16
+ @ApiPropertyOptional({enum: Sorting})
17
+ @IsEnum(Sorting)
18
+ @IsOptional()
19
+ updatedAt?: Sorting;
20
+
21
+ get orderBy() {
22
+ return Object.entries(this).map(entry => {
23
+ return {[entry[0]]: entry[1]};
24
+ });
25
+ }
26
+
27
+ get orderByRDB() {
28
+ return Object.entries(this).map(entry => {
29
+ return {column:entry[0], order:entry[1]};
30
+ });
31
+ }
32
+ }
33
+
34
+ export interface DefaultSortingI {
35
+ id?: Sorting;
36
+ createdAt?: Sorting;
37
+ updatedAt?: Sorting;
38
+ get orderBy();
39
+ }
40
+
41
+ export interface SortingQueryI<T extends SortingDto> {
42
+ sorting?:T
43
+ }
44
+
45
+ export type SortingFieldsOf<T, P extends keyof T = Exclude<keyof T,keyof DefaultSortingI | 'metafields' | 'translations'>> = {
46
+ [K in P]?: Sorting
47
+ }
@@ -0,0 +1,35 @@
1
+ import { Type as TypeI, applyDecorators } from "@nestjs/common";
2
+ import { ApiExtraModels, ApiPropertyOptional, ApiQuery, getSchemaPath } from "@nestjs/swagger";
3
+ import { Transform, Type, plainToClass } from "class-transformer";
4
+ import { IsOptional, ValidateNested } from "class-validator";
5
+
6
+ export const ApiSortingParams = <TModel extends TypeI<any>>(
7
+ model: TModel,
8
+ ) => {
9
+ return applyDecorators(
10
+ ApiExtraModels(model),
11
+ ApiQuery({
12
+ required: false,
13
+ name: 'sorting',
14
+ style: 'deepObject',
15
+ explode: true,
16
+ type: 'object',
17
+ example:'',
18
+ schema: {
19
+ $ref: getSchemaPath(model),
20
+ },
21
+ })
22
+ );
23
+ };
24
+
25
+ export const IsSortingObject = <TModel extends TypeI<any>>(
26
+ model: TModel,
27
+ ) => {
28
+ return applyDecorators(
29
+ ApiPropertyOptional({type:model}),
30
+ ValidateNested(),
31
+ Transform(({value}) => plainToClass(model, JSON.parse(value))),
32
+ Type(() => model),
33
+ IsOptional()
34
+ );
35
+ };
@@ -0,0 +1 @@
1
+ export * from './validation.error'
@@ -0,0 +1,9 @@
1
+ import { BadRequestException } from "@nestjs/common";
2
+
3
+ export const VALIDATION_EXCEPTION_CODE = 'VALIDATION_ERROR'
4
+
5
+ export class ValidationException extends BadRequestException {
6
+ constructor(objectOrError?: any) {
7
+ super(objectOrError,VALIDATION_EXCEPTION_CODE);
8
+ }
9
+ }
@@ -0,0 +1,33 @@
1
+ import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
2
+ import { GqlContextType } from '@nestjs/graphql';
3
+ import { Request, Response } from 'express';
4
+
5
+ @Catch()
6
+ export class AllExceptionsFilter implements ExceptionFilter {
7
+ catch(exception: any, host: ArgumentsHost): void {
8
+ if(host.getType<GqlContextType>() === 'graphql') {
9
+ return exception;
10
+ }
11
+ const ctx = host.switchToHttp();
12
+ const response = ctx.getResponse<Response>();
13
+ const request = ctx.getRequest<Request>();
14
+ const status = HttpStatus.INTERNAL_SERVER_ERROR;
15
+
16
+ const data = process.env.NODE_ENV == 'development' ? {
17
+ statusCode: status,
18
+ method: request.method,
19
+ path: request.url,
20
+ type: exception.name,
21
+ code: exception.code,
22
+ message: exception.message ?? 'Internal Server Error',
23
+ stack: exception.stack?.split('\n')
24
+ } : {
25
+ statusCode: status,
26
+ message: 'Internal Server Error',
27
+ }
28
+
29
+ response
30
+ .status(status)
31
+ .send(data);
32
+ }
33
+ }
@@ -0,0 +1,32 @@
1
+ import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
2
+ import { GqlContextType } from '@nestjs/graphql';
3
+ import { Request, Response } from 'express';
4
+
5
+ @Catch(HttpException)
6
+ export class HttpExceptionFilter implements ExceptionFilter {
7
+ catch(exception: HttpException, host: ArgumentsHost) {
8
+ if(host.getType<GqlContextType>() === 'graphql') {
9
+ return exception;
10
+ }
11
+ const ctx = host.switchToHttp();
12
+ const response = ctx.getResponse<Response>();
13
+ const request = ctx.getRequest<Request>();
14
+ const status = exception.getStatus();
15
+
16
+ const data = process.env.NODE_ENV == 'development' ? {
17
+ statusCode: status,
18
+ method: request.method,
19
+ path: request.url,
20
+ type: exception.name,
21
+ message: exception.message,
22
+ stack: exception.stack?.split('\n')
23
+ } : {
24
+ statusCode: status,
25
+ message: exception.message,
26
+ }
27
+
28
+ response
29
+ .status(status)
30
+ .send(data);
31
+ }
32
+ }
@@ -0,0 +1,3 @@
1
+ export * from './all-exception.filter'
2
+ export * from './http-exception.filter'
3
+ export * from './validation-exception.filter'
@@ -0,0 +1,21 @@
1
+ import { ExceptionFilter, Catch, ArgumentsHost, HttpException, BadRequestException } from '@nestjs/common';
2
+ import { GqlContextType } from '@nestjs/graphql';
3
+ import { Request, Response } from 'express';
4
+ import { ValidationException } from '../errors/validation.error';
5
+
6
+ @Catch(BadRequestException)
7
+ export class ValidationExceptionFilter implements ExceptionFilter {
8
+ catch(exception: BadRequestException, host: ArgumentsHost) {
9
+ if(host.getType<GqlContextType>() === 'graphql') {
10
+ return exception;
11
+ }
12
+
13
+ const ctx = host.switchToHttp();
14
+ const response = ctx.getResponse<Response>();
15
+ const status = exception.getStatus();
16
+
17
+ response
18
+ .status(status)
19
+ .send(exception['response']);
20
+ }
21
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './utils'
2
+ export * from './decorators'
3
+ export * from './dto'
4
+ export * from './errors'
5
+ export * from './filters'
6
+ export * from './logger'
@@ -0,0 +1 @@
1
+ export * from './logger-factory'
@@ -0,0 +1,42 @@
1
+ import { LoggerService } from '@nestjs/common';
2
+ import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston';
3
+ import { format, transports } from 'winston';
4
+ import DailyRotateFile from "winston-daily-rotate-file";
5
+ import { joinFromRoot } from '../utils';
6
+
7
+ export const LoggerFactory = (appName: string): LoggerService => {
8
+ return WinstonModule.createLogger(LoggerFactoryOptions(appName))
9
+ }
10
+
11
+ export const LoggerFactoryOptions = (appName:string): WinstonModuleOptions => {
12
+ const relativePath = `../logs/${appName}`
13
+ return {
14
+ transports: [
15
+ new DailyRotateFile({
16
+ filename: joinFromRoot(relativePath,`%DATE%-error.log`),
17
+ level: 'error',
18
+ format: format.combine(format.timestamp({format:'YYYY-MM-DDTHH:mm:ssZZ'}), format.ms(), format.json()),
19
+ datePattern: 'YYYY-MM-DD',
20
+ zippedArchive: true,
21
+ maxFiles: '30d',
22
+ }),
23
+ new DailyRotateFile({
24
+ filename: joinFromRoot(relativePath,`%DATE%-combined.log`),
25
+ format: format.combine(format.timestamp({format:'YYYY-MM-DDTHH:mm:ssZZ'}), format.ms(), format.json()),
26
+ datePattern: 'YYYY-MM-DD',
27
+ zippedArchive: true,
28
+ maxFiles: '30d',
29
+ }),
30
+ new transports.Console({
31
+ format: format.combine(
32
+ format.timestamp(),
33
+ format.ms(),
34
+ utilities.format.nestLike(appName, {
35
+ colors: true,
36
+ prettyPrint: true,
37
+ }),
38
+ ),
39
+ }),
40
+ ],
41
+ }
42
+ }
@@ -0,0 +1,4 @@
1
+ export const chunk = <T>(arr:T[], size:number): (T[])[] =>
2
+ Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
3
+ arr.slice(i * size, i * size + size)
4
+ );
@@ -0,0 +1,5 @@
1
+ export * from './chunk.helper'
2
+ export * from './path.helper'
3
+ export * from './slug.helper'
4
+ export * from './storage.helper'
5
+ export * from './types.helper'
@@ -0,0 +1,5 @@
1
+ import { join } from 'path';
2
+
3
+ export const ROOT_PATH = join(__dirname,'..','../../')
4
+
5
+ export const joinFromRoot = (...paths:string[]):string => join(ROOT_PATH,...paths)
@@ -0,0 +1,22 @@
1
+ export default class Slugger {
2
+ private unique;
3
+ constructor(private readonly word: string) {}
4
+
5
+ public get(): string {
6
+ return this.word.trim().normalize("NFD").replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9\s]/gi, '').replaceAll(' ','-').toLowerCase() + (this.unique ? '-'+this.randomString(5) : '')
7
+ }
8
+
9
+ public makeUnique() {
10
+ this.unique = true
11
+ }
12
+
13
+ private randomString(length:number) {
14
+ let result = '';
15
+ const characters = 'abcdefghijklmnopqrstuvwxyz';
16
+ const charactersLength = characters.length;
17
+ for (let i = 0; i < length; i++) {
18
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
19
+ }
20
+ return result;
21
+ }
22
+ }
@@ -0,0 +1,39 @@
1
+ import * as fs from 'fs';
2
+ import { promisify } from 'util';
3
+ import { join, dirname } from 'path';
4
+ import { EOL } from 'os';
5
+
6
+ export default class StorageHelper {
7
+ public rootPath = join(__dirname,'..','../../public')
8
+
9
+ public async read(path:string): Promise<string | Buffer> {
10
+ const realPath = join(this.rootPath,path)
11
+ const readFile = promisify(fs.readFile);
12
+ return await readFile(realPath, {});
13
+ }
14
+
15
+ public async write(path:string,data:string): Promise<void> {
16
+ const realPath = join(this.rootPath,path)
17
+ this.safeDirectory(realPath)
18
+ const writeFile = promisify(fs.writeFile);
19
+ return await writeFile(realPath, data, 'utf8');
20
+ }
21
+
22
+ public async append(path:string,data:string): Promise<void> {
23
+ const realPath = join(this.rootPath,path)
24
+ this.safeDirectory(realPath)
25
+ const appendFile = promisify(fs.appendFile);
26
+ return await appendFile(realPath, data + EOL);
27
+ }
28
+
29
+ public async delete(path:string): Promise<void> {
30
+ const realPath = join(this.rootPath,path)
31
+ const unlink = promisify(fs.unlink);
32
+ return fs.existsSync(realPath) ? await unlink(realPath) : null;
33
+ }
34
+
35
+ private safeDirectory(path:string) {
36
+ const dirPath = dirname(path)
37
+ if(!fs.existsSync(dirPath)) fs.mkdirSync(dirPath,{recursive:true});
38
+ }
39
+ }
@@ -0,0 +1,3 @@
1
+ export type ExcludeMethods<T> = { [K in keyof T as (T[K] extends Function ? never : K)]: T[K] }
2
+ export type WithRelations<T,R extends object> = T & R
3
+ export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@maioradv/nestjs-core",
3
+ "version": "1.0.1",
4
+ "description": "NestJS helpers by MaiorADV",
5
+ "repository": "https://github.com/maioradv/nestjs-core.git",
6
+ "author": "Maior ADV Srl",
7
+ "license": "MIT",
8
+ "private": false,
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublish:npm": "npm run build",
12
+ "publish:npm": "npm publish --access public",
13
+ "prepublish:next": "npm run build",
14
+ "publish:next": "npm publish --access public --tag next"
15
+ },
16
+ "dependencies": {
17
+ "@nestjs/common": "^10.3.8",
18
+ "@nestjs/graphql": "^12.1.1",
19
+ "@nestjs/swagger": "^7.3.1",
20
+ "class-transformer": "^0.5.1",
21
+ "class-validator": "^0.14.1",
22
+ "express": "^4.19.2",
23
+ "nest-winston": "^1.10.0",
24
+ "ts-node": "^10.9.2",
25
+ "winston": "^3.13.0",
26
+ "winston-daily-rotate-file": "^5.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.12.13",
30
+ "typescript": "^5.4.5"
31
+ },
32
+ "peerDependencies": {}
33
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "declaration": true,
5
+ "removeComments": true,
6
+ "noLib": false,
7
+ "emitDecoratorMetadata": true,
8
+ "esModuleInterop": true,
9
+ "experimentalDecorators": true,
10
+ "target": "ES2021",
11
+ "sourceMap": false,
12
+ "outDir": "./dist",
13
+ "rootDir": "./lib",
14
+ "skipLibCheck": true,
15
+ "paths": {
16
+ "@/*": ["./lib/*"],
17
+ }
18
+ },
19
+ "include": ["lib/**/*"],
20
+ "exclude": ["node_modules", "**/*.spec.ts", "tests"]
21
+ }