graine
TypeScript icon, indicating that this package has built-in type declarations

1.0.1 • Public • Published

Graine

Graine is a versatile Node.js tool that simplifies the process of populating databases with test data. It allows you to generate structured and realistic data, making it an essential tool for testing and development.

Buy Me A Coffee

Features

  • Hierarchical Data Generation: Graine can create complex, nested data structures with ease, including one-to-one, one-to-many, many-to-many relationships, and even deep hierarchies.
  • Customizable Data Templates: You can tailor the generated data to match your database schema requirements, ensuring accurate testing scenarios.
  • Efficiency and Speed: Graine is designed for efficiency and speed, making it suitable for projects with large-scale data seeding needs.
  • Cross-Framework Compatibility: It can seamlessly integrate with various databases, ORMs, and frameworks, making it a versatile choice for a wide range of projects.

Getting Started

To get started with Graine, follow these simple steps:

Installation

You can install Graine using npm or yarn:

npm install -D graine

Usage

This example demonstrates seeding data for user(s) and channel(s), since having a user requires a channel, we use refs to define the relationship between the two and the seeder will automatically handle the relationship. You can configure factories according to your project's needs.

import Graine, { SeederFactory, ISeederWriter } from 'graine';
import { faker } from '@faker-js/faker';

class MyDatabaseWriter implements ISeederWriter {
  async insert(tableName: string, primaryKey: string, data: object): Promise<number> {
    // insert data into the database...
  }

  async cleanUp(tables?: string[]): Promise<void> {
    // clean up the database... should clean up all tables if no tables are specified
  }
}

Graine.setWriter(new MyDatabaseWriter());

class UserFactory extends SeederFactory {
  name = 'user';
  tableName = 'users';
  primaryKey = 'userID';
  
  get refs() {
    return [
      ref({ 
        factoryName: 'channel', // reference to the channel factory, which is defined below
        foreignKey: 'channelID'
      }) // one-to-many relationship with a foreign key
    ];
  }

  provider(args, context) {
    return {
      name: faker.person.firstName(),
      phone: faker.phone.imei(),
      age: faker.number.int({ min: 18, max: 60 }),
    };
  }
}

class ChannelFactory extends SeederFactory {
  name = 'channel';
  tableName = 'channels';
  primaryKey = 'channelID';

  provider(args, context) {
    return {
      name: faker.word.noun(),
    };
  }
}

Graine.register(new UserFactory());
Graine.register(new ChannelFactory());

// Seed multiple users, with the same channel
const channelID = await Graine.seed('channel',  { name: 'General Channel' });
await Graine.seedMany('user', { count: 2, args: { channelID } });

// Seed multiple users, with the different channels
await Graine.seedMany('user', { count: 2 });

// Seed multiple users, with the same channel
await Graine.seedMany('user', { count: 2, resuseRefs: false });

// Clean up
Graine.cleanUp('users', 'channels');

// Clean up all factories?
Graine.cleanUp();

Showcase (Messaging App)

This example demonstrates a messaging app scenario, where we have users, channels, channel users, and messages. We use refs to define the relationships between the factories, and the seeder will automatically handle the relationships.

class UserFactory extends SeederFactory {
  name = 'user';
  tableName = 'users';
  primaryKey = 'userID';

  provider(args, context) {
    return {
      name: args.name ?? faker.person.firstName(),
      phone: args.phone ?? faker.phone.imei(),
      age: args.age ?? faker.number.int({ min: 18, max: 60 }),
    };
  }
}

class ChannelFactory extends SeederFactory {
  name = 'channel';
  tableName = 'channels';
  primaryKey = 'channelID';

    get refs() {
    return [
      ref({ 
        factoryName: 'user',
        foreignKey: 'createdBy'
      })
    ];
  }

  provider(args, context) {
    return {
      name: args.name ?? faker.word.noun(),
    };
  }

  after(args, context, seeder) {
    // after creating a channel, we also want to add the creator as a channel user
    return seeder.seed('channel_user', { channelID: context.channel, userID: context.user || args.userID });
  }
}

class ChannelUserFactory extends SeederFactory {
  name = 'channel_user';
  tableName = 'channel_user';
  primaryKey = 'id';

  get refs() {
    return [
      ref({ 
        factoryName: 'channel',
        foreignKey: 'channelID'
      }),
      ref({ 
        factoryName: 'user',
        foreignKey: 'userID'
      })
    ];
  }

  provider(args, context) {
    return {
      joinedAt: args.joinedAt ?? faker.date.recent(),
    };
  }
}

class MessageFactory extends SeederFactory {
  name = 'message';
  tableName = 'messages';
  primaryKey = 'messageID';

  get refs() {
    return [
      ref({ 
        factoryName: 'user',
        foreignKey: 'sentBy'
      }),
      ref({ 
        factoryName: 'channel',
        foreignKey: 'channelID'
      })
    ];
  }

  provider(args, context) {
    return {
      content: args.content ?? faker.lorem.sentence(),
      sentAt: args.sentAt ?? faker.date.recent(),
    };
  }
}

Graine.register(new UserFactory());
Graine.register(new ChannelFactory());
Graine.register(new ChannelUserFactory());
Graine.register(new MessageFactory());

describe('Messaging', () => {
  afterEach(() => {
    Graine.cleanUp();
  });

  it('should allow channel owner to send messages', async () => {
    // This seeds a channel, and a user who is the owner of the channel.
    // The channel user is automatically created by the ChannelFactory's after hook
    const [,, context] = await Graine.seed('channel', { name: 'General' });

    const subject = MessagineService.sendMessage({
      content: 'Hello, World!',
      sentBy: context.user.userID,
      channelID: context.channel.channelID,
    });

    await expect(subject).resolves.toEqual(expect.objectContaining({ status: 'success' }));
  });

  it('should not allow non-channel users to send messages', async () => {
    const [,, context] = await Graine.seed('channel', { name: 'General' }); 
    const nonChannelUser = await Graine.seedObject('user');

    const subject = MessagineService.sendMessage({
      content: 'Hello, World!',
      sentBy: nonChannelUser.userID,
      channelID: context.channel.channelID,
    });

    await expect(subject).rejects.toThrowError('User is not a member of the channel');
  });
});

Tests

Graine is thoroughly tested to ensure its reliability and functionality. You can find various test cases in the test folder, covering scenarios like one-to-one, one-to-many, many-to-many relationships, and deep hierarchies.

Contributions

We welcome contributions from the community. If you have any improvements, bug fixes, or new features to add, please open a pull request on our GitHub repository.

License

Graine is licensed under the MIT License. You can find the full license details in the LICENSE file.

Happy seeding!

Package Sidebar

Install

npm i graine

Weekly Downloads

10

Version

1.0.1

License

MIT

Unpacked Size

56.7 kB

Total Files

40

Last publish

Collaborators

  • halimsamy