Using an Abstract class for mutations in NestJS with dynamic input #Args for the update mutation throws type error - node.js

Have been working on adding an abstract class to support versioning for Resolvers for a floorPlan feature and was wondering how I might fix this error. At the moment I do not get any Type errors but when the code compiles NestJs throws this error:
Undefined type error. Make sure you are providing an explicit type for the "floorPlanUpdate" (parameter at index [2]) of the "BaseResolverHost" class.
In a normal resolver this is not a problem as you pass in the type e.g.:
#Args('input') input: UpdateFloorPlanV1Input,
However as I am trying to determine the input type based off the version of the Floorplan using this line:
#Args('input') input: UpdateFloorPlanVersion<FloorPlanVersion>,
So it is not possible to explicitly declare it here. Let me know if this is possible or if anyone has any other approaches
This is the abstract Resolver code:
import { Args, Mutation, Query, Resolver } from '#nestjs/graphql';
import { UserId } from 'src/users/decorators/user-id.decorator';
import { CreateFloorPlanInput } from './v1/dto/create-floor-plan.input';
import { UpdateFloorPlanV1Input } from './v1/dto/update-floor-plan.input';
import { FloorPlanV1 } from './v1/models/floor-plan.model';
import { UpdateFloorPlanV2Input } from './v2/dto/update-floor-plan.input';
import { FloorPlanV2 } from './v2/models/floor-plan.model';
export type FloorPlanVersion = FloorPlanV1 | FloorPlanV2;
export type UpdateFloorPlanVersion<T extends FloorPlanVersion> =
T extends FloorPlanV1 ? UpdateFloorPlanV1Input : UpdateFloorPlanV2Input;
export interface FloorPlanServiceInterface<FloorPlanVersion> {
create(
userId: string,
createFloorPlanInput: CreateFloorPlanInput,
): Promise<FloorPlanVersion>;
findOne(userId: string, floorPlanId: string): Promise<FloorPlanVersion>;
update(
userId: string,
floorPlanId: string,
input: UpdateFloorPlanV1Input,
): Promise<FloorPlanVersion>;
}
export function BaseResolver<T extends Type<FloorPlanVersion>>(
classRef: T,
version: string,
) {
#Resolver({ isAbstract: true })
abstract class BaseResolverHost {
floorPlanService: FloorPlanServiceInterface<FloorPlanVersion>;
constructor(readonly service: FloorPlanServiceInterface<FloorPlanVersion>) {
this.floorPlanService = service;
}
#Mutation(() => classRef, { name: `floorPlanCreate${version}` })
async floorPlanCreate(
#UserId() userId,
#Args('input')
input: CreateFloorPlanInput,
) {
return this.floorPlanService.create(userId, input);
}
#Query(() => classRef, { name: `floorPlan${version}` })
async floorPlan(#UserId() userId, #Args('id') id: string) {
return this.floorPlanService.findOne(userId, id);
}
#Mutation(() => classRef, { name: `floorPlanUpdate${version}` })
async floorPlanUpdate(
#UserId() userId,
#Args('id') id: string,
#Args('input') input: UpdateFloorPlanVersion<FloorPlanVersion>,
) {
return this.floorPlanService.update(userId, id, input);
}
}
return BaseResolverHost;
}
And then it is called like this:
#Resolver(() => FloorPlanV2)
export class FloorPlanV2Resolver extends BaseResolver(FloorPlanV2, 'V2') {
constructor(
readonly floorPlanService: FloorPlanService,
) {
super(floorPlanService);
}
}
So if the ObjectType was FloorPlanV2 we would expect the UpdateFloorPlanInputType to be UpdateFloorPlanV2Input

Related

Ravendb Nodejs, using Enums in the map index

I use ravendb in nestjs
I have an enum in the library project (core library):
export enum GroupType {
Agent = "agent",
System = "system"
}
and I import and use that enum in the index logic (raven project)
import { Models } from '#ply/core';
export class Agents_All extends AbstractJavaScriptMultiMapIndexCreationTask<AgentResult> {
this.map<IUser>(User, user => {
if(user.groupType === Models.Enums.GroupType.Agent){
Q1 :
how can I use my enums or constants in the map index, should I export them to the Ravendb as an additional source? export enum like a function?
and below is the generated map index on the Ravendb side :
Q2 :
why did Ravendb add the project's name of the enum as a namespace before the enum
Models.Enums.GroupType.Agent converted to core_1.Models.Enums.GroupType.Agent
We implemented this feature and in order to use such enum, you should register it via this.registerEnum(() => RoleString.Admin); call. Then you can use such enum inside map/reduce function and it is replaced with correct enum value.
enum RoleNumber {
Admin,
User
}
class User {
id: string;
name: string;
roleString: RoleString;
roleNumber: RoleNumber;
}
class UsersByStringRole extends AbstractJavaScriptIndexCreationTask<User, Pick<User, "name" | "roleString">> {
constructor() {
super();
this.registerEnum(() => (RoleString.Admin));
this.map(User, u => {
if (u.roleString === RoleString.Admin) {
return ({
name: u.name,
roleString: u.roleString
});
}
return null;
});
}
}
class UsersByNumberRole extends AbstractJavaScriptIndexCreationTask<User, Pick<User, "name" | "roleNumber">> {
constructor() {
super();
this.registerEnum(() => RoleNumber.Admin);
this.map(User, u => {
if (u.roleNumber === RoleNumber.Admin) {
return ({
name: u.name,
roleNumber: u.roleNumber
});
}
return null;
});
}
}

TypeScript - Repository pattern with Sequelize

I'm converting my Express API Template to TypeScript and I'm having some issues with the repositories.
With JavaScript, I would do something like this:
export default class BaseRepository {
async all() {
return this.model.findAll();
}
// other common methods
}
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository {
constructor() {
super();
this.model = User;
}
async findByEmail(email) {
return this.model.findOne({
where: {
email,
},
});
}
// other methods
Now, with TypeScript, the problem is that it doesn't know the type of this.model, and I can't pass a concrete model to BaseRepository, because, well, it is an abstraction. I've found that sequelize-typescript exports a ModelCtor which declares all the static model methods like findAll, create, etc., and I also could use another sequelize-typescript export which is Model to properly annotate the return type.
So, I ended up doing this:
import { Model, ModelCtor } from 'sequelize-typescript';
export default abstract class BaseRepository {
protected model: ModelCtor;
constructor(model: ModelCtor) {
this.model = model;
}
public async all(): Promise<Model[]> {
return this.model.findAll();
}
// other common methods
}
import { Model } from 'sequelize-typescript';
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository {
constructor() {
super(User);
}
public async findByEmail(email: string): Promise<Model | null> {
return this.model.findOne({
where: {
email,
},
});
}
// other methods
}
Ok, this works, TypeScript doesn't complain about methods like findOne or create not existing, but that generates another problem.
Now, for example, whenever I get a User from the repository, if I try to access one of its properties, like user.email, TypeScript will complain that this property does not exist. Of course, because the type Model does not know about the specifics of each model.
Ok, it's treason generics then.
Now BaseRepository uses a generic Model type which the methods also use:
export default abstract class BaseRepository<Model> {
public async all(): Promise<Model[]> {
return Model.findAll();
}
// other common methods
}
And the concrete classes pass the appropriate model to the generic type:
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository<User> {
public async findByEmail(email: string): Promise<User | null> {
return User.findOne({
where: {
email,
},
});
}
// other methods
}
Now IntelliSense lights up correctly, it shows both abstract and concrete classes methods and the model properties (e.g. user.email).
But, as you have imagined, that leads to more problems.
Inside BaseRepository, where the methods use the Model generic type, TypeScript complains that 'Model' only refers to a type, but is being used as a value here. Not only that, but TypeScript also doesn't know (again) that the static methods from the model exist, like findAll, create, etc.
Another problem is that in both abstract and concrete classes, as the methods don't use this anymore, ESLint expects the methods to be static: Expected 'this' to be used by class async method 'all'. Ok, I can just ignore this rule in the whole file and the error is gone. It would be even nicer to have all the methods set to static, so I don't have to instantiate the repository, but maybe I'm dreaming too much.
Worth mentioning that although I can just silence those errors with // #ts-ignore, when I execute this, it doesn't work: TypeError: Cannot read property 'create' of undefined\n at UserRepository.<anonymous>
I researched a lot, tried to make all methods static, but static methods can't reference the generic type (because it is considered an instance property), tried some workarounds, tried to pass the concrete model in the constructor of BaseRepository along with the class using the generic type, but nothing seems to work so far.
In case you want to check the code: https://github.com/andresilva-cc/express-api-template/tree/main/src/App/Repositories
EDIT:
Found this: Sequelize-Typescript typeof model
Ok, I removed some unnecessary code from that post and that kinda works:
import { Model } from 'sequelize-typescript';
export default abstract class BaseRepository<M extends Model> {
constructor(protected model: typeof Model) {}
public async all(attributes?: string[]): Promise<M[]> {
// Type 'Model<{}, {}>[]' is not assignable to type 'M[]'.
// Type 'Model<{}, {}>' is not assignable to type 'M'.
// 'Model<{}, {}>' is assignable to the constraint of type 'M', but 'M' could be instantiated with a different subtype of constraint 'Model<any, any>'.
return this.model.findAll({
attributes,
});
}
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository<User> {
constructor() {
super(User);
}
}
I mean, if I put some // #ts-ignore it at least executes, and IntelliSense lights up perfectly, but TypeScript complains.
We faced the same problem. The solution was to declare returning types with an interface that an abstract repository class implements.
Code for the interface:
export type RepoResult<M> = Promise<Result<M | undefined, RepoError | undefined>>;
export interface IRepo<M> {
save(model: M): RepoResult<M>;
findById(id: string): RepoResult<M>;
search(parameterName: string, parameterValue: string, sortBy: string, order: number, pageSize: number, pageNumber: number): RepoResult<M[]>;
getAll(): RepoResult<M[]>;
deleteById(id: string): RepoResult<M>;
findByIds(ids: string[]): RepoResult<M[]>;
deleteByIds(ids: string[]): RepoResult<any>;
};
Code for the abstract class:
export abstract class Repo<M extends sequelize.Model> implements IRepo<M> {
protected Model!: sequelize.ModelCtor<M>;
constructor(Model: sequelize.ModelCtor<M>) {
this.Model = Model;
}
public async save(doc: M) {
try {
const savedDoc = await doc.save();
return Result.ok(savedDoc);
} catch (ex: any) {
logger.error(ex);
return Result.fail(new RepoError(ex.message, 500));
}
}
public async findById(id: string) {
try {
const doc = await this.Model.findOne({where: {
id: id
}});
if (!doc) {
return Result.fail(new RepoError('Not found', 404));
}
return Result.ok(doc);
} catch (ex: any) {
return Result.fail(new RepoError(ex.message, 500));
}
}
}
Hope it helps. Have a nice day:)
EDIT:
Result is a class that looks like this:
export class Result<V, E> {
public isSuccess: boolean;
public isFailure: boolean;
private error: E;
private value: V;
private constructor(isSuccess: boolean, value: V, error: E) {
if (isSuccess && error) {
throw new Error('Successful result must not contain an error');
} else if (!isSuccess && value) {
throw new Error('Unsuccessful error must not contain a value');
}
this.isSuccess = isSuccess;
this.isFailure = !isSuccess;
this.value = value;
this.error = error;
}
public static ok<V>(value: V): Result<V, undefined> {
return new Result(true, value, undefined);
}
public static fail<E>(error: E): Result<undefined, E> {
return new Result(false, undefined, error);
}
public getError(): E {
if (this.isSuccess) {
throw new Error('Successful result does not contain an error');
}
return this.error;
}
public getValue(): V {
if (this.isFailure) {
throw new Error('Unsuccessful result does not contain a value');
}
return this.value;
}
}
RepoError class:
type RepoErrorCode = 404 | 500;
export class RepoError extends Error {
public code: RepoErrorCode;
constructor(message: string, code: RepoErrorCode) {
super(message);
this.code = code;
}
}
RepoResult type:
export type RepoResult<M> = Promise<Result<M | undefined, RepoError | undefined>>;
You can find more info on the pattern at the link below:
https://khalilstemmler.com/articles/enterprise-typescript-nodejs/functional-error-handling/

Problems with ValidationPipe in NestJS when I need to validate the contents of an array

I have a situation where my client user can enter zero or multiple addresses. My problem is that if he enters an address, some fields need to be mandatory.
user.controller.ts
#Post()
#UsePipes(ValidationPipe)
async createUser(
#Body() createUser: CreateUserDto,
) {
return await this.service.saveUserAndAddress(createUser);
}
create-user.dto.ts
export class CreateUserDto {
#IsNotEmpty({ message: 'ERROR_REQUIRED_FULL_NAME' })
fullName?: string;
#IsNotEmpty({ message: 'ERROR_REQUIRED_PASSWORD' })
password?: string;
#IsNotEmpty({ message: 'ERROR_REQUIRED_EMAIL' })
#IsEmail({}, { message: 'ERROR_INVALID_EMAIL' })
email?: string;
...
addresses?: CreateUserAddressDto[];
}
create-user-address.dto.ts
export class CreateUserAddressDto {
...
#IsNotEmpty()
street: string;
...
}
CreateUserDto data is validated correctly and generates InternalServerErrorResponse, but CreateUserAddressDto data is not validated when there is some item in my array. Any idea how I can do this validation?
Nest fw uses class-transformer to convert a json to a class object. You have to set the correct type for the sub-attribute if it is not a primitive value. And your attribute is an array, you have to config to tell class-validator that it is an array, and validate on each item.
Let's update CreateUserDto
import { Type } from 'class-transformer';
import { ..., ValidateNested } from 'class-validator';
export class CreateUserAddressDto {
...
#ValidateNested({ each: true })
#Type(() => CreateUserAddressDto)
addresses?: CreateUserAddressDto[];
...
}
What you are trying to do is - to basically add logic to primitive validators provided out of the box with nest - aka - defining a custom validator.
This can be done by using the two classes ValidatorConstraint and ValidatorConstraintInterface provided by the class validator.
In order to sort this, transform the incoming input / club whatever data you want to validate at once into an object - either using a pipe in nestjs or sent it as an object in the API call itself, then attach a validator on top of it.
To define a custom validator:
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
/**
* declare your custom validator here
*/
#ValidatorConstraint({ name: 'MyValidator', async: false })
export class MyValidator implements ValidatorConstraintInterface {
/** return true when tests pass **/
validate(incomingObject: myIncomingDataInterface) {
try {
// your logic regarding what all is required in the object
const output = someLogic(incomingObject);
return output;
} catch (e) {
return false;
}
}
defaultMessage() {
return 'Address update needs ... xyz';
}
}
Once you have defined this, keep this safe somewhere as per your project structure. Now you just need to call it whenever you want to put this validation.
In the data transfer object,
// import the validator
import { Validate } from 'class-validator';
import { MyValidator } from './../some/safe/place'
export class SomeDto{
#ApiProperty({...})
#Validate(MyValidator)
thisBecomesIncomingObjectInFunction: string;
}
As simple as that.

Correctly Saving and Updating Entites in Netsjs+TypeORM

I've got a Question regarding TypeORM-Relations and how to use them 'nest-like'.
Suppose I have two Entities defined ChildEntity and TestEntity, which are related.
TestEntity:
import { ChildEntity } from 'src/modules/child-entity/entities/child-entity.entity';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
#Entity()
export class TestEntity {
#PrimaryGeneratedColumn()
id: number;
#Column('varchar')
name: string;
#ManyToOne(() => ChildEntity, (childEntity) => childEntity.testEntities)
childEntity: ChildEntity;
constructor(name: string, childEntity: ChildEntity) {
this.name = name;
this.childEntity = childEntity;
}
}
My first question occurs when I want to create the entity. I have to first translate the passed childEntityId into a ChildEntity, which I can pass to the constructor:
CreateTestEntityDto
import { ApiProperty } from '#nestjs/swagger';
import { IsNotEmpty, IsNumber } from 'class-validator';
export class CreateTestEntityDto {
#ApiProperty()
#IsNotEmpty()
name: string;
#ApiProperty()
#IsNumber()
childEntityId: number;
constructor(name: string, childEntityId: number) {
this.name = name;
this.childEntityId = childEntityId;
}
}
async create(createTestEntityDto: CreateTestEntityDto) {
const { name, childEntityId } = createTestEntityDto;
const childEntity = await this.childEntityService.findOne(childEntityId);
const testEntity = new TestEntity(name, childEntity);
return this.testEntityRepo.save(testEntity);
}
Is there a way to just pass the childEntityId to the save()-Method without explicitly looking for the ChildEntity beforehand?
The Second problem occurs when updating.
UpdateTestEntityDto
import { PartialType } from '#nestjs/swagger';
import { CreateTestEntityDto } from './create-test-entity.dto';
export class UpdateTestEntityDto extends PartialType(CreateTestEntityDto) {}
As updating only a partial Entity is possible I have to check if the Id is even passed along the request and if it is I have to retrieve the correct Entity for the update. Is there a more streamlined way to do this?
async update(id: number, updateTestEntityDto: UpdateTestEntityDto) {
const { name, childEntityId } = updateTestEntityDto;
const props = { name };
if (childEntityId) {
props['childEntity'] = await this.childEntityService.findOne(
childEntityId,
);
}
return this.testEntityRepo.update(id, props);
}
You should add a childEntityId to the test entity:
#Entity()
export class TestEntity {
#PrimaryGeneratedColumn()
id: number;
#Column('varchar')
name: string;
#Column('int')
childEntityId: number;
#ManyToOne(() => ChildEntity, (childEntity) => childEntity.testEntities)
childEntity: ChildEntity;
...
}
and then you can use it to set the id directly. Something like:
async create(dto: Dto) {
const { name, childEntityId } = dto;
const entity = new TestEntity();
entity.name = name;
entity.childEntityId = childEntityId;
return this.testEntityRepo.save(entity);
}
Check this out.
1.) Saving relational entity
There's no need to do all these roundtrips cluttering to save the entity. While, the solution given by #UrosAndelic works but still there's no need to write 3 extra lines of code.
If you hover over a relational param inside the create() method of the repository from an IDE, you'll notice that it accepts two types. First, An Instance of an entity OR Second, a DeepPartial object of an entity.
For instance:
const entity = this.testEntityRepo.create({
name: 'Example 1',
childEntity: {
id: childEntityId // notice: it's a DeepPartial object of ChildEntity
}
})
await this.testEntityRepo.save(entity)
2.) Updating entity
There's no need for child entity's id if you are updating test entity. You can simply update the props of test entity.
const testEntityId = 1;
await this.testEntityRepo.update(testEntityId, {
name: 'Example 2'
})
This will update the name of TestEntity = 1;

Sequelize Typescript: Type 'FavoriteEntity[]' is not assignable to type 'FavoriteEntity | null'

I'm writing a NestJS application using Sequelize-Typescript as ORM.
Here I'm trying to get a users favorited jobs (M:M) and so I have a UserEntity, a JobEntity (not relevant for this question) and a FavoriteEntity.
favorite.entity.ts
import { Table, Column, Model, PrimaryKey, ForeignKey, BelongsTo, NotNull } from "sequelize-typescript";
import { IDefineOptions } from "sequelize-typescript/lib/interfaces/IDefineOptions";
import { UserEntity } from "../users/user.entity";
import { JobEntity } from "../jobs/job.entity";
const tableOptions: IDefineOptions = {
timestamp: true,
tableName: "favorites",
schema: process.env.DB_SCHEMA,
} as IDefineOptions;
#Table(tableOptions)
export class FavoriteEntity extends Model<FavoriteEntity> {
#BelongsTo(() => UserEntity)
user: UserEntity;
#ForeignKey(() => UserEntity)
#PrimaryKey
#NotNull
#Column
userId: number;
#BelongsTo(() => JobEntity)
job: JobEntity;
#ForeignKey(() => JobEntity)
#PrimaryKey
#NotNull
#Column
jobId: number;
}
favorite.service.ts
import { Inject, Injectable } from "#nestjs/common";
import { Model } from "sequelize-typescript";
import { IFavorite, IFavoriteService } from "./interfaces";
import { FavoriteEntity } from "./favorite.entity";
#Injectable()
export class FavoriteService implements IFavoriteService {
constructor(
#Inject("FavoriteRepository") private readonly favoriteRepository: typeof Model,
#Inject("SequelizeInstance") private readonly sequelizeInstance,
) {}
public async findByUserId(userId: number): Promise<FavoriteEntity | null> {
return await FavoriteEntity.scope().findAll<FavoriteEntity>({
where: { userId },
});
}
}
I'm getting a type error that I really don't understand:
TSError: тип Unable to compile TypeScript:
src/modules/favorite/favorite.service.ts(21,5): error TS2322: Type
'FavoriteEntity[]' is not assignable to type 'FavoriteEntity | null'.
Type 'FavoriteEntity[]' is not assignable to type 'FavoriteEntity'.
Property 'user' is missing in type 'FavoriteEntity[]'.
I don't understand why it's complaining about a missing user, it's clearly there. (If I make it optional (?) in the entity it complains about the next property until I make them all optional, and then It's complaining about a missing property dataValues in the same way)
What am I missing?
Thanks!
UPDATE:
So I might have figured out something. If I write like this
return await FavoriteEntity.scope().findAll<FavoriteEntity[]>({
where: { userId },
});
with FavoriteEntity[] instead of FavoriteEntity, I get
Property 'dataValues' is missing in type 'FavoriteEntity[]'
I don't really know which is the correct way to write it, but I still have a problem either way...
findAll will return an array of entities. You only want one or null. For that, use findOne. Also, you don't need to do return await here. Unless in a try-catch block, this is redundant.

Resources