@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.
- package/README.md +1 -0
- package/index.ts +1 -0
- package/lib/decorators/api-response-with-rels.decorator.ts +75 -0
- package/lib/decorators/index.ts +1 -0
- package/lib/dto/clauses/index.ts +2 -0
- package/lib/dto/clauses/model.ts +15 -0
- package/lib/dto/clauses/standard-clauses.decorator.ts +80 -0
- package/lib/dto/index.ts +3 -0
- package/lib/dto/pagination/cursor-meta.dto.ts +38 -0
- package/lib/dto/pagination/cursor-paginated.dto.ts +68 -0
- package/lib/dto/pagination/cursor-query.dto.ts +64 -0
- package/lib/dto/pagination/index.ts +16 -0
- package/lib/dto/pagination/model.ts +7 -0
- package/lib/dto/pagination/paginated-meta.dto.ts +37 -0
- package/lib/dto/pagination/paginated-query.dto.ts +40 -0
- package/lib/dto/pagination/paginated-response.decorator.ts +49 -0
- package/lib/dto/pagination/paginated.dto.ts +15 -0
- package/lib/dto/pagination/pagination-params.decorator.ts +21 -0
- package/lib/dto/sorting/enums.ts +4 -0
- package/lib/dto/sorting/index.ts +3 -0
- package/lib/dto/sorting/models.ts +47 -0
- package/lib/dto/sorting/sorting-params.decorator.ts +35 -0
- package/lib/errors/index.ts +1 -0
- package/lib/errors/validation.error.ts +9 -0
- package/lib/filters/all-exception.filter.ts +33 -0
- package/lib/filters/http-exception.filter.ts +32 -0
- package/lib/filters/index.ts +3 -0
- package/lib/filters/validation-exception.filter.ts +21 -0
- package/lib/index.ts +6 -0
- package/lib/logger/index.ts +1 -0
- package/lib/logger/logger-factory.ts +42 -0
- package/lib/utils/chunk.helper.ts +4 -0
- package/lib/utils/index.ts +5 -0
- package/lib/utils/path.helper.ts +5 -0
- package/lib/utils/slug.helper.ts +22 -0
- package/lib/utils/storage.helper.ts +39 -0
- package/lib/utils/types.helper.ts +3 -0
- package/package.json +33 -0
- 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,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
|
+
};
|
package/lib/dto/index.ts
ADDED
|
@@ -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,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,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,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 @@
|
|
|
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,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
|
+
}
|
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
|
+
}
|