# Lockness Drizzle Drizzle ORM integration for Lockness with PostgreSQL support, migration management, and CLI scaffolding. ## Overview @lockness/drizzle provides: - **PostgreSQL Integration** - Full Drizzle ORM support - **Migration Management** - Generate and apply migrations - **Database Seeding** - Populate development/test data - **Drizzle Studio** - Visual database browser - **CLI Scaffolding** - Generate models, repositories, controllers - **Dependency Injection** - Auto-injectable Database service - **Type Safety** - Full TypeScript support with inference ## Installation ### Quick Install (Recommended) ```bash deno task cli package:install drizzle ``` This automatically: - Creates `drizzle.config.ts` - Sets up directory structure (`migrations/`, `app/model/`, etc.) - Creates `database_seeder.ts` - Adds `DATABASE_URL` to `.env` files - Tests database connection ### Manual Installation ```bash deno add @lockness/drizzle ``` Then configure manually (see Configuration). ## Configuration ### Environment Variables Add to `.env`: ```env DATABASE_URL=postgres://user:password@localhost:5432/mydb ``` ### Drizzle Config `drizzle.config.ts` (auto-generated by installer): ```typescript import { defineConfig } from 'drizzle-kit' export default defineConfig({ schema: './app/model/*.ts', out: './database/migrations', dialect: 'postgresql', dbCredentials: { url: Deno.env.get('DATABASE_URL')!, }, }) ``` ## Basic Usage ### Database Service ```typescript import { Inject, Service } from '@lockness/core' import { Database } from '@lockness/drizzle' @Service() export class UserService { @Inject(Database) accessor db!: Database async getAllUsers() { return await this.db.instance.select().from(users) } } ``` ### In Controllers ```typescript import { Context, Controller, Get, Inject } from '@lockness/core' import { Database } from '@lockness/drizzle' import { users } from '../model/user.ts' @Controller('/users') export class UserController { @Inject(Database) accessor db!: Database @Get('/') async index(c: Context) { const allUsers = await this.db.instance.select().from(users) return c.json({ users: allUsers }) } } ``` ## Model Definition Define database schema with Drizzle: ```typescript // app/model/user.ts import { boolean, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core' import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import { z } from 'zod' export const users = pgTable('users', { id: serial('id').primaryKey(), email: text('email').notNull().unique(), name: text('name').notNull(), password: text('password').notNull(), emailVerified: boolean('email_verified').default(false), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), }) // Auto-generated Zod schemas from Drizzle export const selectUserSchema = createSelectSchema(users) export const insertUserSchema = createInsertSchema(users, { email: z.string().email(), name: z.string().min(2).max(100), password: z.string().min(8), }) // TypeScript types export type User = typeof users.$inferSelect export type NewUser = typeof users.$inferInsert ``` ## Relationships Define relations between tables: ```typescript import { integer, pgTable, serial, text } from 'drizzle-orm/pg-core' import { relations } from 'drizzle-orm' export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), }) export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), content: text('content'), authorId: integer('author_id').references(() => users.id), }) // Define relationships export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })) export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id], }), })) ``` Query with relations: ```typescript // Get users with their posts const usersWithPosts = await db.instance.query.users.findMany({ with: { posts: true, }, }) // Get post with author const postWithAuthor = await db.instance.query.posts.findFirst({ where: eq(posts.id, 1), with: { author: true, }, }) ``` ## Repository Pattern Create repositories for clean data access: ```typescript // app/repository/user_repository.ts import { Inject, Service } from '@lockness/core' import { Database } from '@lockness/drizzle' import { eq } from 'drizzle-orm' import { type NewUser, type User, users } from '../model/user.ts' @Service() export class UserRepository { @Inject(Database) accessor db!: Database async findAll(): Promise { return await this.db.instance.select().from(users) } async findById(id: number): Promise { const result = await this.db.instance .select() .from(users) .where(eq(users.id, id)) return result[0] } async findByEmail(email: string): Promise { const result = await this.db.instance .select() .from(users) .where(eq(users.email, email)) return result[0] } async create(data: NewUser): Promise { const result = await this.db.instance .insert(users) .values(data) .returning() return result[0] } async update(id: number, data: Partial): Promise { const result = await this.db.instance .update(users) .set({ ...data, updatedAt: new Date() }) .where(eq(users.id, id)) .returning() return result[0] } async delete(id: number): Promise { await this.db.instance.delete(users).where(eq(users.id, id)) } } ``` ## CLI Commands ### Model Scaffolding Generate complete model with all files: ```bash deno task cli make:model Post -a ``` Generates: - `app/model/post.ts` - Schema and types - `app/repository/post_repository.ts` - Repository - `app/controller/post_controller.ts` - Controller - `database/seeders/post_seeder.ts` - Seeder Flags: - `-r, --repository` - Generate repository only - `-s, --seeder` - Generate seeder only - `-c, --controller` - Generate controller only - `-a, --all` - Generate everything ### Migration Commands ```bash # Generate migration from schema changes deno task cli db:generate # Apply pending migrations deno task cli db:migrate # Check migration status deno task cli db:status # Drop all tables and re-migrate deno task cli db:fresh # Push schema without migrations (dev only) deno task cli db:push ``` ### Database Commands ```bash # Test database connection deno task cli db:check # Run seeders deno task cli db:seed # Launch Drizzle Studio dx drizzle-kit studio ``` ## Advanced Queries ### Filtering ```typescript import { and, eq, gt, gte, ilike, like, lt, lte, ne, or } from 'drizzle-orm' // Single condition const activeUsers = await db.instance .select() .from(users) .where(eq(users.emailVerified, true)) // Multiple conditions (AND) const recentActiveUsers = await db.instance .select() .from(users) .where(and( eq(users.emailVerified, true), gt(users.createdAt, new Date('2024-01-01')), )) // OR conditions const specialUsers = await db.instance .select() .from(users) .where(or( eq(users.email, 'admin@example.com'), eq(users.role, 'admin'), )) // Pattern matching const searchResults = await db.instance .select() .from(users) .where(ilike(users.name, `%${query}%`)) ``` ### Sorting and Pagination ```typescript import { asc, desc } from 'drizzle-orm' // Sort by creation date (newest first) const users = await db.instance .select() .from(users) .orderBy(desc(users.createdAt)) // Pagination const page = 2 const perPage = 20 const paginatedUsers = await db.instance .select() .from(users) .limit(perPage) .offset((page - 1) * perPage) ``` ### Joins ```typescript // Left join const postsWithAuthors = await db.instance .select({ postId: posts.id, postTitle: posts.title, authorName: users.name, authorEmail: users.email, }) .from(posts) .leftJoin(users, eq(posts.authorId, users.id)) // Inner join const publishedPostsWithAuthors = await db.instance .select() .from(posts) .innerJoin(users, eq(posts.authorId, users.id)) .where(eq(posts.published, true)) ``` ### Aggregations ```typescript import { avg, count, max, min, sum } from 'drizzle-orm' // Count users const [{ value: userCount }] = await db.instance .select({ value: count() }) .from(users) // Group by and count const postsByAuthor = await db.instance .select({ authorId: posts.authorId, postCount: count(), }) .from(posts) .groupBy(posts.authorId) ``` ## Transactions Execute multiple operations atomically: ```typescript await db.instance.transaction(async (tx) => { // Create user const [user] = await tx.insert(users).values({ email: 'new@example.com', name: 'New User', password: hashedPassword, }).returning() // Create first post await tx.insert(posts).values({ title: 'Hello World', authorId: user.id, }) // If any operation fails, all are rolled back }) ``` ## Database Seeding Create seed data for development: ```typescript // database/seeders/user_seeder.ts import { Database } from '@lockness/drizzle' import { users } from '../../app/model/user.ts' import * as bcrypt from 'bcrypt' export class UserSeeder { async run(db: Database) { const hashedPassword = await bcrypt.hash('password123', 10) await db.instance.insert(users).values([ { email: 'alice@example.com', name: 'Alice Smith', password: hashedPassword, emailVerified: true, }, { email: 'bob@example.com', name: 'Bob Johnson', password: hashedPassword, emailVerified: true, }, ]) } } ``` Register in `database/seeders/database_seeder.ts`: ```typescript import { UserSeeder } from './user_seeder.ts' import { PostSeeder } from './post_seeder.ts' export const seeders = [ UserSeeder, PostSeeder, ] ``` Run seeders: ```bash deno task cli db:seed ``` ## Drizzle Studio Visual database browser: ```bash dx drizzle-kit studio ``` Features: - Browse and edit tables - Run SQL queries - View relationships - Manage data visually - Real-time updates ## Migration Workflow 1. **Modify schema** in `app/model/*.ts` 2. **Generate migration:** ```bash deno task cli db:generate ``` 3. **Review migration** in `database/migrations/` 4. **Apply migration:** ```bash deno task cli db:migrate ``` 5. **Commit migration** to version control ## Best Practices - **Use repositories** for data access (don't query directly in controllers) - **Define Zod schemas** with drizzle-zod for validation - **Use transactions** for related operations - **Create migrations** for all schema changes (never use db:push in production) - **Use seeders** for test/development data - **Leverage TypeScript inference** from Drizzle schemas - **Add indexes** for frequently queried columns - **Use relations** for cleaner queries with joins - **Validate input** with Zod before inserting/updating - **Handle unique constraint violations** gracefully ## Common Patterns ### Soft Deletes ```typescript export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), deletedAt: timestamp('deleted_at'), }) // Soft delete await db.instance .update(posts) .set({ deletedAt: new Date() }) .where(eq(posts.id, id)) // Query non-deleted const activePosts = await db.instance .select() .from(posts) .where(isNull(posts.deletedAt)) ``` ### Timestamps ```typescript export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) // Update with timestamp await db.instance .update(posts) .set({ title: 'New Title', updatedAt: new Date(), }) .where(eq(posts.id, id)) ``` ### UUID Primary Keys ```typescript import { pgTable, text, uuid } from 'drizzle-orm/pg-core' export const users = pgTable('users', { id: uuid('id').defaultRandom().primaryKey(), email: text('email').notNull().unique(), name: text('name').notNull(), }) ``` ## Complete Example ```typescript // app/model/post.ts import { boolean, integer, pgTable, serial, text, timestamp, } from 'drizzle-orm/pg-core' import { relations } from 'drizzle-orm' import { users } from './user.ts' export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), content: text('content'), published: boolean('published').default(false), authorId: integer('author_id').references(() => users.id), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), }) export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id], }), })) export type Post = typeof posts.$inferSelect export type NewPost = typeof posts.$inferInsert // app/repository/post_repository.ts import { Inject, Service } from '@lockness/core' import { Database } from '@lockness/drizzle' import { eq } from 'drizzle-orm' import { type NewPost, type Post, posts } from '../model/post.ts' @Service() export class PostRepository { @Inject(Database) accessor db!: Database async findPublished(): Promise { return await this.db.instance .select() .from(posts) .where(eq(posts.published, true)) } async findByIdWithAuthor(id: number) { return await this.db.instance.query.posts.findFirst({ where: eq(posts.id, id), with: { author: true }, }) } async create(data: NewPost): Promise { const [post] = await this.db.instance .insert(posts) .values(data) .returning() return post } } // app/controller/post_controller.ts import { Context, Controller, Get, Inject } from '@lockness/core' import { PostRepository } from '../repository/post_repository.ts' @Controller('/posts') export class PostController { @Inject(PostRepository) accessor postRepo!: PostRepository @Get('/') async index(c: Context) { const posts = await this.postRepo.findPublished() return c.json({ posts }) } @Get('/:id') async show(c: Context) { const id = Number(c.req.param('id')) const post = await this.postRepo.findByIdWithAuthor(id) if (!post) { return c.json({ error: 'Not found' }, 404) } return c.json({ post }) } } ``` ## Dependencies - `drizzle-orm` - ORM library - `drizzle-kit` - CLI tools - `drizzle-zod` - Zod schema generation - `postgres` - PostgreSQL client