Nestjs + Swagger: How to add a custom option to #ApiProperty - node.js

Is it possible to add a custom option to #ApiProperty decorator?
import { ApiProperty } from '#nestjs/swagger';
class Animal {
#ApiProperty({
type: String,
description: 'animal name',
'x-description': 'some information' // how to add a cutom option 'x-description' ?
})
name: string;
}

I'm not sure i fully understand, but If you are talking about openapi's extensions. Take a look at this: https://github.com/nestjs/swagger/issues/195

Solution from https://github.com/nestjs/swagger/issues/195#issuecomment-526215840
import { ApiProperty } from '#nestjs/swagger';
type SwaggerDecoratorParams = Parameters<typeof ApiProperty>;
type SwaggerDecoratorMetadata = SwaggerDecoratorParams[0];
type Options = SwaggerDecoratorMetadata & DictionarySwaggerParameters;
type DictionarySwaggerParameters = { 'x-property'?: string };
export const ApiPropertyX = (params: Options) => {
return ApiProperty(params);
};

Related

TypeScript failes to infer return type of constrained generic function in some cases, why is this happening?

I have this code, which works just fine in a simple case, meaning the "permissionList" const is type inferred, VSCode can suggest me the properties, such as "permission_1". Also inside the "constraintFn", when I declare the object from which the type is inferred, it suggests me "code" and "id", since these are the valid properties that you can use based on the generic type "T".
type PropType<T> = {
[key: string]: T;
};
type Codes = 'code1' | 'code2';
class PermissionType {
code: string;
id: number;
}
const constraintFn = <T, TRe extends PropType<T>>(t: { new(): T; }, obj: TRe): TRe => {
return obj;
};
export const permissionList = constraintFn(PermissionType, ({
permission_1: {
code: 'code1',
id: 1,
},
permission_2: {
code: 'code2',
id: 2,
},
}));
Now, if I change one thing:
class PermissionType {
code: Codes; // this changed from string, to Codes
id: number;
}
The inference still works when I declare the object in the function param, I can chose from a list of Codes, BUT the inference of the const "permissionList" disappears, and it only displays:
PropType<PermissionType>
Instaead of:
{
permission_1: {
code: string;
id: number;
};
permission_2: {
code: string;
id: number;
};
}
Thanks, I hope there's a solution to this. It would be fun to create these kind of types.
Odd that using as const here makes TypeScript give you the whole type...
export const permissionList = constraintFn(PermissionType, {
permission_1: {
code: "code1",
id: 1,
},
permission_2: {
code: 'code2',
id: 2,
},
} as const); // added here
Playground
My solution so far is this. I don't really consider this as a perfect one, since it involves some kind of weird type recursion (see how TRe is constrained by a type that accepts TRe as a generic parameter), which feels like a hack. Anyways, if it helps someone, this compiles fine.
[edit]: reflecting on the answer containing "as const", that's something I don't want to write on the end of each object that I supply as parameter to the "constraintFn". I much rather find a solution that does this inside the function.
type PropConstraintType<TObj, TProp> = {
[K in keyof TObj]: TProp;
};
type Codes = 'code1' | 'code2';
class PermissionType {
code: Codes;
id: number;
}
const constraintFn = <T, TRe extends PropConstraintType<TRe, T>>(t: { new(): T; }, obj: TRe): TRe => {
return obj;
};
export const permissionList = constraintFn(PermissionType, ({
permission_1: {
code: 'code1',
id: 1,
},
permission_2: {
code: 'code2',
id: 2,
},
}));
When we use generics for type alias, the Typescript compiler generally doesn't go to infer types at each of the levels, hence we just see the alias PropType<PermissionType>
There is a trick using which we can make this type expand with a custom type alias Id<T>
type Id<T> = {} & {[k in keyof T]: T[k]}
// This type allows the expansion of other type
type Id<T> = {} & {[k in keyof T]: T[k]}
const constraintFn = <T, TRe extends PropType<T>>(t: { new(): T; }, obj: TRe): Id<TRe> => {
return obj;
};
export const permissionList = constraintFn(PermissionType, {
permission_1: {
code: "code1",
id: 1,
},
permission_2: {
code: 'code2',
id: 2,
},
});
// Now permission list has types => { [x: string]: PermissionType }
Code Playground

How to pass default parameters to the #Query class in Nest.Js?

I'm trying to pass the default parameters maxnodes=3 and addstats=false to the controller via the #Query parameter in Nest.Js.
The code works fine, but the default parameters are not used. When I pass on the query parameters the ones that are passed are shown, but if none are passed, the default values (3 and false) are not used.
How to fix that?
context.contructor.ts:
import { CreateContextQuery } from './context.query';
import { CreateContextDto } from './context.dto';
#Post('graph')
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
context.query.ts:
import { ApiProperty } from '#nestjs/swagger';
export class CreateContextQuery {
#ApiProperty({
description: 'Maximum number of nodes to show on the graph',
})
maxnodes;
#ApiProperty({
description: 'Include graph statistics',
})
addstats;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
So basically in your DTO, you can give default values.
export class CreateContextQuery {
#IsOptional()
#Type(() => Number)
#IsNumber()
#Min(0)
maxnodes?: number = 3;
#IsOptional()
#Type(() => Boolean)
#IsBoolean()
addstats?: boolean = false;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
// as you can see i am using validation too
And in your controller :
#Post('graph')
#UsePipes(new ValidationPipe({ transform: true }))
// you need to add this for tansformation
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
PS
Also if you want you can add custom decorators, in your case:
// add this decorator
export const GetContextQuery = createParamDecorator((_data: unknown, ctx: ExecutionContext): CreateContextDto => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const maxnodes = parseInt(query.maxnodes) || 3;//default values here in case it fails to parse
const addstats = Boolean(query.addstats) || 0;
return { addstats, addstats };
});
and in your controller, you can call the decorator instead of #Query
just add your decorator #GetContextQuery() context: CreateContextDto, and now you do not need the UsePipes
What you receive in the query param is a plain object. You can achieve what you want putting a pipe in your query param and applying a class transform to instantiate the class.
Read this: https://docs.nestjs.com/pipes#providing-defaults
contextQuery isn't an instance of CreateContextQuery because, without any configuration, Nest won't call new CreateContextQuery any time. This is why you end up using pipes (read this https://docs.nestjs.com/techniques/validation#transform-payload-objects)

how can i access nested properties for CASL conditions

I can't seem to access the nested object with the condition rule.
i want a user to have access to delete an article if the article's comment has the same id as the user.
these are just some made up classes to test...
here is my code:
import { defineAbility, AbilityBuilder } from '#casl/ability';
import { Ability, AbilityClass, ExtractSubjectType, InferSubjects } from '#casl/ability';
export class Article {
static readonly modelName = "Article";
static readonly __typename = "Article";
constructor( id: number,
title: string,
content: string,
user: User,
comment: Comment) {
this.id = id
this.title = title
this.content = content
this.user = user
this.comment = comment
}
id: number
title: string
content: string
user: User
comment: Comment
}
export class User {
static readonly modelName = "User"
static readonly __typename = "User";
constructor (id: number,
name: string,
comment: Comment) {
this.id = id
this.name = name
this.comment = comment
}
id: number
name: string
comment: Comment
}
export class Comment {
static readonly modelName = "Comment"
static readonly __typename = "Comment";
constructor(id: number,
content: string,
authorId: number) {
this.id = id
this.content = content
this.authorId = authorId
}
id: number
content: string
authorId: number
}
type Action = 'create' | 'read' | 'update' | 'delete';
type Subjects = InferSubjects<typeof Article | typeof Comment| typeof User, true>;
export type AppAbility = Ability<[Action, Subjects]>;
export function createForUser(user: User) {
const { can, cannot, build } = new AbilityBuilder<
Ability<[Action, Subjects]>
>(Ability as AbilityClass<AppAbility>);
can('delete', Article, { comment: {id: user.comment.id}})
return build({
detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects>
});
}
and im testing it with:
const comment = new Comment(0, 'a', 0)
const user = new User(1, 'sd', comment);
const article = new Article(2, 'sd', 'asd', user, comment)
const ability = createForUser(user);
console.log(ability.can('delete', article))// false
i saw somewhere that i need to do somthing like this:
can('delete', Article, { 'comment.id': user.comment.id})
but when i do is says 'Object literal may only specify known properties, and ''comment.id'' does not exist in type 'string[]'
You can find “Nested fields with dot notation” section on https://casl.js.org/v5/en/advanced/typescript useful. In short, when you use for notation to define conditions together with typescript, you need to create a custom type:
type FlatArticle = Article & {
'comment.id': Article['comment']['id']
};
can<FlatArticle>('read', Article, { 'comment.id': 1 });
Ended up solving it like this:
export enum Action {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
export type Subject = InferSubjects<typeof Article |
typeof Comment |
typeof User |
'all';
export type AppAbility = PureAbility<[Action, Subject]>;
export const AppAbility = Ability as AbilityClass<AppAbility>;
and then defining rules like this:
createForUser(user: User) {
const { can, cannot, build } = new AbilityBuilder(AppAbility);
can(Action.Read, Article, { 'comment.id': 1 });
return build({
detectSubjectType: item => item.constructor as ExtractSubjectType<Subject>
});
}

Dynamic Dependency injection with Typescript using tsyringe

I am trying to build and example to understand how the DI framework/library works, but i am encountering some problems.
I have this interface with two possible implementations:
export interface Operation {
calculate(a: number, b: number): number;
}
sub.ts
import { Operation } from "./operation.interface";
export class Sub implements Operation {
calculate(a: number, b: number): number {
return Math.abs(a - b);
}
}
sum.ts
import { Operation } from "./operation.interface";
export class Sum implements Operation {
calculate(a: number, b: number): number {
return a + b;
}
}
calculator.ts
import { Operation } from "./operation.interface";
import {injectable, inject} from "tsyringe";
#injectable()
export class Calculator {
constructor(#inject("Operation") private operation?: Operation){}
operate(a: number, b: number): number {
return this.operation.calculate(a, b);
}
}
index.ts
import "reflect-metadata";
import { container } from "tsyringe";
import { Calculator } from "./classes/calculator";
import { Sub } from "./classes/sub";
import { Sum } from "./classes/sum";
container.register("Operation", {
useClass: Sum
});
container.register("OperationSub", {
useClass: Sub
});
const calculatorSum = container.resolve(Calculator);
const result = calculatorSum.operate(4,6);
console.log(result);
// const calculatorSub = ???
is there a way where I could have two calculators with different behaviours or am I doing it completely wrong?
Since OperationSub isn’t used anywhere, it cannot affect injected Operation value.
Calculators with different dependency sets should be represented with multiple containers. Summing calculator can be considered a default implementation and use root container, or both implementations can be represented by children containers while root container remains abstract.
// common deps are registered on `container`
const sumContainer = container.createChildContainer();
const subContainer = container.createChildContainer();
sumContainer.register("Operation", { useClass: Sum });
subContainer.register("Operation", { useClass: Sub });
const calculatorSum = sumContainer.resolve(Calculator);
const calculatorSub = subContainer.resolve(Calculator);

It's not possible to mock classes with static methods using jest and ts-jest

I have two classes that simulate a simple sum operation.
import SumProcessor from "./SumProcessor";
class Calculator {
constructor(private _processor: SumProcessor) { }
sum(a: number, b: number): number {
return this._processor.sum(a, b)
}
}
export default Calculator
And the operation processor.
class SumProcessor {
sum(a: number, b: number): number {
return a + b
}
static log() {
console.log('houston...')
}
}
export default SumProcessor
I'm tryng to mock the class SumProcessor to write the following unit test using jest+ts-jest.
import Calculator from "./Calculator"
import SumProcessor from "./SumProcessor"
import { mocked } from "ts-jest/utils"
jest.mock('./SumProcessor')
describe('Calculator', () => {
it('test sum', () => {
const SomadorMock = <jest.Mock>(SumProcessor)
SomadorMock.mockImplementation(() => {
return {
sum: () => 2
}
})
const somador = new SomadorMock()
const calc = new Calculator(somador)
expect(calc.sum(1, 1)).toBe(2)
})
})
When the static method is present in class SumProcessor, the mock code const SomadorMock = (SumProcessor) indicates the following compilation error:
TS2345: Argument of type '() => jest.Mock<any, any>' is not assignable to parameter of type '(values?: object, option
s?: BuildOptions) => SumOperator'.
Type 'Mock<any, any>' is missing the following properties from type 'SumOperator...
If the static method is removed from SumProcessor class, everything work's fine.
Can anybody help?
since you have already mocked the SumProcessor class with jest.mock('./SumProcessor'); you can just add a spy to the method you would like to mock, for an example:
jest.spyOn(SumProcessor.prototype, 'sum').mockImplementation(() => 2);
this way your test class would look something like this:
import Calculator from "./Calculator"
import SumProcessor from "./SumProcessor"
jest.mock('./SumProcessor')
describe('Calculator', () => {
it('test sum', () => {
jest.spyOn(SumProcessor.prototype, 'sum').mockImplementation(() => 2);
const somador = new SumProcessor();
const calc = new Calculator(somador)
expect(calc.sum(1, 1)).toBe(2)
})
})
much simpler, right?

Resources