Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ISSUE]: Update failing when primary key is @PrimaryColumn instead of @PrimaryGeneratedColumn #529

Open
chihabhajji opened this issue May 4, 2024 · 11 comments

Comments

@chihabhajji
Copy link

Description:
When attempting to update an entity that has a PrimaryColumn decorator the update fails because it doesnt include the id of the entity in the mutation setting it to null. Tried using an interceptor to force the payload body to have he ID inferred from the path param but that resulted in a failed mutation too because it counted as a new insert that violates unique constraint.

Steps to Reproduce:
Define an entity with a PrimaryColumn primary key
Attempt to update
Expected Behavior:
Update successful and new entity returned.
Actual Behavior:
A 409 http error code exception is thrown that signals that the row's primary key is null

Screenshots:
image

Environment:
Database: Postgres SQL
Library Version: v0.12.0
Node version: v21.5.0

@chihabhajji
Copy link
Author

another side effect is updating an entity that has a nested entity or a nested entity array that has PrimaryColumn as primary key also doesnt update them

@chihabhajji
Copy link
Author

resolved

@chihabhajji
Copy link
Author

re opening, update still failing, you can try the example of OneToOne in typeorm docs https://orkhan.gitbook.io/typeorm/docs/one-to-one-relations , if you try to update the profile, it always fails because its inserting the userId as null, also tried using params option in the update route, still fails

@jiho-kr
Copy link
Contributor

jiho-kr commented Jun 17, 2024

hi, @chihabhajji

Can you please check if the issue you have is the following?

  • You are trying to change 2 entities in a relation through one api?

if it is true,

The currently implemented CRUD library is focused on an Entity.

You will be able to use it by separating endpoints per entity.

CRUD for relation entities is features to will be implemented in the future. (like #419)

I don't have an immediate need for this feature, but if someone contributes it, I'll include it.

@chihabhajji
Copy link
Author

hello @jiho-kr , thank you for your swift responses as always.
Furthermore, i'm using seperate API's for the entities, here is my code

@Entity({
    name: 'users',
    schema: 'public',
})
export class User extends BaseEntity {
    @Index({ unique: true })
    @PrimaryColumn()
    @IsString({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] })
    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    username!: string;

    @Index({ unique: true })
    @Column()
    @IsEmail(undefined, { groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] })
    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    @ApiProperty({ type: String, description: 'Email of the user', format: 'email' })
    email!: string;

    @ApiHideProperty()
    @Exclude()
    @IsString({
        groups: [GROUP.CREATE],
    })
    @IsOptional({ always: true })
    @Column({
        nullable: false,
    })
    password: string;

    @IsEnum(ERole, { groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] })
    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    @ApiProperty({ enum: ERole, description: 'Role of the user', type: String })
    @Column({
        type: 'enum',
        enum: ERole,
        default: ERole.USER,
        nullable: false,
    })
    role: ERole;

    @Type((_type) => Boolean)
    @ApiProperty({ type: Boolean, description: 'Is the user active' })
    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    @Column({
        type: 'boolean',
        default: true,
        nullable: false,
    })
    isActive!: boolean;

    @ValidateNested({ always: true })
    @Type((_type) => UserProfile)
    @Allow({
        always: true,
    })
    @OneToOne((_type) => UserProfile, (profile) => profile.user, {
        eager: true,
        nullable: false,
        cascade: true,
    })
    @JoinColumn()
    profile: Relation<UserProfile>;
}
@Entity({
    name: 'user_profile',
    schema: 'public',
})
export class UserProfile extends BaseEntity {
    @IsOptional({ groups: [GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] })
    @Allow({ always: true })
    @ApiHideProperty()
    @PrimaryColumn()
    @OneToOne(() => User, (user) => user.profile)
    user: string;

    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    @ApiProperty({ type: String, description: 'User avatar' })
    @IsString({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] })
    @Column({ type: 'varchar', nullable: true, default: null })
    avatar?: string;

    @Column({ nullable: true, default: null })
    @IsString({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] })
    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    lastName?: string;

    @Column({ nullable: true, default: null })
    @IsString({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] })
    @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
    firstName?: string;

    constructor(partial: Partial<UserProfile>) {
        super();
        Object.assign(this, partial);
    }
}
@Crud({
    entity: UserProfile,
    routes: {
        readMany: {
            paginationType: 'offset',
            numberOfTake: 10,
            softDelete: false,
            sort: 'ASC',
            swagger: {
                response: UserProfile,
            },
            relations: false,
        },
        readOne: {
            params: ['user'] as (keyof UserProfile)[],
            swagger: {
                response: UserProfile,
            },
        },
        update: {},
        search: {
            paginationType: 'offset',
            numberOfTake: 10,
            swagger: {
                response: UserProfile,
            },
        },
    },
    only: [Method.READ_MANY, Method.READ_ONE, Method.UPDATE, Method.SEARCH],
})
@Controller('user-profile')
export class UserProfileController {
    // noinspection JSUnusedLocalSymbols
    constructor(private readonly crudService: UserProfileService) {}
}

and when i try to edit a user-profile entity

image

i get 409 error

{
  "query": "INSERT INTO \"public\".\"user_profile\"(\"user\", \"avatar\", \"last_name\", \"first_name\") VALUES (DEFAULT, $1, $2, $3) RETURNING \"avatar\", \"last_name\", \"first_name\"",
  "parameters": [
    "string",
    "string",
    "string"
  ],
  "driverError": {
    "length": 228,
    "name": "error",
    "severity": "ERROR",
    "code": "23502",
    "detail": "Failing row contains (null, string, string, string).",
    "schema": "public",
    "table": "user_profile",
    "column": "user",
    "file": "execMain.c",
    "line": "2003",
    "routine": "ExecConstraints"
  },
  "length": 228,
  "severity": "ERROR",
  "code": "23502",
  "detail": "Failing row contains (null, string, string, string).",
  "schema": "public",
  "table": "user_profile",
  "column": "user",
  "file": "execMain.c",
  "line": "2003",
  "routine": "ExecConstraints"
}

@chihabhajji
Copy link
Author

tried forcing the username by using this interceptor on the update

class A implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler<any>) {
        const r = context.switchToHttp().getRequest();
        console.log(r.params.user);
        if (r.params.user) {
            r.body.user = r.params.user;
        }
        return next.handle();
    }
}

but got the following error: 422
Error: Unprocessable Entity

{
  "message": "Cannot changed value of primary key",
  "error": "Unprocessable Entity",
  "statusCode": 422
}

@JadenKim-dev
Copy link
Contributor

JadenKim-dev commented Jun 18, 2024

Hello!

In UserProfile entity, you have applied both PrimaryColumn and OneToOne to the user property, with the type of user being a string instead of the related entity type.
I think this is not a normal use-case. - official docs
You should separate the relation field and the property field that manages the relationship to ensure TypeORM correctly handles the entity.
Like below...

export class UserProfile extends BaseEntity {
    @PrimaryColumn()
    userProfileName: string;

    @OneToOne(() => User, (user) => user.profile)
    user: User;
}
export class User extends BaseEntity {
    @OneToOne((_type) => UserProfile, (profile) => profile.userProfileName)
    @JoinColumn()
    profile: Relation<UserProfile>;
}

@chihabhajji
Copy link
Author

tried that i get the same error

@JadenKim-dev
Copy link
Contributor

tried that i get the same error

Is the custom interceptor disabled?
When I tested it the first issue, where insert query occurred instead of update, seemed to be resolved.

@chihabhajji
Copy link
Author

yes, i disabled the interceptor, i can do a minimal reproduction repo if you want

@JadenKim-dev
Copy link
Contributor

yes, i disabled the interceptor, i can do a minimal reproduction repo if you want

Yes, please share the modified code example again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants