drizzle-query-patterns
Drizzle ORM query patterns for this project — base repository, soft delete, NON_PASSWORD_COLUMNS, query builder conventions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Drizzle ORM query patterns for this project — base repository, soft delete, NON_PASSWORD_COLUMNS, query builder conventions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Awilix dependency injection patterns for this Fastify project — auto-loading conventions, Cradle usage, partial application, type declarations.
CQS (Command Query Separation) patterns — mutations for writes with events, queries for reads without side effects.
Safe Drizzle ORM migration patterns for PostgreSQL. Auto-loads on migration files. Mandatory reading before writing or editing a migration.
Fastify route handler patterns — TypeBox validation, DI access, schema definitions, JWT guards, error handling conventions.
E2E testing patterns using Node.js native test runner — createTestingApp, createDbHelper, factories, app.inject() for HTTP testing.
| name | drizzle-query-patterns |
| description | Drizzle ORM query patterns for this project — base repository, soft delete, NON_PASSWORD_COLUMNS, query builder conventions. |
| globs | ["src/modules/**/*.repository.ts","src/modules/**/*.queries.ts","src/infra/database/**/*.ts","src/libs/persistence/**/*.ts"] |
| allowed-tools | Read, Write, Edit, Grep, Glob, Bash(pnpm:*) |
Use createBaseRepository from #libs/persistence/base-repository.ts for standard CRUD:
import { createBaseRepository } from "#libs/persistence/base-repository.ts";
export default function usersRepository(deps: Cradle) {
const baseRepo = createBaseRepository<typeof users, User, UserInsert>({
table: users,
logger: deps.logger,
db: deps.db,
defaultSelectColumns: NON_PASSWORD_COLUMNS,
softDeleteColumn: "deletedAt",
});
return {
createOne: baseRepo.createOne,
findOneById: baseRepo.findOneById,
softDeleteOneById: baseRepo.softDeleteOneById,
// Custom methods below...
findOneByEmail: partial(findOneByEmail, [deps]),
};
}
Base repository provides: createOne, createMany, findOneById, findManyByIds, softDeleteOneById, softDeleteManyByIds, deleteOneById, deleteManyByIds.
Never return password in queries:
import { getTableColumns } from "drizzle-orm";
import { omit } from "rambda";
export const NON_PASSWORD_COLUMNS = omit(["password"], getTableColumns(users));
Use in custom queries:
const [user] = await db
.select(NON_PASSWORD_COLUMNS)
.from(users)
.where(and(eq(users.email, email), isNull(users.deletedAt)));
Every query on soft-deletable tables MUST include isNull(deletedAt):
import { and, eq, isNull } from "drizzle-orm";
// Base repository handles this automatically via softDeleteColumn option
// Custom queries MUST add it manually:
const [user] = await db
.select(NON_PASSWORD_COLUMNS)
.from(users)
.where(and(eq(users.id, id), isNull(users.deletedAt)));
const findOneByEmail = async ({ db }: Cradle, email: string): Promise<undefined | User> => {
const [maybeUser] = await db
.select(NON_PASSWORD_COLUMNS)
.from(users)
.where(and(eq(users.email, email), isNull(users.deletedAt)));
return maybeUser;
};
Key points:
undefined | Entity for single results (not null)isNull(deletedAt) for soft-deletable tablesNON_PASSWORD_COLUMNS for user queriesconst updateOneById = async (
{ db, dateTimeService }: Cradle,
id: UUID,
data: Partial<Omit<UserInsert, "id" | "createdAt" | "updatedAt" | "deletedAt">>,
): Promise<undefined | User> => {
const [updated] = await db
.update(users)
.set({ ...data, updatedAt: dateTimeService.now() })
.where(and(eq(users.id, id), isNull(users.deletedAt)))
.returning(NON_PASSWORD_COLUMNS);
return updated;
};
export const users = pgTable(
TABLE_NAMES.users,
{
id: uuid("id").$type<UUID>().primaryKey().notNull().defaultRandom(),
email: varchar("email", { length: 256 }).notNull(),
createdAt: timestamp("created_at", { mode: "string" }).$type<DateTimeString>().defaultNow().notNull(),
updatedAt: timestamp("updated_at", { mode: "string" }).$type<DateTimeString>().defaultNow().notNull(),
deletedAt: timestamp("deleted_at", { mode: "string" }).$type<DateTimeString>(),
},
(table) => [
uniqueIndex("users_email_unique_idx").on(table.email),
index("users_deleted_at_email_idx").on(table.deletedAt, table.email),
],
);
For request-scoped transactions, use getTransactionContext() from #libs/persistence/transaction-context.ts. The base repository handles this automatically via getDb().
NON_PASSWORD_COLUMNS)isNull(deletedAt) in custom queries