Seed by Snaplet

repository·main·Indexed 21 days ago

https://github.com/supabase-community/seed

A tool for automatically seeding PostgreSQL, SQLite, and MySQL databases with production-like dummy data for local development and testing. It features a type-safe TypeScript client, automated relationship management, and AI-powered value generation using LLMs. Seed includes a CLI for initialization and synchronization, and provides dedicated adapters for various database clients and Prisma integration.

Tokens
23.8K
Snippets
97
Records
107
Agent score
67%

What's inside Seed

  1. Overview of Seed components

    main

    Seed is a toolset designed to understand your database schema and generate production-accurate data. It consists of three main components:

    • Seed Client: An auto-generated, type-safe data client for Node.js and TypeScript that allows you to interact with your data using a fluent API.
    • Seed CLI: A command-line interface used to initialize Seed and keep your data client in sync with your database schema.
    • Seed AI: A custom model that analyzes your database schema to ensure the generated data accurately reflects the shape and context of your real data.

    Seed is compatible with PostgreSQL, SQLite, and MySQL.

  2. How Snaplet handles ID generation and auto-increment fields

    main

    Snaplet uses specific strategies to mimic database behavior for identifiers:

    UUID Generation

    For fields requiring a UUID or unique identifier, Snaplet uses a seed-based approach to ensure values are both unique and reproducible. It typically uses the copycat library to generate these values based on the current seed.

    // Default UUID generation for fields requiring unique identifiers
    id: ({ seed }) => copycat.uuid(seed);

    Auto-Increment Fields

    To manage auto-incrementing sequences without causing conflicts, Snaplet follows these steps:

    1. Fetch Current Sequence Value: It retrieves the current maximum value from the database sequence.
    2. Internal Sequence Management: It maintains an in-memory counter starting from that maximum value to ensure new values follow the existing sequence.
    3. Committing New Rows: After seeding, Snaplet ensures the database sequence is updated to reflect the newly inserted values.
  3. How connections work in @snaplet/seed

    main

    In @snaplet/seed, a "connection" refers to referencing existing data rather than creating new data. This allows you to build complex relational graphs in your seed scripts. There are four primary ways to define connections, ranging from implicit to explicit:

    1. Path connect (Implicit): Connections made when a parent entity declares children. The children automatically reference the parent.
    2. Plan connect: Defines a connection pool for a specific seeding function (a "plan"), allowing you to break down logic into dedicated, scoped functions.
    3. Global connect: Sets default connection behavior for all plans in the seed script. This is useful for shared configuration or augmenting all rows with specific existing data.
    4. Field connect: The most granular method, allowing you to connect specific columns and rows to specific existing entities.

    While all connections can technically be achieved via field connect, the other methods serve as convenient shortcuts for common patterns.

  4. Understand how Snaplet handles default values and constraints

    main

    Snaplet manages default values for fields with specific constraints—such as unique keys, primary keys, or sequences—to ensure data integrity and prevent seeding errors.

    This management serves two primary purposes:

    1. Preventing duplicates: Automatically generating unique values for fields with uniqueness constraints to avoid insertion errors.
    2. Ensuring relational integrity: Producing predictable and reproducible values to maintain links between related data across different tables.
  5. Manage state in the SnapletClient (v0.81.0+)

    main

    In versions 0.81.0 and above, the SnapletClient is stateful. Every time you call a model function, a global seed is incremented, ensuring unique ID sequences.

    Key state management features:

    • $store: A global store property on the data client that contains all generated data.
    • $reset(): Resets the state of the data client.
    • $transaction(callback): Provides a new instance of the data client with a reset state. This is recommended for testing to ensure isolation between tests.
    // Calling a model multiple times continues the id sequences
    await snaplet.users([{}]);
    await snaplet.users([{}]);
    
    // Access the global store
    console.log(snaplet.$store);
    
    // Reset the state
    snaplet.$reset();
    
    // Use a transaction for isolated state (useful in tests)
    await snaplet.$transaction(async (snaplet) => {
      await snaplet.users([{}]);
    });
  6. Define data using Plan Inputs

    main

    Plan inputs for a model can be a static object or a callback function receiving a PlanContext.

    PlanContext

    • index: The current index of the record being generated.
    • seed: A deterministic seed string.
    • store: The current Seed Client store.

    Field Contexts

    When defining fields within a plan, you can use callbacks to access:

    • FieldContext: Access previously generated scalar fields in the same record via ctx.data.
    • ParentContext: Use ctx.connect(modelData) to satisfy a relationship by connecting to an existing record in the store without creating a new one.
    // Scalar field as a callback using previously generated data
    await seed.posts([
      {
        title: "Hello, world!",
        content: (ctx) => `The title of this post is "${ctx.data.title}"`,
      },
    ]);
    
    // Parent field (relationship) using connect
    await seed.posts([
      {
        author: (ctx) => ctx.connect(seed.$store.users[0]),
      },
    ]);
  7. Use callback functions for model and field seeding

    main

    Starting from version 0.85.0, you can use callback functions to define dynamic data for models and fields. This allows you to access the current iteration index, the unique seed for that iteration, and the data generated for previous fields.

    Children fields

    Callbacks receive index and seed:

    await seed.posts([
      ({ index, seed }) => ({
        title: `Post #${index}`,
        content: `This post's seed is ${seed}`
      }),
    ]);

    Parent fields

    Callbacks receive seed and a connect function to explicitly link data:

    await seed.posts([
      {
        author: (ctx) => ctx.connect(({ store }) => store.users[0])
      },
    ]);

    Accessing previously generated data

    Use the data parameter in a callback to access values from other fields in the same record:

    await seed.users([{
      createdAt: ({ seed }) => copycat.dateString(seed),
      updateAt: ({ data }) => {
        const createdAt = new Date(data.createdAt)
        const updatedAtMs = Number(createdAt) + 60_000
        return new Date(updatedAtMs).toISOString()
      }
    }])
    // Example of using the 'data' parameter to create dependent fields
    await seed.users([{
      createdAt: ({ seed }) => copycat.dateString(seed),
      updateAt: ({ data }) => {
        const createdAt = new Date(data.createdAt)
        const updatedAtMs = Number(createdAt) + 60_000
        return new Date(updatedAtMs).toISOString()
      }
    }])
  8. Hook into the Supabase seeding workflow using dryRun

    main

    You can integrate Seed's output directly into the Supabase seeding workflow by using the dryRun option in createSeedClient.

    Setting dryRun: true prevents actual execution and instead logs the SQL queries that would have been run to the console. You can then redirect this output to a .sql file for Supabase to consume.

    Workflow

    1. Configure seed.ts with dryRun: true.
    2. Generate the SQL file:
      npx tsx seed.ts > supabase/seed.sql
    3. Apply the seed via Supabase:
      npx supabase db reset
    import { createSeedClient } from "@snaplet/seed";
    
    const seed = await createSeedClient({ dryRun: true });
  9. Sync the Seed Client with Database Changes

    main

    Whenever your database schema changes (e.g., after running migrations), you must regenerate the Seed client to reflect the new structure using the sync command.

    To automate this, you can add a postmigrate script to your package.json that runs after your migration command.

    npx @snaplet/seed sync
    "scripts": {
      "migrate": "prisma migrate dev",
      "postmigrate": "npx @snaplet/seed sync"
    }
  10. Use the Seed Client to generate data

    main

    The Seed Client is auto-generated from your database schema, providing a type-safe and auto-completed experience in TypeScript. It allows you to define specific data requirements while automatically handling complex relationships.

    Common patterns include:

    • Generating a specific count of records: Use a function to specify the number of items.
    • Providing specific field values: Pass an object to override default values (e.g., setting a specific email).
    • Generating nested relationships with varying counts: Use a function within an object to define a range (min/max) for related records.
    // Generate 5 posts
    await seed.posts((x) => x(5))
    
    // Create a user with a specific email
    await seed.users([
      { email: 'snappy@snaplet.dev' },
    ])
    
    // Create an organization with a varying number of members (between 1 and 10)
    await seed.organizations([
      {
        name: 'Snaplet',
        members: (x) => x({ min: 1, max: 10 }),
      },
    ])
  11. Run the Seed documentation locally

    main

    If you want to host the Seed documentation locally, follow these steps:

    1. Prerequisites: Ensure brew, git, pnpm, and Node.js are installed.
    2. Installation:
      cd docs
      pnpm install
    3. Run:
      pnpm dev
    4. Access the docs at http://localhost:3000.
    cd docs
    pnpm install
    pnpm dev