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 🙏

© 2026 – Pkg Stats / Ryan Hefner

graine

v1.0.4

Published

Efficiently populating databases with hierarchical and customizable test data

Readme

Graine

Graine is a flexible data seeding library for TypeScript/JavaScript projects. It allows you to define factories for your data models, manage relationships, and seed complex data structures into any database or in-memory store. Graine is ideal for testing, prototyping, and demo environments.

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

API Reference

Core Concepts

Factory

A factory defines how to generate data for a model/table. It includes a provider function (for generating data) and references to other factories.

Ref

References (ref) define relationships between factories, such as foreign keys.

Writer

A writer handles how and where data is stored. You can implement your own (e.g., for in-memory, SQL, etc.).


ISeederWriter

Interface

The ISeederWriter interface defines the contract for custom writers that handle data storage for seeded records.

export default interface ISeederWriter {
  insert: (tableName: string, primaryKey: string, data: object) => Promise<number>;
  cleanUp: (tables?: string[]) => Promise<void>;
}
  • insert:
    Inserts a record into the specified table, assigning a primary key.
    Returns the primary key value (usually a number).

  • cleanUp:
    Removes all records from the specified tables, or from all tables if none are specified.

Example Implementation:

class InMemoryDatabaseWriter implements ISeederWriter {
  public database: any = {};

  async insert(tableName: string, primaryKey: string, data: object): Promise<number> {
    if (!this.database[tableName]) this.database[tableName] = [];
    const id = this.database[tableName].length + 1;
    this.database[tableName].push({ [primaryKey]: id, ...data });
    return id;
  }

  async cleanUp(tables?: string[]): Promise<void> {
    if (!tables) {
      this.database = {};
      return;
    }
    for (const table of tables) {
      this.database[table] = [];
    }
  }
}

Graine.setWriter(new InMemoryDatabaseWriter());

ref

ref({ factoryName: string, foreignKey: string, optional?: boolean }): SeederRef

Defines a reference (relationship) between factories.

  • Parameters:

    • factoryName: Name of the referenced factory.
    • foreignKey: Foreign key field in the current factory.
    • optional: Whether the reference is optional (default: false).
  • Returns: SeederRef object.

  • Example:

    ref({ factoryName: "team", foreignKey: "teamID" });
  • Note: If the reference is optional, Graine will not seed the related record unless it is explicitly provided in the seed call.


SeederFactory

Defines a factory for seeding data into a specific model/table.

  • Properties:

    • name: Name of the factory.
    • tableName: Name of the database table.
    • primaryKey: Name of the primary key field.
    • refs: Array of references to other factories.
    • before: Optional hook to modify data before insertion.
    • after: Optional hook to modify data after insertion.
  • Example:

    class UserFactory extends SeederFactory {
      name = 'user';
      tableName = 'users';
      primaryKey = 'userID';
    
      refs() {
        return [ref({ factoryName: "team", foreignKey: "teamID" })];
      }
        
      before(args, context, seeder) {
        // ...
      }
    
      provider(args) {
        return {
          name: args.name ?? "Default",
        };
      }
    
      after(args, context, seeder) {
        // ...
        // Might be used to seed related data after this factory
      }
    }
What is args?

args is an object containing the arguments passed to the factory's provider function. It can include any data needed to generate the model instance.

What is context?

See below.


register(...factories: SeederFactory[]): void

Registers a factory for a model/table.

  • Parameters:

    • factories: Array of objects describing the factory (name, tableName, primaryKey, provider, refs). Could be a class extending SeederFactory.
  • Example 1:

    Graine.register({
      name: "user",
      tableName: "users",
      primaryKey: "userID",
      provider: (args) => ({ name: args.name ?? "Default" }),
      refs: [ref({ factoryName: "team", foreignKey: "teamID" })],
    });
  • Example 2:

    Graine.register(new UserFactory(), new TeamFactory());

seed(factoryName: string, args?: object, reuseRefs?: boolean): Promise<[id, record, context]>

Seeds a single record for the given factory.

  • Parameters:

    • factoryName: Name of the registered factory.
    • args: Optional overrides for the provider.
    • reuseRefs: Whether to reuse existing references (default: true).
  • Returns: Promise resolving to [id, record, context]:

    • id: Primary key value of the created record.
    • record: The created record object.
    • context: Context object containing references and related data.
What is context?

The context object contains references to all related records created or reused during the seeding process for a given factory. The keys in context correspond to the factory names used in the references (refs). Each value is the seeded record for that factory.

Example:

Suppose you have the following factories:

Graine.register({
  name: "organization",
  tableName: "organizations",
  primaryKey: "organizationID",
  provider: (args) => ({ name: args.name ?? "Default Org" }),
  refs: [],
});

Graine.register({
  name: "team",
  tableName: "teams",
  primaryKey: "teamID",
  provider: (args) => ({ teamName: args.teamName ?? "Alpha" }),
  refs: [ref({ factoryName: "organization", foreignKey: "organizationID" })],
});

Graine.register({
  name: "channel",
  tableName: "channels",
  primaryKey: "channelID",
  provider: (args) => ({ channelName: args.channelName ?? "General" }),
  refs: [ref({ factoryName: "team", foreignKey: "teamID" })],
});

When you seed a channel:

const [channelID, channel, context] = await Graine.seed("channel", { channelName: "Announcements" });

The context object will look like:

{
  team: {
    teamID: ...,
    teamName: ...,
    organizationID: ...,
  },
  organization: {
    organizationID: ...,
    name: ...,
  }
}
  • context.team contains the seeded team record, which itself references the organization.
  • context.organization contains the seeded organization record.

You can use context.team.teamID and context.organization.organizationID to access the seeded IDs, which are also set as foreign keys in the channel and team records, respectively.

This makes it easy to chain and relate seeded data across multiple factories, assert relationships, and pass correct foreign keys when seeding dependent records.


seedObject(factoryName: string, args?: object, reuseRefs?: boolean): Promise<record>

A shorthand method for seeding a single object. It internally calls seed and returns just the record. Use if you don't care about the context.

Example:

const user = await Graine.seedObject("user", { name: "John Doe" });

seedMany(factoryName: string, options: { count: number, reuseRefs?: boolean, args?: object }): Promise<Array<[id, record, context]>>

Seeds multiple records for the given factory.

  • Parameters:

    • factoryName: Name of the registered factory.
    • options:
      • count: Number of records to create.
      • reuseRefs: If true, reuse references for all records.
      • args: Optional overrides for the provider.
  • Returns: Promise resolving to an array of [id, record, context].

Behavior of reuseRefs
  • If reuseRefs: true, all seeded records share the same referenced objects (e.g., all channels share the same team and organization).
  • If reuseRefs: false, each seeded record gets its own referenced objects (e.g., each channel gets a unique team, and each team gets a unique organization).

Examples

Example 1: reuseRefs: false

const results = await Graine.seedMany("channel", { count: 3, reuseRefs: false });
for (const [channelID, channel, context] of results) {
  // context.team is the unique team for this channel
  // context.organization is the unique organization for this team
  // channel.teamID === context.team.teamID
  // context.team.organizationID === context.organization.organizationID
}
  • Each channel gets its own team, and each team gets its own organization.
  • No teams or organizations are shared between channels.
  • Each context object for each channel contains the unique related records for that channel, allowing you to access and assert relationships for each seeded entity independently.

Example 2: reuseRefs: true

const results = await Graine.seedMany("channel", { count: 3, reuseRefs: true });
for (const [channelID, channel, context] of results) {
  // context.team is the same for all channels
  // context.organization is the same for all teams
  // channel.teamID === context.team.teamID
  // context.team.organizationID === context.organization.organizationID
}
  • All channels share the same team, and all teams share the same organization.
  • The context object is identical for all channels, referencing the same related records.

cleanUp(...factoryNames: string[]): void

Removes all seeded data for the specified factories from the writer.

If no factory names are provided, all seeded data will be removed.

  • Example 1:

    Graine.cleanUp(); // removes all seeded data
  • Example 2:

    Graine.cleanUp("user"); // removes all seeded data for the user factory (might cause foreign key constraints issues)

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 Graine.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(), new ChannelFactory(), new ChannelUserFactory(), 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!