Drizzle ORM

Drizzle Overview

Auto-generate Drizzle ORM table schemas from TypeScript types and mion Type Formats.

The package @mionkit/drizzle auto generates Drizzle ORM table schemas directly from TypeScript types. Unlike drizzle-zod or other drizzle plugins that generates Zod schemas FROM drizzle tables, this package works in the opposite direction: it auto-generates drizzle table configurations FROM TypeScript types while allowing optional overrides.

Column types are derived from Typescript types and @mionkit/type-formats - when you use format types like StrUUIDv7, StrEmail, NumInteger, etc., the package automatically selects the most appropriate database column type for each database.
Quick Tip: This package is designed to work with flat types that reflect the columnar structure of a database table, not deeply nested JavaScript objects with relationships. Use foreign key IDs instead of embedded entity references. Nested objects are stored as JSON columns.

Supported Databases

DatabaseFunctionImport
PostgreSQLmapPGTable<T>()@mionkit/drizzle
MySQLmapMySqlTable<T>()@mionkit/drizzle
SQLitemapSqliteTable<T>()@mionkit/drizzle
import {mapPGTable} from '@mionkit/drizzle';
import {uuid, timestamp} from 'drizzle-orm/pg-core';
// Note: Must use regular import (not `import type`) for reflection to work
import {StrUUIDv7, StrEmail} from '@mionkit/type-formats/FormatsString';
import {NumInteger} from '@mionkit/type-formats/FormatsNumber';

/** User entity with format types for intelligent column mapping */
interface User {
    id: StrUUIDv7;
    email: StrEmail;
    name: string;
    bio?: string; // Optional = nullable column
    age: NumInteger;
    tags: string[]; // Array = jsonb column
    settings: {theme: string; notifications: boolean}; // Nested object = jsonb
    createdAt: Date;
}

// we should always configure primary and foreign keys
export const usersAutoGenerated = mapPGTable<User>().build('users', {
    id: uuid('id').primaryKey(),
    createdAt: timestamp('created_at').defaultNow().notNull(),
});

How To Use

Primary and Foreign Keys

Primary keys and foreign keys should always be defined in the tableConfig. This gives you full control over key constraints and relationships:

import {mapPGTable} from '@mionkit/drizzle';
import {uuid, timestamp} from 'drizzle-orm/pg-core';
// Note: Must use regular import (not `import type`) for reflection to work
import {StrUUIDv7} from '@mionkit/type-formats/FormatsString';

/** User entity */
interface User {
    id: StrUUIDv7;
    name: string;
    createdAt: Date;
}

/** Post entity with foreign key reference */
interface Post {
    id: StrUUIDv7;
    title: string;
    content: string;
    authorId: StrUUIDv7; // Foreign key - just a string type
    createdAt: Date;
}

// Primary keys should be defined in the tableConfig override
export const users = mapPGTable<User>().build('users', {
    id: uuid('id').primaryKey(), // Primary key defined here
    createdAt: timestamp('created_at').defaultNow().notNull(),
});

// Foreign keys should also be defined in the tableConfig override
export const posts = mapPGTable<Post>().build('posts', {
    id: uuid('id').primaryKey(), // Primary key
    authorId: uuid('author_id') // Foreign key with reference
        .references(() => users.id, {onDelete: 'cascade'})
        .notNull(),
    createdAt: timestamp('created_at').defaultNow().notNull(),
});
Quick Tip: Types should only contain information related to type and format, this will be used for validation and serialization. Any columns asn SQL logic like primary, foreign keys, indexes, etc. should be defined in the tableConfig override.

Length Buffer

The lengthBuffer option multiplies the maxLength defined in custom string formats (StrFormat) to provide extra capacity for the database column. This is useful when you want to allow for some flexibility in the data length prevent resizing in the future, but still want to validate the correct length at runtime.

The lengthBuffer only applies to custom StrFormat types with a maxLength constraint. Well-known formats like StrEmail, StrURL, and StrDomain use their standard maximum lengths without any buffer.
// Custom string format with maxLength
type Username = StrFormat<{maxLength: 50}>;

interface User {
    id: StrUUIDv7;
    username: Username;
}

// Default lengthBuffer is 1.5
const users = mapPGTable<User>().build('users');
// username with maxLength 50 → varchar(75)

// Custom lengthBuffer of 2.0
const users = mapPGTable<User>({lengthBuffer: 2.0}).build('users');
// username with maxLength 50 → varchar(100)

Table Config Validation

The the build function validates that your tableConfig matches the TypeScript type

interface User {
    id: string;
    name: string;
}

// ❌ Error: Column "email" exists in tableConfig but not in type "User"
const users = mapPGTable<User>().build('users', {
    id: uuid('id').primaryKey(),
    email: text('email'), // This property doesn't exist in User!
});

Important Notes

Import Format Types Correctly

When using format types from @mionkit/type-formats, you must use regular imports (not import type) for the runtime type metadata to be preserved.
// ✅ Correct - regular import preserves metadata
import {StrUUIDv7, StrEmail} from '@mionkit/type-formats/FormatsString';

// ❌ Wrong - type import strips metadata
import type {StrUUIDv7, StrEmail} from '@mionkit/type-formats';

Nested Objects vs Foreign Keys

Nested objects are stored as JSON. Use foreign key IDs for entity references:

// ✅ Good: Profile is a value object - stored as JSON
interface User {
    id: string;
    profile: {bio: string; avatar: string};
}

// ❌ Bad: Don't embed entire entities
interface Book {
    id: string;
    owner: User; // This stores the entire User as JSON!
}

// ✅ Good: Use foreign key ID instead
interface Book {
    id: string;
    ownerId: string; // Reference by ID
}