npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rs-db-seeder

v2.3.1

Published

DB seeder tool for testing

Downloads

6,287

Readme

RS DB Seeder

RS DB Seeder makes it easy to populate database tables for your tests.

Motivation

When you have many dependent tables, it becomes difficult to create test data. Sometimes you need to test the function of changing a user's email address, but you need to create two more dependent tables because the user table has constraints.

dependent tables

In order to create user you have to create project and channel records first.

const { rows: project} = client.query({
  text: 'INSERT INTO projects(name, description) VALUES($1, $2)',
  values: ['Foo Project', 'Bla bla bla'],
})
const { rows: channel} = client.query({
  text: 'INSERT INTO channels(name, project_id) VALUES($1, $2)',
  values: ['dev-hunour', project.id],
})
const { rows: user} = client.query({
  text: 'INSERT INTO users(name, phone, foreign_id, channel_id) VALUES($1, $2)',
  values: ['brianc', '555-555-555', 123 channel.id],
})

rs-db-seeder allows you to do it in just one step

await dbSeeder.insert("user", { name: "john" });

Any missing information(phone, foreign_id fields) will be added and dependent records (channels, projects) will be automatically created behind the scenes.

Getting Started

DB Adapter

rs-db-seeder is framework agnostic. It doesn't use specific ORM - like knex, typeorm, sequelize. So you will need to build your own simple adapter for you application. The adapter implements IStorageWriter and has only one method insert. You can find example Postgres and Mysql adapter implementations here.

Here's a simple knex adapter for pg.

export class KnexPgStorageWriter implements IStorageWriter {
    private knex: Knex;
    constructor(knex: Knex) {
        this.knex = knex;
    }

    insert = async (tableName: string, data: any) => {
        const [result] = await this.knex(tableName).insert(data, "*");
        return {
            ...result,
            ...data,
        };
    };
}

Factories

Now that we have an adapter, we need to configure rs-db-seeder, so that it can generate test data and know about the dependent tables.

seeder.addFactory({
    id: "user", // unique factory ID
    tableName: "users", // table name
    dataProvider: (data: any): any => ({
        // data provider implementation
        id: 99,
        name: "John",
        phone: "55555555",
        foreign_id: 2132323,
        ...data,
    }),
    refs: [
        // identify dependent relations
        ref("channel"), // by default consider users.channel_id => {channel}.id
        // alternatively  you can do  `ref("channel", 'uuid', ch_id)` users.ch_id => {channel}.uuid
    ],
});
seeder.addFactory({
    id: "channel",
    tableName: "channels",
    dataProvider: (data: any): any => ({ name: "channel_1", ...data }),
});

Usage

// configure dbSeeder
const knex = configure();
const storage = new KnexPgStorageWriter(knex);
const dbSeeder = new DbSeeder(storage);

seeder.addFactory({
    id: "user",
    tableName: "users",
    dataProvider: (data: any): any => ({
        name: faker.name.firstName(),
        phone: faker.phone.phoneNumber(),
        ...data,
    }),
    refs: [ref("channel")],
});
seeder.addFactory({
    id: "channel",
    tableName: "channels",
    dataProvider: (data: any): any => ({
        name: faker.random.alpha({ count: 10 }),
        ...data,
    }),
});

// usage in your tests
import dbSeeder from "./configureDbSeeder";

it("updates user email", () => {
    const user = seeder.build("user", { name: "john" });

    const updatedUser = myUserMananger.updateName("tom");
    expect(updatedUser.name).toEqual("tom");
});

API

Build

Build operation allows you to build fake data for your entity. Data is not written to the database. It is somewhat like a faker, it just builds data for the entire entity. Note: data for referenced tables

const data = dbSeeder.build("user", { id: 100 });
// {
//   id: 100,
//   name: 'John',
//   phone: '55555555',
//   foreign_id: 2132323
// }

Note: data for referenced tables is not added. At the same time, no one bothers to add them ourselves.

const data = dbSeeder.build("user", {
    id: 100,
    channel: dbSeeder.build("channel"),
});
// {
//   id: 100,
//   name: 'John',
//   phone: '55555555',
//   foreign_id: 2132323
//   channel: {
//     name: "my channel"
//   }
// }

Insert

dbSeeder.insert - will build and write data to the DB. Note: it's async method. All referenced fields will be built and inserted as well, i.e. we will do 2 inserts into (ref) channels and users

const data = await dbSeeder.insert("user", { id: 100 });
// {
//    id: 100,
//    name: 'John',
//    phone: '55555555',
//    channel_id: 60,            // channel with ID = 60 has created
//    foreign_id: 2132323
//  }

Insert Many

dbSeeder.insertMany inserts multiple records

const [channel1, channel2] = await seeder.insertMany(2, "channel", {
    name: "channel_1",
});

inserts 2 channel records with name: channel_1

References

If you created a dependent entity before, pass it as a simple column value or as an object.

const channel = dbSeeder.build("channel");
const user1 = dbSeeder.build("user", { id: 100, channel_id: channel.id });
const user2 = dbSeeder.build("user", { id: 100, channel });

if you pass data as an object

const data = dbSeeder.build("user", {
    id: 100,
    channel: { name: "my channel" },
});

{ name: "my channel"} will be passed to channel insert method.

if you pass data as an object and an id field is specified, the dependent record will not be inserted.

const data = dbSeeder.build("user", { id: 100, channel: { id: 123 } });

Custom insert implementation

It's possible to provide custom insert implementation

seeder.addFactory({
    id: "user",
    tableName: "users",
    dataProvider: (data): any => ({
        id: 99,
        name: "John",
        phone: "55555555",
        foreign_id: 2132323,
        ...data,
    }),
    insert: async (data: any) => {
        const [user] = await knex("users")
            .insert(data, "*")
            .onConflict("foreign_id")
            .merge();
        return user;
    },
});

Clean up

The simplest and most efficient approach is to use uncommitted transactions. We insert data, do our tests, then inserted data is erased because the transaction is rolled back.

// use transaction instead if clean up
beforeEach(async () => {
    await knex.raw("BEGIN");
});
afterEach(async () => {
    await knex.raw("ROLLBACK");
});

it("insert data - simple case", async () => {
    const data = await seeder.insert("channel");
    ...
});

Why Seeder does not delete data automatically

Let's say you are testing the creation of an article. The article must have an author. (creator_id integer constraint article_users_id_fk references users)

afterEach(async () => {
    await dbSeeder.cleanup(); // this method doesn't not exist, see explanation below
});

const author = seeder.insert("author");
const article = postManager.addPost(...data, author);

Seeder knows that it inserted an author record. It doesn't know about article record. It has been added by postManager. If we try to delete data inserted by the Seeder we will delete only the author record and it leads to constraint violation since an article must have an author.

To avoid such conflicts, we abandoned the idea of automatic data deletion.

Advanced usage

Typings

type Factories = "users" | "channels";

const seeder: Seeder<Factories> = new DbSeeder(storage);
seeder.addFactory({
    id: "users",
    tableName: "users",
});
seeder.build("users");
seeder.insert("users");

// TS will signal errors
seeder.insert("usr");
seeder.build("usr");
seeder.addFactory({
    id: "user",
    tableName: "users",
});

Scenarios

For non-trivial cases, you can use scripts. Let's say you need to create records using transactions.

class AdvancedKnexPgStorageWriter extends KnexPgStorageWriter {
    getKnex() {
        return this.knex;
    }
}

const knex = getKnexPgClient();
const storage = new AdvancedKnexPgStorageWriter(knex);
const seeder: Seeder<"userWithChannel"> = new DbSeeder(storage);

seeder.addScenario({
    id: "userWithChannel",
    insert: async (
        storage: AdvancedKnexPgStorageWriter,
        data: any
    ): Promise<any> => {
        const knex = storage.getKnex();
        knex.raw("BEGIN");

        const channel = await storage.insert("channels", {
            name: data.channel.name,
        });

        const user = await storage.insert("users", {
            name: "John",
            phone: "55555555",
            foreign_id: randNumber(),
            channel_id: channel.id,
        });
        knex.raw("COMMIT");
        return { user, channel };
    },
});

Then you can use it in the same way as you use factories.

const { user, channel } = await seeder.insert("userWithChannel", {
    channel: { name: "Woh channel" },
});

Circular Table Dependencies

In case you have tables with circular dependencies

users(id, channel_id)    -> channel_id -> channels.od
channels(id, project_id) -> project_id -> projects.id
project(id, creator_id)  -> creator_id -> users.id

At least one table has nullable reference. Make sure you break the chain explicitly providing entity with nullable field.

const channel = await seeder.insert("channel", { project_id: null });
const user = await seeder.insert("user", { channel });