w3tecch / typeorm-seeding

🌱 A delightful way to seed test data into your database.
https://www.npmjs.com/package/typeorm-seeding
MIT License
877 stars 131 forks source link

Deeply saving factory #209

Open matiasgarcia opened 2 years ago

matiasgarcia commented 2 years ago

I have the following factories:

question.factory.ts

import Faker from '@faker-js/faker';
import { define, factory } from 'typeorm-seeding';
import { Answer } from '../../entities/answer.entity';
import { Question } from '../../entities/question.entity';

define(Question, (faker: typeof Faker) => {
  const question = new Question();
  question.question = faker.lorem.sentence() + '?';
  question.answers = factory(Answer)().makeMany(5) as any;
  question.private = faker.random.boolean();
  return question;
});

answer.factory.ts

import Faker from '@faker-js/faker';
import { define, factory } from 'typeorm-seeding';
import { Answer } from '../../entities/answer.entity';
import { User } from '../../entities/user.entity';

define(Answer, (faker: typeof Faker) => {
  const answer = new Answer();
  answer.answerer = factory(User)() as any;
  answer.answer = faker.lorem.sentence();
  return answer;
});

user.factory.ts

import Faker from '@faker-js/faker';
import { define } from 'typeorm-seeding';
import { User } from '../../entities/user.entity';

define(User, (faker: typeof Faker) => {
  const user = new User();
  const firstName = faker.name.firstName();
  const lastName = faker.name.lastName();
  user.name = `${firstName} ${lastName}`;
  return user;
});

And also I have a seed:

import { Factory, Seeder } from 'typeorm-seeding';
import { Connection } from 'typeorm';
import { Question } from '../../entities/question.entity';

export default class InitialDatabaseSeed implements Seeder {
  public async run(factory: Factory, connection: Connection): Promise<void> {
    const questions = await factory(Question)().createMany(15);
    console.log({ questions });
  }
}

When I run this, only the Questions records are stored, the Answers and User are ignored.

How can I persist them?

jorgebodega commented 2 years ago

This is related on how the related entities are being processed:

  if (isPromiseLike(entity[attribute])) {
    entity[attribute] = await entity[attribute]
  }
if (isSeeding) {
    entity[attribute] = await (subEntityFactory as any).create()
} else {
    entity[attribute] = await (subEntityFactory as any).make()
}

In your definition, Question is using makeMany to create Answers[], that uses the first code snippet, not the second one, and make operations does not persist info in database. And then, in Answer definition, a User is created using the second code snippet, but in this case, coming from makeMany operation, isSeeding is false.

The only way this could work is changing that makeMany to createMany or relating all the entities with cascade (I do not like to work with cascades on ORMs)

Also, please try to avoid using @faker-js/faker to type the faker param. They are completely different packages, and this could lead to some troubles. I recommend to use either one of the following:

import faker from '@faker-js/faker';

define(Answer, () => {
  const answer = new Answer();
  ...
  answer.answer = faker.lorem.sentence();
  ...
});
import Faker from 'faker';

define(Answer, (faker: typeof Faker) => {
  const answer = new Answer();
  ...
  answer.answer = faker.lorem.sentence();
  ...
});
matiasgarcia commented 2 years ago

@jorgebodega Thanks for taking your time to answer.

I am wondering if there is any way to force the persistence of the Entity and its relations at the moment of running the seed and not before. I am trying to avoid forcing my factories to introduce side effects in the database until its strictly necessary (this is, replacing the make() calls with create()). Also I dont want to modify the Entity schema to have cascade: true.

Is that possible?

jorgebodega commented 2 years ago

I'm not understanding your problem, could you try to explain with code?

matiasgarcia commented 2 years ago

@jorgebodega Let's assume that I don't change the factories. I want them to NOT store values in the database. Just build the entities and its associations, nothing else.

However, in the seed, I would like to call my factory and tell it "save the entity and all its related records".

import { Factory, Seeder } from 'typeorm-seeding';
import { Connection } from 'typeorm';
import { Question } from '../../entities/question.entity';

export default class InitialDatabaseSeed implements Seeder {
  public async run(factory: Factory, connection: Connection): Promise<void> {
    const questions = await factory(Question)().createMany(15); // Is there a way to tell the ORM, save the Question and its associations?
  }
}

I would like to avoid adding cascade: true to the Entity, because that's a global side-effect I would like to avoid.

matiasgarcia commented 2 years ago

I come from a Rails background, heavily using this factory gem called FactoryBot so maybe this is not possible in TypeORM nor with this lib.

jorgebodega commented 2 years ago

Hi, sorry about the delay, let me check this in the next few days and if is already fixed on the forked package

matiasgarcia commented 2 years ago

@jorgebodega no worries.

Just to clarify, in my codebase I temporarily replaced

define(Answer, (faker: typeof Faker) => {
  const answer = new Answer();
  answer.answerer = factory(User)() as any;
  answer.answer = faker.lorem.sentence();
  return answer;
});

with

  answer.answerer = factory(User)().create()

But I think it would be ideal that rather than doing that, I can just do

factory(Answer)().createWithAssociations()

And that somehow "hacks" TypeORM to store the associations first rather than being forced to add cascade: true to the Entity.

As mentioned, I am taking inspiration on https://github.com/thoughtbot/factory_bot that uses Rails' ActiveRecord and maybe this is something that cannot be done with TypeORM or requires a lot of work. I am a newbie in the JS backend world but maybe if you could guide me a little bit on what this would require I can try to make a proof of concept.