Core Concepts
Seed

Seed data

🐥

This is a preview feature. We would love your feedback (opens in a new tab)!

The need for generated data

Generated data, or seed data, is a useful tool in software development, providing an initial set of data for your database.

Here's why it's beneficial:

  • Development: Provides a consistent dataset for developers.
  • Testing: Ensures predictability when verifying features.
  • Demonstration: Showcases your software's abilities with example content.
  • Default Data: Adds necessary built-ins, like country lists.
  • Onboarding: Gives new users a filled-out starting point.
  • Performance: Helps simulate heavy usage scenarios.
  • Guides: Common reference in tutorials.

While generated data might be the unsung hero of early development, it can get a bit needy as software evolves. But don't sweat it! Our mission is to save you from the never-ending saga of maintaining those seed scripts. Who has time for that, right?

The workflow at a glance

Using Snaplet to seed data will typically look something like this:

  1. Initial setup with npx snaplet setup
  2. Using @snaplet/seed to generate seed data
  3. Regenerating assets as your database changes with npx snaplet generate

You can also refer to the Seed Quick Start Guide for a practical example of the workflow.

1. Initial setup with npx snaplet setup

At the start, you'll setup Snaplet for your project with npx snaplet setup, and follow the interactive prompts.

snaplet setup will look at your database and its structure, and use it to generate the configuration files and assets needed by @snaplet/seed.

2. Using @snaplet/seed to generate seed data

@snaplet/seed is what you'll use to actually generate data. It is a javascript library, giving you a fully-typed programmatic interface tailored to your database structure, which you'll use to describe how data should be generated.

Most commonly you'll write and run seed scripts that use @snaplet/seed. If you write your seed scripts in typescript, tsx (opens in a new tab) might come in handy for running your scripts:

>_ terminal

npx tsx seed.mts

Writing and running vanilla javascript works too:

>_ terminal

# a vanilla javascript file that uses `@snaplet/seed`
node seed.js

You can also use @snaplet/seed in other scenarios - for example, in your tests.

For more detail on using @snaplet/seed, check out Generating data with @snaplet/seed below.

3. Regenerating assets as your database changes with npx snaplet generate

@snaplet/seed provides you with a client library that corresponds to your database structure. To do this, snaplet looks at your database structure and uses this to generate assets that @snaplet/seed can use to provide this client library.

Whenever your database structure changes, @snaplet/seed will need these assets to be regenerated so that they are in sync with your new database structure.

You can regenerate these assets by running:

>_ terminal

npx snaplet generate

If you are familiar with @prisma/client (opens in a new tab), you can think of snaplet generate doing the same thing, just in this case for @snaplet/seed rather than @prisma/client.

Generating data with @snaplet/seed

You can refer to the Seed Quick Start Guide to learn about the basics of @snaplet/seed.

Inside the @snaplet/seed workflow

Plans, stores and SQL statements

The plan is what's returned by the functions of @snaplet/seed. It's a representation of the data you want to seed.


const userPlan = seed.users([{ email: 'snappy@snaplet.dev' }]);
// ^ the plan ^ the plan's inputs

When a plan is executed, triggered by awaiting the plan, it stores the generated data in an in-memory object called a store. Each plan has its own store. Then, the store is turned into SQL statements that are executed to persist the data in your database.

Given the plan from the Seed Quick Start Guide:

seed.mts

import { createSeedClient } from '@snaplet/seed';
const seed = await createSeedClient();
const postsStore = await seed.posts([{
title: 'Hello World!',
author: {
email: (ctx) =>
copycat.email(ctx.seed, {
domain: 'acme.org',
}),
},
comments: (x) => x(3),
}]);

Here is what its store looks like internally:


const postsStore = {
users: [user, user, user, user],
posts: [post],
comments: [comment, comment, comment],
}

The global store

The global store is a special store which contains all the data that was generated as part of the plans you executed. It is accessible under the $store property of the seed client.


await seed.posts(x => x(3));
// { posts: [post, post, post], users: [user, user, user] }
console.log(seed.$store);

Local stores

Each plan produces its own store and returns it when executed. It's particularly useful for the connect option.


const postsStore = await seed.posts(x => x(3));
// { posts: [post, post, post], users: [user, user, user] }
console.log(postsStore);
const usersStore = await seed.users(x => x(2));
// { users: [user, user] }
console.log(usersStore);
// { posts: [post, post, post], users: [user, user, user, user, user] }
console.log(seed.$store);

Client state

The SeedClient keeps track of what was created before with seed.$store and increments global seeds each time you call a model function.

If you want to reset this state, you can use the seed.$reset function.


await seed.posts(x => x(3));
seed.$reset();
// will give you the same result as before
await seed.posts(x => x(3));

We also provide a way to get a new instance of the SeedClient with a clean state through the seed.$transaction function.


await seed.posts(x => x(3));
await seed.$transaction((seed) => {
// will give you the same result as above
await seed.posts(x => x(3));
})

The seed.$transaction function can be used in tests to ensure that each test is executed with a clean state.

If you need a fully deterministic plan execution, you can provide your own seed which will override the global seed counter.


// These statements will produce the exact same results
await seed.posts(x => x(3), { seed: '42' });
await seed.posts(x => x(3), { seed: '42' })

Static or dynamic data, we got you covered

If you want to use static data, you can directly pass an array to the models functions. As always, all the required fields and relationships will be automatically created for you based on your database schema.


// an array of 2 users
seed.users([
{ email: 'alice@acme.com' },
{ email: 'bob@acme.com' },
]);

If you want to use dynamic data, you can pass a callback function to the models functions. We inject a function x that you can use to generate as many models as you want.


// an array of 10 users
seed.users((x) => x(10));
// an array of 10 users with a custom email
seed.users((x) => x(10, {
email: (ctx) => copycat.email(ctx.seed, { domain: 'snaplet.dev' })
}));
// an array of 10 users with a custom email depending on contextual informations
seed.users((x) => x(10, (ctx) => ({
email: `user${ctx.index}@snaplet.dev`,
})));

And if you need both static and dynamic data, you can mix them together!


// an array of 10 users
seed.users((x) => [
{ email: 'first@acme.com' },
...x(8),
{ email: 'last@acme.com' },
])

Using the generate callback

If you need more control over the value generated for each field, you can also pass a function for that field:


seed.users([{
email: (ctx) => copycat.email(ctx.seed)
}])

seed

A value unique to a particular field, and to a particular model being generated.

seed is deterministic in the sense that for each new session (for example, each new run of a seed script using @snaplet/seed, or each new run of the same $transaction), the generate callback will receive the same value for seed for the same field of the same model.


const run = () =>
seed.$transaction(async (seed) => {
await seed.customers([
{
email: (ctx) => copycat.email(ctx.seed),
},
]);
});
await run();
// INSERT INTO public."Customer" ... VALUES (..., 'Autumn_Boyer64366@vague-foal.org', ...);
await run();
// INSERT INTO public."Customer" ... VALUES (..., 'Autumn_Boyer64366@vague-foal.org', ...);

You can also access the seed of a model by using a callback.


await seed.customers([
(ctx) => {
const createdAt = copycat.dateString(ctx.seed);
const day = 60 * 60 * 24 * 1000;
const updatedAt = new date(createdAt.getTime() + day);
return {
createdAt,
updatedAt,
};
}
]);

data

An object containing the data already generated for a particular model.


await seed.users([{
email: (ctx) => `${ctx.data.first_name}${ctx.data.last_name}@example.org`.toLowerCase()
}])

Field precedence

The place in which the generate callback was given will determine what fields will be already available in data.

For example, fields specified earlier in the same object for a particular model will be available in data for fields specified later in that object:


await seed.users([{
// `name` is evaluated first, then `email`
name: (ctx) => copycat.firstName(ctx.seed),
email: (ctx) => `${ctx.data.name}@example.org`.toLowerCase()
}])

And similarly, fields specified for a model when creating a new SeedClient (name in the example below) will be available in data for fields specified as part of a plan (email in the example below):


await createSeedClient({
models: {
users: {
name: (ctx) => copycat.firstName(ctx.seed)
}
}
})
await seed.users([{
// `name` was given when creating `SeedClient`, so it'll be available here
email: (ctx) => `${ctx.data.name}@example.org`.toLowerCase()
}])

To help with knowing what exactly will be available in data, we generate data in this order:

  1. Values for parent relation fields (for example, an organizationId field on a members model) will be generated first

  2. Then default values for fields (if you didn't explicitly define what the value of a field should be anywhere) will be generated next

  3. Then values given for fields using the models option for createSeedClient (as shown below) will be generated next:


await createSeedClient({
models: {
users: {
name: (ctx) => copycat.firstName(ctx.seed)
}
}
})

  1. Then values given for fields using the models option for a plan:

seed.users([{ ... }], {
models: {
name: (ctx) => copycat.firstName(ctx.seed)
}
})

  1. Then finally, fields specified earlier in the same object for a particular model will be generated before fields specified later in that object:

await seed.users([{
// `name` is evaluated first, then `email`
name: (ctx) => copycat.firstName(ctx.seed),
email: (ctx) => `${ctx.data.name}@example.org`.toLowerCase()
}])

store

The store local to this plan, containing models already created in this plan.

$store

The global store (opens in a new tab) containing all models created in this snaplet instance so far.

Connecting data

Using the connect option

We provide a special option that you can activate in the options of a plan, called connect.

When true, the plan will automatically connect models relationships to fulfill to the client's global store if possible. The corresponding model will be picked randomly (but deterministically, we're using copycat.oneOf function under the hood).

In the following example, the post model will be connected to one of the 3 users in the store.


await seed.users((x) => x(3));
await seed.posts([{}], { connect: true });

You can also provide a store to the connect option, in which case the plan will connect to the models in the store instead of the client's global store.


await seed.users((x) => x(3));
const heroesStore = await seed.users([
{ email: 'bruce@wayne-enterprises.com' },
{ email: 'peter.parker@gmail.com' }
]);
await seed.posts([{}], { connect: heroesStore });

In the above case, the post model will be connected to one of the user in the heroesStore.

The store can also be manually constructed, for example if you want to connect to existing records in your database.


const users = await prisma.users.findMany({
where: { status: 'HERO' }
})
await seed.posts([{}], { connect: { users } });

Using the connect function

If you need a more fine-grained control over the connection, you can use the connect function injected into the model callback API.

Let's start from our previous plan.

seed.mts

import { createSeedClient } from '@snaplet/seed';
import { copycat } from '@snaplet/copycat';
const seed = await createSeedClient();
await seed.posts([{
title: 'Hello World!',
author: {
email: (ctx) =>
copycat.email(ctx.seed, {
domain: 'acme.org',
}),
},
comments: (x) => x(3),
}]);

We specify that the author of each comment will be provided from an external source rather than being generated using the model callback API.

seed.mts

import { createSeedClient } from '@snaplet/seed';
import { copycat } from '@snaplet/copycat';
const seed = await createSeedClient()
await seed.posts([{
title: 'Hello World!',
author: {
email: (ctx) =>
copycat.email(ctx.seed, {
domain: 'acme.org',
}),
},
comments: (x) => x(3, {
author: () => {}
}),
}]);

We can use the connect function injected in the context to express that the author of the comments is the same as the post. We are using the plan's internal store to fetch the generated user. In this case the only generated user in the plan will be the author of the post.

seed.mts

import { createSeedClient } from '@snaplet/seed';
import { copycat } from '@snaplet/copycat';
await seed.posts([{
title: 'Hello World!',
author: {
email: (ctx) =>
copycat.email(ctx.seed, {
domain: 'acme.org',
}),
},
comments: (x) => x(3, {
author: (ctx) => ctx.connect(
({ store }) => store.users[0]
)
}),
}]);

Let's start from our previous plan.

We specify that the author of each comment will be provided from an external source rather than being generated using the model callback API.

We can use the connect function injected in the context to express that the author of the comments is the same as the post. We are using the plan's internal store to fetch the generated user. In this case the only generated user in the plan will be the author of the post.

seed.mts

import { createSeedClient } from '@snaplet/seed';
import { copycat } from '@snaplet/copycat';
const seed = await createSeedClient();
await seed.posts([{
title: 'Hello World!',
author: {
email: (ctx) =>
copycat.email(ctx.seed, {
domain: 'acme.org',
}),
},
comments: (x) => x(3),
}]);

store

The store local to this plan, containing models already created in this plan.

$store

The global store (opens in a new tab) containing all models created in this snaplet instance so far.

Customizing @snaplet/seed with aliases

The models, fields and relationships names are fully customizable. You can use the alias option to change them.

ℹ️

You need to regenerate your types after changing the alias option.

To do that, run this command in your terminal:

>_ terminal

npx snaplet generate

Inflection

Inflection generally refers to the modification of words to express different grammatical categories. In our context, inflection is about altering the form of a model, field or relationship name to fit its intended use or to adhere to certain conventions so your code is more readable.

You can use the inflection option to provide your own rules to define the names of your models, fields and relationships.

We provide a default and sensible implementation of the inflection rules, inspired by PostGraphile (opens in a new tab), but you can override them if you want.

These rules are:

Given your tables being named User, Post and Comment, here is an example of how the default inflection strategy is applied to change the names of your models:

seed.mts

import { createSeedClient } from '@snaplet/seed';
const seed = await createSeedClient()
// without inflection, you would have to use seed.User(...)
await seed.users((x) => x(3))

And here is how you would change the inflection rules:

snaplet.config.ts

/// <reference path=".snaplet/snaplet.d.ts" />
import { defineConfig } from 'snaplet';
export default defineConfig({
seed: {
alias: {
inflection: {
modelName: (modelName) => `Super${modelName}s`,
},
},
},
});

seed.mts

import { createSeedClient } from '@snaplet/seed';
const seed = await createSeedClient()
// without inflection, you would have to use seed.User(...)
await seed.SuperUsers((x) => x(3))

If you don't want to use the inflection feature, you can disable it by setting inflection: false in the alias option.

snaplet.config.ts

/// <reference path=".snaplet/snaplet.d.ts" />
import { defineConfig } from 'snaplet';
export default defineConfig({
seed: {
alias: {
inflection: false,
},
}
});

Override

If you are not satisfied with the default names of your models, fields or relationships, but you only want to do one-off changes, you can use the override option.

snaplet.config.ts

/// <reference path=".snaplet/snaplet.d.ts" />
import { defineConfig } from 'snaplet';
export default defineConfig({
seed: {
alias: {
override: {
User: {
fields: {
Post_Post_validator_idToUser: 'validatedPosts',
}
}
}
},
},
});

seed.mts

import { createSeedClient } from '@snaplet/seed';
const seed = await createSeedClient()
await seed.users([{
// without override, it would be: postsByValidatorId: [...]
validatedPosts: [{ title: 'Hello World!' }]
}])

The override option is applied on top of the inflection option, it's perfect for one-off changes.