wwwouter / typed-knex

A TypeScript wrapper for Knex.js

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Problem with insert

Rwanito opened this issue · comments

Issue type:

[X] Question
[ ] Bug report
[ ] Feature request
[ ] Documentation issue

Database system/driver:

[ ] Postgres
[ ] MSSQL
[X] MySQL
[ ] MariaDB
[ ] SQLite3
[ ] Oracle
[ ] Amazon Redshift

typed-knex version:

[X latest
[ ] @next
[ ] 0.x.x (or put your version here)

Knex.js version:
Latest

Steps to reproduce or a small repository showing the problem:

Hi !
I want to make a simple insertion with two models.

import { Column, Table } from '@wwwouter/typed-knex';

import { Email, UUID } from '../../ts/types/base.type';

@Table('user')
class UserDB {
  @Column({ primary: true })
  public id!: UUID;

  @Column()
  public firstname!: string;

  @Column()
  public lastname!: string;

  @Column()
  public email!: Email;

  @Column()
  public birthdate!: Date;

  @Column()
  public gender: 'M' | 'F' | 'N' = 'N';

  @Column()
  public created_at!: Date;

  @Column()
  public updated_at!: Date;

  @Column()
  public termsofuse: boolean = true;

  @Column()
  public active: boolean = true;
}

export default UserDB;
import { Column, Table } from '@wwwouter/typed-knex';

import User from './User.db.model';
import { Bcrypt, UUID } from '../../ts/types/base.type';

@Table('password')
class PasswordDB {
  @Column({ primary: true })
  public id!: UUID;

  @Column()
  public password!: Bcrypt;

  @Column({ name: 'user_id' })
  public user!: User;

  @Column()
  public created_at!: Date;

  @Column()
  public updated_at!: Date;
}

export default PasswordDB;

I just run :

await db.query(UserDB).insertItem(userDB);
await db.query(GroupDB).insertItem(groupDB);

userDB and groupDB are instances of UserDB and GroupDB. They are full and there is no misfiled attributes.

And I got :

Error: insert into `group` (`group_name`, `user_id`) values (0, {"gender":"N","termsofuse":true,"active":true,"id":"6dc9f9f9-ae34-4eef-8a33-6f514ec05c6e","firstname":"string","lastname":"string","email":"user@example.com","birthdate":"2020-10-08T13:51:50.489Z","created_at":"2020-10-08 16:11:18","updated_at":"2020-10-08 16:11:18"}) - Column count doesn't match value count at row 1
        at Packet.asError (/........node_modules/mysql2/lib/packets/packet.js:712:17)
        at Query.execute (......../node_modules/mysql2/lib/commands/command.js:28:26)
        at Connection.handlePacket (/.........node_modules/mysql2/lib/connection.js:425:32)
        at PacketParser.onPacket (/h........node_modules/mysql2/lib/connection.js:75:12)
        at PacketParser.executeStart (/.........../node_modules/mysql2/lib/packet_parser.js:75:16)
        at Socket.<anonymous> (/............./node_modules/mysql2/lib/connection.js:82:25)
        at Socket.emit (events.js:314:20)
        at Socket.EventEmitter.emit (domain.js:486:12)
        at addChunk (_stream_readable.js:307:12)
        at readableAddChunk (_stream_readable.js:282:9)

What can I do, to avoid to put the user object instead of just user.id ?

Thanks for your work !

Can you share GroupDB and the code where you set the properties of groupDB?

Here my GroupDB

import { Column, Table } from '@wwwouter/typed-knex';

import User from './User.db.model';

export enum TypeGroup {
  Student,
  Teacher,
  SuperTeacher,
  Admin
}

@Table('group')
class GroupDB {
  @Column({ name: 'user_id' })
  public user!: User;

  @Column()
  public group_name!: TypeGroup;
}

export default GroupDB;

And, here how I create my GroupDB

import GroupDB, { TypeGroup } from './db/Group.db.model';
import User from './User.model';

class Group implements GroupDB {
  public user: User;

  public group_name: TypeGroup;

  public constructor(groupCreationParams: { user: User, group_name: TypeGroup }) {
    this.user = groupCreationParams.user;
    this.group_name = groupCreationParams.group_name;
  }

  // /**
  //  * Recupère l'instance GroupDB pour permettre une gestion en base de données.
  //  */
  // private getGroupDB(): GroupDB {
  //   /**
  //    * Possibilité de destructurer l'objet puisque User implemente GroupDB.
  //    */
  //   return { ...this } as GroupDB;
  // }
}

And How I set User :

import { v4 as uuidv4 } from 'uuid';

import { Email, UUID } from '../ts/types/base.type';
import UserDB from './db/User.db.model';
import db from '../libs/db.lib';
import { UserCreationParams } from '../ts/types/params.type';

class User implements UserDB {
  public id: UUID;

  public firstname: string;

  public lastname: string;

  public email: Email;

  public birthdate: Date;

  public gender: 'M' | 'F' | 'N' = 'N';

  public created_at!: Date;

  public updated_at!: Date;

  public termsofuse: boolean = true;

  public active: boolean = true;

  public constructor(userCreationParams: UserCreationParams) {
    this.id = uuidv4();
    this.firstname = userCreationParams.firstname;
    this.lastname = userCreationParams.lastname;
    this.email = userCreationParams.email;
    this.birthdate = userCreationParams.birthdate;
    this.gender = userCreationParams.gender;
  }

  // /**
  //  * Recupère l'instance UserDB pour permettre une gestion en base de données.
  //  */
  // private getUserDB(): UserDB {
  //   /**
  //    * Possibilité de destructurer l'objet puisque User implemente UserDB.
  //    */
  //   return { ...this } as UserDB;
  // }

  // public static async getUserById(id: UUID): Promise<User> {
  //   return db.query(UserDB).where((i) => i.id, id).getFirst()
  //     .then((userDB) => new User(userDB));
  // }

  // public async insertUser(): Promise<User> {
  //   return db.query(UserDB).insertItem(this.getUserDB())
  //     .then(() => this);
  // }
}

export default User;

To set UserDB or GroupDB, I just do : { ...this } as GroupDB or UserDB, because my class implements the model.
It is okay to do like that ?

So to insert GroupDB or UserDB, I do db.query(UserDB).insertItem(myUserFromUserClass.getUserDB())

Thanks you for your reply !

Looking at the error message, it seems like you're inserting a UserDB object into a .query<GroupDB> query.

Personally I stopped using a class like User some time ago. Storing the user data and providing access to the database in one class breaks the single responsibility principle. Now I always use a repository for data access, or just use typed-knex directly in a controller..

Hi !
I write UUID instead of UserDB because it doesn't work.
However, it's the good class I have.

I've tried with only my interface and I have the same result.

You right, I will write a specific user model !
Thanks a lot !

Hi !
I have always the same issue.

In my SQL, I use knex to migrate.

  await knex.schema.createTable(tableUser, (table) => {
    table.uuid('id').notNullable().primary();
    table.string('firstname').notNullable();
    table.string('lastname').notNullable();
    table.string('email').notNullable().unique();
    table.date('birthdate').notNullable();
    table.string('gender', 1).notNullable().defaultTo('N');
    table.boolean('active').notNullable().defaultTo(true);
    table.boolean('termsofuse').notNullable().defaultTo(true);
    table
      .timestamp('created_at')
      .notNullable()
      .defaultTo(knex.raw('CURRENT_TIMESTAMP'));

    table
      .timestamp('updated_at')
      .notNullable()
      .defaultTo(
        knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
      );
  });

  /**
   * Création de la table group et group_user
   */
  await knex.schema.createTable(tableGroup, (table) => {
    // Table
    table.integer('group_name').notNullable();
    table.uuid('user_id').notNullable();

    // Clefs
    table.foreign('user_id').references('user.id');

import { Column, Table } from '@wwwouter/typed-knex';
import { Email, GenderType, UUID } from '../../types/base.type';

@Table('user')
class UserInDB {
  @Column({ primary: true })
  id!: UUID;

  @Column()
  firstname!: string;

  @Column()
  lastname!: string;

  @Column()
  email!: Email;

  @Column()
  birthdate!: Date;

  @Column()
  gender!: GenderType;

  @Column()
  created_at!: Date;

  @Column()
  updated_at!: Date;

  @Column()
  termsofuse!: boolean;

  @Column()
  active!: boolean;
}

export default UserInDB;

and

import { Column, Table } from '@wwwouter/typed-knex';
import { TypeGroup } from '../../types/base.type';
import UserInDB from './user.class';

/**
 * Définition de Group dans la base de données.
 */
@Table('group')
class GroupInDB {
  @Column({ name: 'user_id' })
  user!: UserInDB;

  @Column({ primary: true })
  group_name!: TypeGroup;
}

export default GroupInDB;

When I try a simple insert :

    const user = new UserInDB();
    Object.assign(user, {
      id: 'test',
      active: true,
      birthdate: new Date(),
      created_at: new Date(),
      email: 'test@test.com',
      firstname: 'ok',
      gender: GenderType.MALE,
      lastname: 'ok',
      termsofuse: true,
      updated_at: new Date(),
    });

    const group = new GroupInDB();
    Object.assign(group, {
      user,
      group_name: TypeGroup.Student,
    });

    await typedKnex.query(UserInDB).insertItem(user);
    await typedKnex.query(GroupInDB).insertItem(group);

I have

insert into `group` (`group_name`, `user_id`) values (0, {"id":"test","active":true,"birthdate":"2020-10-17T10:56:34.425Z","created_at":"2020-10-17T10:56:34.425Z","email":"test@test.com","firstname":"ok","gender":"M","lastname":"ok","termsofuse":true,"updated_at":"2020-10-17T10:56:34.425Z"}) - Column count doesn't match value count at row 1
    Error: insert into `group` (`group_name`, `user_id`) values (0, {"id":"test","active":true,"birthdate":"2020-10-17T10:56:34.425Z","created_at":"2020-10-17T10:56:34.425Z","email":"test@test.com","firstname":"ok","gender":"M","lastname":"ok","termsofuse":true,"updated_at":"2020-10-17T10:56:34.425Z"}) - Column count doesn't match value count at row 1
        at Packet.asError (/home/erwan//-api/node_modules/mysql2/lib/packets/packet.js:712:17)
        at Query.execute (/home/erwan//-api/node_modules/mysql2/lib/commands/command.js:28:26)
        at Connection.handlePacket (/home/erwan//-api/node_modules/mysql2/lib/connection.js:425:32)
        at PacketParser.onPacket (/home/erwan//-api/node_modules/mysql2/lib/connection.js:75:12)
        at PacketParser.executeStart (/home/erwan//-api/node_modules/mysql2/lib/packet_parser.js:75:16)
        at Socket.<anonymous> (/home/erwan//-api/node_modules/mysql2/lib/connection.js:82:25)
        at Socket.emit (events.js:314:20)
        at Socket.EventEmitter.emit (domain.js:486:12)
        at addChunk (_stream_readable.js:307:12)
        at readableAddChunk (_stream_readable.js:282:9)

User is inserted, but not Group.

Did I miss anything?
Thanks you !

Hi,

If you'd log the group object, you should see what's going wrong.

console.log(group)
 await typedKnex.query(GroupInDB).insertItem(group);

group should only contain the fields you want to insert in the database, so user_id and group_name.
Right now the Object.assign puts too much data in the group object.

I think something like this should work:

@Table('group')
class GroupInDB {
  @Column()
  user_id!: string;  // always add the real column

  @Column({ name: 'user_id' })
  user!: UserInDB;

  @Column({ primary: true })
  group_name!: number; // I would keep this type as close to the database type as possible
}
const user = new UserInDB();
    Object.assign(user, {
      id: 'test',
      active: true,
      birthdate: new Date(),
      created_at: new Date(),
      email: 'test@test.com',
      firstname: 'ok',
      gender: GenderType.MALE,
      lastname: 'ok',
      termsofuse: true,
      updated_at: new Date(),
    });

    const group = new GroupInDB();
    Object.assign(group, {
      user_id: user.id,
      group_name: TypeGroup.Student,
    });

    await typedKnex.query(UserInDB).insertItem(user);
    await typedKnex.query(GroupInDB).insertItem(group);

Thanks for you reply !
It works !

But, now I have another problem.
I want to make a join.

Password contains the id from user. I want to get results joined with this id.

In UserDAO

  /**
   * Renvoie vrai si le duo Email+Password correspond.
   * @param {LoginParams} loginParams
   */
  public static async checkLogin(_loginParams: LoginParams): Promise<boolean> {
    const test = await typedKnex.query(PasswordInDB).innerJoinColumn((i) => i.user_id).getMany();
    console.log(test);
    return true;
  }

The error is :
TypeError: Cannot read property 'tableName' of undefined at TypedQueryBuilder.joinColumn (/home/erwan/astronef/astronef-api/node_modules/@wwwouter/typed-knex/src/typedKnex.ts:1876:68) at TypedQueryBuilder.innerJoinColumn (/home/erwan/astronef/astronef-api/node_modules/@wwwouter/typed-knex/src/typedKnex.ts:1130:21) at Function.<anonymous> (/home/erwan/astronef/astronef-api/src/models/userDAO.model.ts:48:54)

PasswordInDB

@Table('password')
class PasswordInDB {
  @Column({ primary: true })
  id!: string;

  @Column()
  password!: string;

  @Column()
  user_id!: string;

  @Column({ name: 'user_id' })
  user!: UserInDB;

  @Column()
  created_at!: Date;

  @Column()
  updated_at!: Date;
}

And UserInDB

@Table('user')
class UserInDB {
  @Column({ primary: true })
  id!: string;

  @Column()
  firstname!: string;

  @Column()
  lastname!: string;

  @Column()
  email!: string;

  @Column()
  birthdate!: Date;

  @Column()
  gender!: string;

  @Column()
  created_at!: Date;

  @Column()
  updated_at!: Date;

  @Column()
  termsofuse!: boolean;

  @Column()
  active!: boolean;
}

Did I do something wrong ?
Thanks a lot !

You must use property with the UserInDB type, so: const test = await typedKnex.query(PasswordInDB).innerJoinColumn((i) => i.user).getMany();

In the next version that I'm working on, there is a compilation check to make sure that your previous code doesn't compile.

(BTW, it looks like you're storing a plain text password. I would highly advise against that)

Okay thanks a lot !
Thanks for your work !

It's a bcrypt hash, I compare the new hash from the db request !

It's a bcrypt hash, I compare the new hash from the db request !

👍