Skip to content
Back to skills

Drizzle Prisma Orm Architect

BSecurity

Architect relational database schemas, model complex entity relationships, optimize query execution, and generate zero-downtime database migration files.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 27, 2026
developmenttypescriptgobashsqlnodedatabaseperformance

Works with

  • cli

Security analysis

B88/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 27, 2026

npx -y skills add RMAzrim/opencode-artes --skill drizzle-prisma-orm-architect --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Drizzle Prisma Orm Architect?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Drizzle Prisma Orm Architect
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/rmazrim-drizzle-prisma-orm-architect/badge)](https://www.skillsdirectory.com/skills/rmazrim-drizzle-prisma-orm-architect)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Drizzle & Prisma ORM Architect
description: Architect relational database schemas, model complex entity relationships, optimize query execution, and generate zero-downtime database migration files.
metadata:
  source: skills/drizzle-prisma-orm-architect/drizzle-prisma-orm-architect.md
---

# Drizzle & Prisma ORM Architect

## 1. System Architecture & Prerequisites

This skill covers end-to-end PostgreSQL schema design and ORM layer implementation using **Prisma** and **Drizzle ORM** side-by-side on the same e-commerce domain model. The architecture enforces:

- **1:1** User → UserProfile
- **1:N** Category → Products, User → Orders, User → Reviews, Order → OrderItems
- **M:N** Product ↔ Category via Prisma implicit many-to-many and Drizzle manual junction table
- **Soft-delete** via `deletedAt` column with automatic filtering middleware
- **Composite indexes** for high-throughput query patterns

**Prerequisites:**

- Node.js ≥ 18
- PostgreSQL 14+
- `prisma` CLI (`npm i -D prisma`)
- `drizzle-kit` CLI (`npm i -D drizzle-kit`)
- `drizzle-orm` runtime (`npm i drizzle-orm`)

## 2. Input/Output Data Contracts

**Input — Environment Variables:**

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["DATABASE_URL"],
  "properties": {
    "DATABASE_URL": {
      "type": "string",
      "description": "PostgreSQL connection string",
      "pattern": "^postgresql://"
    }
  }
}
```

**Output Artifacts:**

| Path | Description |
|---|---|
| `prisma/schema.prisma` | Prisma schema definition |
| `prisma/migrations/` | Auto-generated Prisma migration SQL |
| `src/drizzle/schema.ts` | Drizzle table definitions |
| `src/drizzle/migrations/` | Auto-generated Drizzle migration SQL |
| `drizzle.config.ts` | Drizzle Kit configuration |
| `src/lib/prisma-client.ts` | Prisma client singleton + soft-delete middleware |
| `src/lib/drizzle-client.ts` | Drizzle client + helper utilities |
| `src/repositories/prisma-repo.ts` | Prisma CRUD repository |
| `src/repositories/drizzle-repo.ts` | Drizzle CRUD repository |

## 3. Production Reference Implementation

### 3A — Prisma Variant

```prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  role      Role     @default(CUSTOMER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  deletedAt DateTime?

  profile   UserProfile?
  orders    Order[]
  reviews   Review[]
  cartItems CartItem[]

  @@index([email])
  @@index([role])
  @@map("users")
}

model UserProfile {
  id        String  @id @default(cuid())
  userId    String  @unique
  avatar    String?
  bio       String?
  phone     String?
  address   Json?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("user_profiles")
}

model Category {
  id        String   @id @default(cuid())
  name      String   @unique
  slug      String   @unique
  parentId  String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  deletedAt DateTime?

  parent   Category?  @relation("CategoryTree", fields: [parentId], references: [id])
  children Category[] @relation("CategoryTree")
  products Product[]

  @@index([parentId])
  @@index([slug])
  @@map("categories")
}

model Product {
  id          String   @id @default(cuid())
  name        String
  slug        String   @unique
  description String?
  price       Decimal  @db.Decimal(10, 2)
  sku         String   @unique
  stock       Int      @default(0)
  isActive    Boolean  @default(true)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  deletedAt   DateTime?

  categoryIds String[]
  categories  Category[]
  orderItems  OrderItem[]
  cartItems   CartItem[]
  reviews     Review[]

  @@index([sku])
  @@index([slug])
  @@index([isActive, price])
  @@map("products")
}

model Order {
  id          String      @id @default(cuid())
  userId      String
  status      OrderStatus @default(PENDING)
  total       Decimal     @db.Decimal(10, 2)
  createdAt   DateTime    @default(now())
  updatedAt   DateTime    @updatedAt
  deletedAt   DateTime?

  user  User        @relation(fields: [userId], references: [id])
  items OrderItem[]

  @@index([userId, status])
  @@index([createdAt])
  @@map("orders")
}

model OrderItem {
  id        String  @id @default(cuid())
  orderId   String
  productId String
  quantity  Int
  unitPrice Decimal @db.Decimal(10, 2)

  order   Order   @relation(fields: [orderId], references: [id], onDelete: Cascade)
  product Product @relation(fields: [productId], references: [id])

  @@index([orderId])
  @@index([productId])
  @@map("order_items")
}

model Review {
  id        String   @id @default(cuid())
  userId    String
  productId String
  rating    Int
  title     String?
  body      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  deletedAt DateTime?

  user    User    @relation(fields: [userId], references: [id])
  product Product @relation(fields: [productId], references: [id])

  @@unique([userId, productId])
  @@index([productId, rating])
  @@map("reviews")
}

model CartItem {
  id        String  @id @default(cuid())
  userId    String
  productId String
  quantity  Int     @default(1)

  user    User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  product Product @relation(fields: [productId], references: [id])

  @@unique([userId, productId])
  @@map("cart_items")
}

enum Role {
  CUSTOMER
  ADMIN
  VENDOR
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}
```

```typescript
// src/lib/prisma-client.ts
import { PrismaClient, Prisma } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

function createPrismaClient(): PrismaClient {
  const client = new PrismaClient({
    log:
      process.env.NODE_ENV === "development"
        ? ["query", "error", "warn"]
        : ["error"],
  });

  // Soft-delete middleware: automatically filters out deleted records
  client.$use(async (params, next) => {
    const skipPaths = ["$connect", "$disconnect", "$transaction"];

    if (skipPaths.includes(params.action)) {
      return next(params);
    }

    const modelName = params.model ?? "";

    if (params.action === "findFirst" || params.action === "findMany" || params.action === "count" || params.action === "aggregate") {
      params.args.where = {
        ...params.args.where,
        deletedAt: null,
      };
    }

    if (params.action === "findUnique") {
      params.action = "findFirst";
      params.args.where = {
        ...params.args.where,
        deletedAt: null,
      };
    }

    if (params.action === "update") {
      params.action = "updateMany";
      params.args.where = {
        ...params.args.where,
        deletedAt: null,
      };
    }

    if (params.action === "delete") {
      // Convert to soft-delete
      params.action = "updateMany";
      params.args.data = {
        deletedAt: new Date(),
        updatedAt: new Date(),
      };
    }

    return next(params);
  });

  // Query logging in development
  client.$on("query", (e) => {
    if (e.duration > 200) {
      console.warn(`[SLOW QUERY] ${e.query} (${e.duration}ms)`);
    }
  });

  return client;
}

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

// Soft-delete helper: allows querying deleted records when needed
export function withDeletedArgs<T extends Record<string, unknown>>(
  args: T
): T & { deletedAt?: null } {
  const { deletedAt: _, ...rest } = args as { deletedAt?: unknown };
  return rest as T;
}

// Hard-delete for admin operations
export async function hardDelete<T>(
  model: { delete: (args: { where: { id: string } }) => Promise<T> },
  id: string
): Promise<T> {
  return model.delete({ where: { id } });
}
```

```typescript
// src/repositories/prisma-repo.ts
import { prisma } from "../lib/prisma-client";
import {
  Prisma,
  User,
  Product,
  Order,
  Category,
  Review,
} from "@prisma/client";

// ── User Repository ──────────────────────────────────────────────

export async function findUserByEmail(email: string) {
  return prisma.user.findFirst({ where: { email } });
}

export async function createUser(data: Prisma.UserCreateInput) {
  return prisma.user.create({ data });
}

export async function findUserWithProfile(userId: string) {
  return prisma.user.findFirst({
    where: { id: userId },
    include: { profile: true },
  });
}

export async function updateUserProfile(
  userId: string,
  data: Prisma.UserProfileUpdateInput
) {
  return prisma.userProfile.upsert({
    where: { userId },
    update: data,
    create: { userId, ...data },
  });
}

// ── Product Repository ───────────────────────────────────────────

export async function listProducts(params: {
  skip?: number;
  take?: number;
  categoryId?: string;
  minPrice?: number;
  maxPrice?: number;
  search?: string;
}) {
  const { skip = 0, take = 20, categoryId, minPrice, maxPrice, search } = params;

  const where: Prisma.ProductWhereInput = {
    isActive: true,
  };

  if (categoryId) {
    where.categoryIds = { has: categoryId };
  }

  if (minPrice !== undefined || maxPrice !== undefined) {
    where.price = {};
    if (minPrice !== undefined) (where.price as Prisma.DecimalFilter).gte = minPrice;
    if (maxPrice !== undefined) (where.price as Prisma.DecimalFilter).lte = maxPrice;
  }

  if (search) {
    where.OR = [
      { name: { contains: search, mode: "insensitive" } },
      { description: { contains: search, mode: "insensitive" } },
    ];
  }

  const [products, total] = await Promise.all([
    prisma.product.findMany({
      where,
      skip,
      take,
      include: { categories: true },
      orderBy: { createdAt: "desc" },
    }),
    prisma.product.count({ where }),
  ]);

  return { products, total, hasMore: skip + take < total };
}

export async function createProduct(data: Prisma.ProductCreateInput) {
  return prisma.product.create({ data });
}

export async function updateProduct(
  id: string,
  data: Prisma.ProductUpdateInput
) {
  return prisma.product.update({ where: { id }, data });
}

export async function softDeleteProduct(id: string) {
  return prisma.product.update({
    where: { id },
    data: { deletedAt: new Date(), isActive: false },
  });
}

// ── Order Repository ─────────────────────────────────────────────

export async function createOrder(data: {
  userId: string;
  items: Array<{ productId: string; quantity: number; unitPrice: number }>;
}) {
  const total = data.items.reduce(
    (sum, item) => sum + item.unitPrice * item.quantity,
    0
  );

  return prisma.$transaction(async (tx) => {
    const order = await tx.order.create({
      data: {
        userId: data.userId,
        total,
        items: {
          create: data.items.map((item) => ({
            productId: item.productId,
            quantity: item.quantity,
            unitPrice: item.unitPrice,
          })),
        },
      },
      include: { items: true },
    });

    // Decrement stock for each product
    for (const item of data.items) {
      await tx.product.update({
        where: { id: item.productId },
        data: { stock: { decrement: item.quantity } },
      });
    }

    return order;
  });
}

export async function listOrdersByUser(userId: string) {
  return prisma.order.findMany({
    where: { userId },
    include: {
      items: {
        include: { product: true },
      },
    },
    orderBy: { createdAt: "desc" },
  });
}

// ── Category Repository ──────────────────────────────────────────

export async function getCategoryTree() {
  const categories = await prisma.category.findMany({
    where: { parentId: null },
    include: {
      children: {
        include: {
          children: true,
        },
      },
    },
  });
  return categories;
}

export async function createCategory(data: Prisma.CategoryCreateInput) {
  return prisma.category.create({ data });
}

// ── Review Repository ────────────────────────────────────────────

export async function getProductReviews(productId: string) {
  return prisma.review.findMany({
    where: { productId },
    include: { user: { select: { id: true, name: true, profile: { select: { avatar: true } } } } },
    orderBy: { createdAt: "desc" },
  });
}

export async function upsertReview(data: {
  userId: string;
  productId: string;
  rating: number;
  title?: string;
  body?: string;
}) {
  return prisma.review.upsert({
    where: {
      userId_productId: {
        userId: data.userId,
        productId: data.productId,
      },
    },
    update: {
      rating: data.rating,
      title: data.title,
      body: data.body,
    },
    create: data,
  });
}
```

### 3B — Drizzle Variant

```typescript
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/drizzle/schema.ts",
  out: "./src/drizzle/migrations",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  verbose: true,
  strict: true,
});
```

```typescript
// src/drizzle/schema.ts
import {
  pgTable,
  text,
  varchar,
  integer,
  decimal,
  boolean,
  timestamp,
  jsonb,
  uniqueIndex,
  index,
  primaryKey,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

// ── Users ────────────────────────────────────────────────────────

export const users = pgTable(
  "users",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    email: varchar("email", { length: 255 }).notNull().unique(),
    name: varchar("name", { length: 255 }).notNull(),
    role: varchar("role", { length: 50 }).notNull().default("CUSTOMER"),
    createdAt: timestamp("created_at", { withTimezone: true })
      .notNull()
      .defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
    deletedAt: timestamp("deleted_at", { withTimezone: true }),
  },
  (table) => [
    index("idx_users_email").on(table.email),
    index("idx_users_role").on(table.role),
  ]
);

export const userProfiles = pgTable(
  "user_profiles",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    userId: text("user_id")
      .notNull()
      .unique()
      .references(() => users.id, { onDelete: "cascade" }),
    avatar: text("avatar"),
    bio: text("bio"),
    phone: varchar("phone", { length: 30 }),
    address: jsonb("address"),
  },
  (table) => [index("idx_user_profiles_user_id").on(table.userId)]
);

// ── Categories (self-referential tree) ───────────────────────────

export const categories = pgTable(
  "categories",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    name: varchar("name", { length: 255 }).notNull().unique(),
    slug: varchar("slug", { length: 255 }).notNull().unique(),
    parentId: text("parent_id").references((): any => categories.id, {
      onDelete: "set null",
    }),
    createdAt: timestamp("created_at", { withTimezone: true })
      .notNull()
      .defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
    deletedAt: timestamp("deleted_at", { withTimezone: true }),
  },
  (table) => [
    index("idx_categories_parent_id").on(table.parentId),
    index("idx_categories_slug").on(table.slug),
  ]
);

// ── Products ─────────────────────────────────────────────────────

export const products = pgTable(
  "products",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    name: varchar("name", { length: 255 }).notNull(),
    slug: varchar("slug", { length: 255 }).notNull().unique(),
    description: text("description"),
    price: decimal("price", { precision: 10, scale: 2 }).notNull(),
    sku: varchar("sku", { length: 100 }).notNull().unique(),
    stock: integer("stock").notNull().default(0),
    isActive: boolean("is_active").notNull().default(true),
    createdAt: timestamp("created_at", { withTimezone: true })
      .notNull()
      .defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
    deletedAt: timestamp("deleted_at", { withTimezone: true }),
  },
  (table) => [
    index("idx_products_sku").on(table.sku),
    index("idx_products_slug").on(table.slug),
    index("idx_products_active_price").on(table.isActive, table.price),
  ]
);

// ── Product ↔ Category junction (M:N) ───────────────────────────

export const productCategories = pgTable(
  "product_categories",
  {
    productId: text("product_id")
      .notNull()
      .references(() => products.id, { onDelete: "cascade" }),
    categoryId: text("category_id")
      .notNull()
      .references(() => categories.id, { onDelete: "cascade" }),
  },
  (t) => [primaryKey({ columns: [t.productId, t.categoryId] })]
);

// ── Orders ───────────────────────────────────────────────────────

export const orders = pgTable(
  "orders",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    userId: text("user_id")
      .notNull()
      .references(() => users.id, { onDelete: "restrict" }),
    status: varchar("status", { length: 50 }).notNull().default("PENDING"),
    total: decimal("total", { precision: 10, scale: 2 }).notNull(),
    createdAt: timestamp("created_at", { withTimezone: true })
      .notNull()
      .defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
    deletedAt: timestamp("deleted_at", { withTimezone: true }),
  },
  (table) => [
    index("idx_orders_user_status").on(table.userId, table.status),
    index("idx_orders_created_at").on(table.createdAt),
  ]
);

// ── Order Items ──────────────────────────────────────────────────

export const orderItems = pgTable(
  "order_items",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    orderId: text("order_id")
      .notNull()
      .references(() => orders.id, { onDelete: "cascade" }),
    productId: text("product_id")
      .notNull()
      .references(() => products.id, { onDelete: "restrict" }),
    quantity: integer("quantity").notNull(),
    unitPrice: decimal("unit_price", { precision: 10, scale: 2 }).notNull(),
  },
  (table) => [
    index("idx_order_items_order_id").on(table.orderId),
    index("idx_order_items_product_id").on(table.productId),
  ]
);

// ── Reviews ──────────────────────────────────────────────────────

export const reviews = pgTable(
  "reviews",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    userId: text("user_id")
      .notNull()
      .references(() => users.id, { onDelete: "restrict" }),
    productId: text("product_id")
      .notNull()
      .references(() => products.id, { onDelete: "restrict" }),
    rating: integer("rating").notNull(),
    title: varchar("title", { length: 255 }),
    body: text("body"),
    createdAt: timestamp("created_at", { withTimezone: true })
      .notNull()
      .defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true })
      .notNull()
      .defaultNow()
      .$onUpdate(() => new Date()),
    deletedAt: timestamp("deleted_at", { withTimezone: true }),
  },
  (table) => [
    uniqueIndex("idx_reviews_user_product").on(table.userId, table.productId),
    index("idx_reviews_product_rating").on(table.productId, table.rating),
  ]
);

// ── Cart Items ───────────────────────────────────────────────────

export const cartItems = pgTable(
  "cart_items",
  {
    id: text("id")
      .primaryKey()
      .$defaultFn(() => crypto.randomUUID()),
    userId: text("user_id")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
    productId: text("product_id")
      .notNull()
      .references(() => products.id, { onDelete: "cascade" }),
    quantity: integer("quantity").notNull().default(1),
  },
  (table) => [
    uniqueIndex("idx_cart_items_user_product").on(table.userId, table.productId),
  ]
);

// ── Relations ────────────────────────────────────────────────────

export const usersRelations = relations(users, ({ one, many }) => ({
  profile: one(userProfiles, {
    fields: [users.id],
    references: [userProfiles.userId],
  }),
  orders: many(orders),
  reviews: many(reviews),
  cartItems: many(cartItems),
}));

export const userProfilesRelations = relations(userProfiles, ({ one }) => ({
  user: one(users, {
    fields: [userProfiles.userId],
    references: [users.id],
  }),
}));

export const categoriesRelations = relations(categories, ({ one, many }) => ({
  parent: one(categories, {
    fields: [categories.parentId],
    references: [categories.id],
    relationName: "CategoryTree",
  }),
  children: many(categories, { relationName: "CategoryTree" }),
  productCategories: many(productCategories),
}));

export const productsRelations = relations(products, ({ many }) => ({
  productCategories: many(productCategories),
  orderItems: many(orderItems),
  reviews: many(reviews),
  cartItems: many(cartItems),
}));

export const productCategoriesRelations = relations(
  productCategories,
  ({ one }) => ({
    product: one(products, {
      fields: [productCategories.productId],
      references: [products.id],
    }),
    category: one(categories, {
      fields: [productCategories.categoryId],
      references: [categories.id],
    }),
  })
);

export const ordersRelations = relations(orders, ({ one, many }) => ({
  user: one(users, {
    fields: [orders.userId],
    references: [users.id],
  }),
  items: many(orderItems),
}));

export const orderItemsRelations = relations(orderItems, ({ one }) => ({
  order: one(orders, {
    fields: [orderItems.orderId],
    references: [orders.id],
  }),
  product: one(products, {
    fields: [orderItems.productId],
    references: [products.id],
  }),
}));

export const reviewsRelations = relations(reviews, ({ one }) => ({
  user: one(users, {
    fields: [reviews.userId],
    references: [users.id],
  }),
  product: one(products, {
    fields: [reviews.productId],
    references: [products.id],
  }),
}));

export const cartItemsRelations = relations(cartItems, ({ one }) => ({
  user: one(users, {
    fields: [cartItems.userId],
    references: [users.id],
  }),
  product: one(products, {
    fields: [cartItems.productId],
    references: [products.id],
  }),
}));
```

```typescript
// src/drizzle/client.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";

const globalForDrizzle = globalThis as unknown as { drizzlePool: Pool };

function createPool(): Pool {
  if (globalForDrizzle.drizzlePool) return globalForDrizzle.drizzlePool;

  const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
    max: 20,
    idleTimeoutMillis: 30_000,
    connectionTimeoutMillis: 10_000,
  });

  pool.on("error", (err) => {
    console.error("[DRIZZLE POOL] Unexpected error on idle client:", err);
  });

  if (process.env.NODE_ENV !== "production") {
    globalForDrizzle.drizzlePool = pool;
  }

  return pool;
}

export const db = drizzle(createPool(), { schema });

// ── Soft-delete filter helper ────────────────────────────────────

import { eq, isNull, SQL, and } from "drizzle-orm";

type SoftDeletable = { deletedAt: any };

export function notDeleted<T extends SoftDeletable>(
  table: T
): SQL | undefined {
  return isNull(table.deletedAt);
}

export function softDeleteById<T extends { deletedAt: any; id: any }>(
  table: T,
  id: string
) {
  return db
    .update(table)
    .set({ deletedAt: new Date() } as any)
    .where(eq((table as any).id, id));
}
```

```typescript
// src/repositories/drizzle-repo.ts
import { db } from "../drizzle/client";
import { eq, desc, and, sql, isNull, inArray } from "drizzle-orm";
import {
  users,
  userProfiles,
  categories,
  products,
  productCategories,
  orders,
  orderItems,
  reviews,
} from "../drizzle/schema";

// ── User Repository ──────────────────────────────────────────────

export async function findUserByEmail(email: string) {
  const rows = await db
    .select()
    .from(users)
    .where(and(eq(users.email, email), isNull(users.deletedAt)))
    .limit(1);

  return rows[0] ?? null;
}

export async function createUser(data: typeof users.$inferInsert) {
  const [row] = await db.insert(users).values(data).returning();
  return row;
}

export async function findUserWithProfile(userId: string) {
  const rows = await db
    .select({
      user: users,
      profile: userProfiles,
    })
    .from(users)
    .leftJoin(userProfiles, eq(users.id, userProfiles.userId))
    .where(and(eq(users.id, userId), isNull(users.deletedAt)))
    .limit(1);

  if (rows.length === 0) return null;

  return { ...rows[0].user, profile: rows[0].profile };
}

export async function upsertUserProfile(
  userId: string,
  data: Partial<typeof userProfiles.$inferInsert>
) {
  const existing = await db
    .select({ id: userProfiles.id })
    .from(userProfiles)
    .where(eq(userProfiles.userId, userId))
    .limit(1);

  if (existing.length > 0) {
    const [row] = await db
      .update(userProfiles)
      .set(data)
      .where(eq(userProfiles.userId, userId))
      .returning();
    return row;
  }

  const [row] = await db
    .insert(userProfiles)
    .values({ userId, ...data } as typeof userProfiles.$inferInsert)
    .returning();
  return row;
}

// ── Product Repository ───────────────────────────────────────────

export async function listProducts(params: {
  skip?: number;
  take?: number;
  categoryId?: string;
  minPrice?: number;
  maxPrice?: number;
  search?: string;
}) {
  const { skip = 0, take = 20, categoryId, minPrice, maxPrice, search } = params;

  const conditions = [isNull(products.deletedAt), eq(products.isActive, true)];

  if (minPrice !== undefined) {
    conditions.push(sql`${products.price} >= ${minPrice.toString()}`);
  }
  if (maxPrice !== undefined) {
    conditions.push(sql`${products.price} <= ${maxPrice.toString()}`);
  }
  if (search) {
    conditions.push(
      sql`(${products.name} ILIKE ${`%${search}%`} OR ${products.description} ILIKE ${`%${search}%`})`
    );
  }

  const rows = await db
    .select()
    .from(products)
    .where(and(...conditions))
    .orderBy(desc(products.createdAt))
    .limit(take)
    .offset(skip);

  const countRows = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(products)
    .where(and(...conditions));

  const total = countRows[0]?.count ?? 0;

  // Attach categories for each product
  if (rows.length > 0) {
    const productIds = rows.map((p) => p.id);
    const catRows = await db
      .select({
        productId: productCategories.productId,
        category: categories,
      })
      .from(productCategories)
      .innerJoin(categories, eq(productCategories.categoryId, categories.id))
      .where(inArray(productCategories.productId, productIds));

    const catMap = new Map<string, typeof categories.$inferSelect[]>();
    for (const row of catRows) {
      const list = catMap.get(row.productId) ?? [];
      list.push(row.category);
      catMap.set(row.productId, list);
    }

    for (const product of rows) {
      (product as any).categories = catMap.get(product.id) ?? [];
    }
  }

  return { products: rows, total, hasMore: skip + take < total };
}

export async function createProduct(data: typeof products.$inferInsert) {
  const [row] = await db.insert(products).values(data).returning();
  return row;
}

export async function addProductCategory(
  productId: string,
  categoryId: string
) {
  return db
    .insert(productCategories)
    .values({ productId, categoryId })
    .onConflictDoNothing();
}

export async function softDeleteProduct(id: string) {
  const [row] = await db
    .update(products)
    .set({ deletedAt: new Date(), isActive: false })
    .where(eq(products.id, id))
    .returning();
  return row;
}

// ── Order Repository ─────────────────────────────────────────────

export async function createOrder(data: {
  userId: string;
  items: Array<{ productId: string; quantity: number; unitPrice: number }>;
}) {
  return db.transaction(async (tx) => {
    const total = data.items
      .reduce((sum, i) => sum + Number(i.unitPrice) * i.quantity, 0)
      .toString();

    const [order] = await tx
      .insert(orders)
      .values({ userId: data.userId, total })
      .returning();

    const items = await tx
      .insert(orderItems)
      .values(
        data.items.map((item) => ({
          orderId: order.id,
          productId: item.productId,
          quantity: item.quantity,
          unitPrice: item.unitPrice.toString(),
        }))
      )
      .returning();

    for (const item of data.items) {
      await tx
        .update(products)
        .set({ stock: sql`${products.stock} - ${item.quantity}` })
        .where(eq(products.id, item.productId));
    }

    return { ...order, items };
  });
}

export async function listOrdersByUser(userId: string) {
  const orderRows = await db
    .select()
    .from(orders)
    .where(and(eq(orders.userId, userId), isNull(orders.deletedAt)))
    .orderBy(desc(orders.createdAt));

  const orderIds = orderRows.map((o) => o.id);
  if (orderIds.length === 0) return [];

  const itemRows = await db
    .select({
      orderItem: orderItems,
      product: products,
    })
    .from(orderItems)
    .innerJoin(products, eq(orderItems.productId, products.id))
    .where(inArray(orderItems.orderId, orderIds));

  const itemsMap = new Map<string, typeof itemRows>();
  for (const row of itemRows) {
    const list = itemsMap.get(row.orderItem.orderId) ?? [];
    list.push(row);
    itemsMap.set(row.orderItem.orderId, list);
  }

  return orderRows.map((order) => ({
    ...order,
    items: itemsMap.get(order.id) ?? [],
  }));
}

// ── Category Repository ──────────────────────────────────────────

export async function getCategoryTree() {
  const all = await db
    .select()
    .from(categories)
    .where(isNull(categories.deletedAt))
    .orderBy(categories.name);

  const map = new Map<string, any>();
  const roots: any[] = [];

  for (const cat of all) {
    map.set(cat.id, { ...cat, children: [] });
  }

  for (const cat of all) {
    const node = map.get(cat.id)!;
    if (cat.parentId && map.has(cat.parentId)) {
      map.get(cat.parentId)!.children.push(node);
    } else {
      roots.push(node);
    }
  }

  return roots;
}

export async function createCategory(data: typeof categories.$inferInsert) {
  const [row] = await db.insert(categories).values(data).returning();
  return row;
}

// ── Review Repository ────────────────────────────────────────────

export async function getProductReviews(productId: string) {
  const rows = await db
    .select({
      review: reviews,
      user: { id: users.id, name: users.name },
      profile: { avatar: userProfiles.avatar },
    })
    .from(reviews)
    .innerJoin(users, eq(reviews.userId, users.id))
    .leftJoin(userProfiles, eq(users.id, userProfiles.userId))
    .where(
      and(eq(reviews.productId, productId), isNull(reviews.deletedAt))
    )
    .orderBy(desc(reviews.createdAt));

  return rows.map((row) => ({
    ...row.review,
    user: { ...row.user, profile: row.profile },
  }));
}

export async function upsertReview(data: {
  userId: string;
  productId: string;
  rating: number;
  title?: string;
  body?: string;
}) {
  const existing = await db
    .select({ id: reviews.id })
    .from(reviews)
    .where(
      and(eq(reviews.userId, data.userId), eq(reviews.productId, data.productId))
    )
    .limit(1);

  if (existing.length > 0) {
    const [row] = await db
      .update(reviews)
      .set({
        rating: data.rating,
        title: data.title ?? null,
        body: data.body ?? null,
      })
      .where(eq(reviews.id, existing[0].id))
      .returning();
    return row;
  }

  const [row] = await db.insert(reviews).values(data).returning();
  return row;
}
```

## 4. Execution Protocol & Step-by-Step Workflow

### Prisma Setup

```bash
# 1. Install dependencies
npm install prisma @prisma/client
npm install -D typescript @types/node tsx

# 2. Initialize Prisma (creates prisma/ directory)
npx prisma init --datasource-provider postgresql

# 3. Apply schema (writes migration SQL)
npx prisma migrate dev --name init

# 4. Generate Prisma Client
npx prisma generate

# 5. Run project
npx tsx src/index.ts

# 6. View data in browser
npx prisma studio
```

### Drizzle Setup

```bash
# 1. Install dependencies
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg typescript tsx

# 2. Configure (already written to drizzle.config.ts)
# drizzle.config.ts is placed at project root

# 3. Generate migration files
npx drizzle-kit generate

# 4. Push schema directly to database (skip migrations for dev)
npx drizzle-kit push

# 5. Open Drizzle Studio for visual inspection
npx drizzle-kit studio

# 6. Run project
npx tsx src/index.ts
```

## 5. Edge Cases & Error Handling

- **Concurrent soft-deletes:** Two users may soft-delete the same record; the `updatedAt` field acts as a conflict detector — always check `updatedAt` before proceeding with updates.
- **Circular category references:** The self-referential `parentId` can form cycles; validate in application code before inserting. Consider adding a maximum depth check (e.g., 5 levels).
- **Prisma middleware ordering:** The soft-delete middleware must be registered before any queries execute — place it at client creation time, not per-request.
- **Drizzle transaction isolation:** Use `db.transaction()` for multi-table writes (order + items + stock) to ensure atomicity; Prisma handles this with `$transaction`.
- **Migration conflicts:** When both ORMs modify the same table, use `--create-only` flags and review generated SQL before applying; never auto-push to production.
- **Decimal precision:** PostgreSQL `DECIMAL(10,2)` truncates beyond 2 decimal places; always round in application code before writing to avoid silent data loss.
- **Stock race conditions:** Use `UPDATE ... WHERE stock >= quantity` with returned row-count to detect overselling rather than blind decrement.
- **Schema drift:** Periodically run `npx prisma validate` and `npx drizzle-kit check` to detect drift between ORM definitions and actual database state.
- **Performance:** The composite index `@@index([userId, status])` on orders is critical for the order-listing query pattern — without it, a full table scan occurs at scale.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.