npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wordrhyme/auto-crud-server

v1.0.1

Published

tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM

Readme

@wordrhyme/auto-crud-server

tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM

为 Drizzle ORM 表自动生成 tRPC CRUD 路由,支持分页、排序、高级过滤。

npm version License: MIT


✨ 特性

  • 🚀 零代码 CRUD: 一行代码生成完整 CRUD 路由
  • 🔒 类型安全: 完整的 TypeScript 类型推断
  • 🎯 高级过滤: 支持多条件、多操作符(等于、包含、范围等)
  • 📊 排序分页: 内置分页和多字段排序支持
  • 🔌 Drizzle 集成: 无缝集成 Drizzle ORM
  • 🌐 tRPC 原生: 基于 tRPC v11 构建
  • 📦 零依赖: 所有依赖都是 peer dependencies

📦 安装

# pnpm
pnpm add @wordrhyme/auto-crud-server

# npm
npm install @wordrhyme/auto-crud-server

# yarn
yarn add @wordrhyme/auto-crud-server

Peer Dependencies

{
  "@trpc/server": "^11.0.0",
  "drizzle-orm": "^0.40.0",
  "zod": "^3.0.0 || ^4.0.0"
}

🚀 快速开始

步骤 1: 定义数据库 Schema

// src/db/schema.ts
import { pgTable, varchar, real, boolean, timestamp } from "drizzle-orm/pg-core";

export const tasks = pgTable("tasks", {
  id: varchar("id", { length: 30 }).primaryKey(),
  title: varchar("title", { length: 128 }).notNull(),
  status: varchar("status", {
    enum: ["todo", "in-progress", "done", "canceled"],
  }).notNull().default("todo"),
  priority: varchar("priority", {
    enum: ["low", "medium", "high"],
  }).notNull().default("low"),
  estimatedHours: real("estimated_hours").default(0),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

步骤 2: 创建 CRUD 路由

// src/server/routers/tasks.ts
import { createCrudRouter } from "@wordrhyme/auto-crud-server";
import { tasks } from "@/db/schema";

// 🚀 零配置!一行代码生成完整 CRUD 路由
export const tasksRouter = createCrudRouter({
  table: tasks,
  // Schema 自动从 table 派生,排除 id, createdAt, updatedAt
});

或者,显式传入 Schema:

import { createCrudRouter } from "@wordrhyme/auto-crud-server";
import { tasks } from "@/db/schema";
import { createSelectSchema } from "drizzle-zod";

// 从 Drizzle Schema 自动生成 Zod Schema
const taskSchema = createSelectSchema(tasks).omit({
  id: true,
  createdAt: true,
  updatedAt: true,
});

export const tasksRouter = createCrudRouter({
  table: tasks,
  schema: taskSchema,  // 主 Schema(用于 create/upsert)
  // updateSchema 自动派生为 schema.partial()
});

步骤 3: 汇总路由

// src/server/routers/index.ts
import { router } from "../trpc";
import { tasksRouter } from "./tasks";

export const appRouter = router({
  tasks: tasksRouter,
});

export type AppRouter = typeof appRouter;

步骤 4: 创建 API 路由处理器

// src/app/api/trpc/[trpc]/route.ts (Next.js App Router)
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers";
import { db } from "@/db";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => ({ db }),
  });

export { handler as GET, handler as POST };

📖 自动生成的路由

createCrudRouter 会自动生成以下 8 个路由:

1. list - 列表查询

输入:

{
  page: number;           // 页码(从 1 开始)
  perPage: number;        // 每页条数
  sort?: Array<{          // 排序
    id: string;           // 字段名
    desc: boolean;        // 是否降序
  }>;
  filters?: Array<{       // 过滤条件
    id: string;           // 字段名
    value: any;           // 值
    operator: string;     // 操作符
    variant: string;      // 筛选器类型
  }>;
  joinOperator?: "and" | "or";  // 多条件连接方式
}

输出:

{
  data: Task[];           // 数据列表
  total: number;          // 总条数
}

示例:

const result = await trpc.tasks.list({
  page: 1,
  perPage: 10,
  sort: [{ id: "createdAt", desc: true }],
  filters: [
    { id: "status", value: "done", operator: "eq", variant: "select" },
    { id: "priority", value: "high", operator: "eq", variant: "select" },
  ],
  joinOperator: "and",
});

2. get - 单条查询

输入:

{ id: string }

输出:

Task

示例:

const task = await trpc.tasks.get({ id: "123" });

3. create - 创建

输入:

Omit<Task, "id" | "createdAt" | "updatedAt">

输出:

Task

示例:

const newTask = await trpc.tasks.create({
  title: "New Task",
  status: "todo",
  priority: "high",
});

4. update - 更新

输入:

{
  id: string;
  data: Partial<Omit<Task, "id" | "createdAt" | "updatedAt">>;
}

输出:

Task

示例:

const updatedTask = await trpc.tasks.update({
  id: "123",
  data: { status: "done" },
});

5. delete - 删除

输入:

{ id: string }

输出:

void

示例:

await trpc.tasks.delete({ id: "123" });

6. deleteMany - 批量删除

输入:

{ ids: string[] }

输出:

void

示例:

await trpc.tasks.deleteMany({ ids: ["1", "2", "3"] });

7. updateMany - 批量更新

输入:

{
  ids: string[];
  data: Partial<Omit<Task, "id" | "createdAt" | "updatedAt">>;
}

输出:

{ updated: number }

示例:

await trpc.tasks.updateMany({
  ids: ["1", "2", "3"],
  data: { status: "done" },
});

8. upsert - 存在则更新,不存在则创建

输入:

Omit<Task, "createdAt" | "updatedAt">  // 需要包含 id

输出:

{
  data: Task;       // 创建或更新后的记录
  isNew: boolean;   // true = 新建, false = 更新
}

示例:

// 如果 id="123" 存在则更新,不存在则创建
const { data, isNew } = await trpc.tasks.upsert({
  id: "123",
  title: "My Task",
  status: "todo",
});

if (isNew) {
  console.log("Created new task");
} else {
  console.log("Updated existing task");
}

适用场景

  • 同步外部数据
  • 幂等导入
  • 配置项更新

🎯 高级过滤

支持的操作符

| 操作符 | 说明 | 示例 | |--------|------|------| | eq | 等于 | status = "done" | | ne | 不等于 | status != "canceled" | | gt | 大于 | estimatedHours > 5 | | gte | 大于等于 | estimatedHours >= 5 | | lt | 小于 | estimatedHours < 10 | | lte | 小于等于 | estimatedHours <= 10 | | like | 包含 | title LIKE "%bug%" | | notLike | 不包含 | title NOT LIKE "%test%" | | in | 在列表中 | status IN ["todo", "in-progress"] | | notIn | 不在列表中 | status NOT IN ["canceled"] | | between | 范围 | createdAt BETWEEN "2024-01-01" AND "2024-12-31" | | isNull | 为空 | description IS NULL | | isNotNull | 不为空 | description IS NOT NULL |

过滤示例

单条件过滤

await trpc.tasks.list({
  page: 1,
  perPage: 10,
  filters: [
    { id: "status", value: "done", operator: "eq", variant: "select" },
  ],
});

多条件 AND 过滤

await trpc.tasks.list({
  page: 1,
  perPage: 10,
  filters: [
    { id: "status", value: "done", operator: "eq", variant: "select" },
    { id: "priority", value: "high", operator: "eq", variant: "select" },
  ],
  joinOperator: "and",  // status = "done" AND priority = "high"
});

多条件 OR 过滤

await trpc.tasks.list({
  page: 1,
  perPage: 10,
  filters: [
    { id: "status", value: "todo", operator: "eq", variant: "select" },
    { id: "status", value: "in-progress", operator: "eq", variant: "select" },
  ],
  joinOperator: "or",  // status = "todo" OR status = "in-progress"
});

范围过滤

await trpc.tasks.list({
  page: 1,
  perPage: 10,
  filters: [
    {
      id: "createdAt",
      value: ["2024-01-01", "2024-12-31"],
      operator: "between",
      variant: "dateRange",
    },
  ],
});

模糊搜索

await trpc.tasks.list({
  page: 1,
  perPage: 10,
  filters: [
    { id: "title", value: "bug", operator: "like", variant: "text" },
  ],
});

🔧 API 参考

createCrudRouter(config)

创建 CRUD 路由。

参数

interface CrudRouterConfig<TTable, TSelect, TInsert, TUpdate> {
  // ========== 必填 ==========
  table: TTable;                    // Drizzle 表定义

  // ========== Schema 配置(可选) ==========
  schema?: z.ZodType<TInsert>;      // 主 Schema(用于 create/upsert)
  updateSchema?: z.ZodType<TUpdate>;// 更新 Schema(覆盖自动派生)
  selectSchema?: z.ZodType<TSelect>;// 查询返回 Schema

  // ========== 其他配置 ==========
  idField?: string;                 // ID 字段名,默认 "id"
  omitFields?: string[];            // 自动派生时排除的字段
                                    // 默认 ["id", "createdAt", "updatedAt"]
}

Schema 派生规则

| 配置 | 派生行为 | |-----|---------| | 无 schema | 从 table 自动派生,排除 omitFields | | 无 updateSchema | 从 schema.partial().refine(nonEmpty) 派生 | | 无 selectSchema | 若有 schema 则使用它,否则从 table 派生 |

使用示例

// 1. 零配置 - 全部自动派生
const tasksRouter = createCrudRouter({
  table: tasks,
});

// 2. 传入 schema,updateSchema 自动派生
const postsRouter = createCrudRouter({
  table: posts,
  schema: postSchema,
});

// 3. 传入 schema + 自定义 updateSchema
const usersRouter = createCrudRouter({
  table: users,
  schema: userSchema,
  updateSchema: customUpdateSchema,
});

// 4. 自定义排除字段
const ordersRouter = createCrudRouter({
  table: orders,
  omitFields: ["id", "createdAt", "updatedAt", "internalCode"],
});

返回值

// 返回增强的 tRPC router,带有 .procedures 属性
{
  // 标准 tRPC router(可直接嵌套到 appRouter)
  ...routerMethods,

  // 可 spread 的 procedures 对象(用于扩展自定义路由)
  procedures: {
    list: Procedure<ListInput, ListOutput>,
    get: Procedure<string, TSelect>,
    create: Procedure<TInsert, TSelect>,
    update: Procedure<{ id: string, data: TUpdate }, TSelect>,
    delete: Procedure<string, TSelect>,
    deleteMany: Procedure<string[], { deleted: number }>,
    updateMany: Procedure<{ ids: string[], data: TUpdate }, { updated: number }>,
    upsert: Procedure<TInsert, { data: TSelect, isNew: boolean }>,
  }
}

🔐 权限控制

使用 procedure 配置

import { createCrudRouter } from "@wordrhyme/auto-crud-server";
import { protectedProcedure, adminProcedure, publicProcedure } from "../trpc";

export const tasksRouter = createCrudRouter({
  table: tasks,
  // 按操作指定不同的 procedure
  procedure: {
    list: publicProcedure,      // 公开读
    get: publicProcedure,
    create: protectedProcedure, // 需要登录
    update: protectedProcedure,
    delete: adminProcedure,     // 需要管理员
    default: protectedProcedure,
  },
});

使用 guard(操作级守卫)

export const tasksRouter = createCrudRouter({
  table: tasks,
  guard: (ctx, operation) => {
    // 删除操作需要管理员权限
    if (operation === "delete") {
      return ctx.user.role === "admin";
    }
    // 其他操作需要登录
    return !!ctx.user;
  },
});

使用 scope(行级过滤 RLS)

import { eq } from "drizzle-orm";

export const tasksRouter = createCrudRouter({
  table: tasks,
  // 自动注入到所有查询的 WHERE 子句
  scope: (ctx, table, operation) => {
    return eq(table.tenantId, ctx.user.tenantId);
  },
});

使用 authorize(资源级检查 ABAC)

export const tasksRouter = createCrudRouter({
  table: tasks,
  // 在 get/update/delete 时,预取资源后调用
  authorize: (ctx, resource, operation) => {
    return resource.ownerId === ctx.user.id;
  },
});

使用 inject(强制注入字段)

export const tasksRouter = createCrudRouter({
  table: tasks,
  // 写入时强制覆盖字段(防止伪造)
  inject: (ctx, operation) => ({
    tenantId: ctx.user.tenantId,
    ...(operation === "create" ? { createdBy: ctx.user.id } : { updatedBy: ctx.user.id }),
  }),
});

🎨 自定义扩展

添加自定义路由(推荐方式)

使用 .procedures 属性 spread 出 CRUD 路由,然后添加自定义路由:

import { createCrudRouter, router } from "@wordrhyme/auto-crud-server";
import { protectedProcedure } from "../trpc";
import { z } from "zod";

// 创建基础 CRUD
const tasksCrud = createCrudRouter({
  table: tasks,
});

// 使用 .procedures spread 并添加自定义路由
export const tasksRouter = router({
  ...tasksCrud.procedures,

  // 自定义路由:归档
  archive: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db
        .update(tasks)
        .set({ archived: true })
        .where(eq(tasks.id, input.id));
    }),

  // 自定义路由:取消归档
  unarchive: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db
        .update(tasks)
        .set({ archived: false })
        .where(eq(tasks.id, input.id));
    }),
});

直接使用(不需要扩展时)

如果不需要添加自定义路由,可以直接使用返回的 router:

const tasksCrud = createCrudRouter({
  table: tasks,
});

// 直接嵌套到 appRouter
export const appRouter = router({
  tasks: tasksCrud,
});

自定义过滤逻辑

// 覆盖 list 路由,添加自定义过滤逻辑
const tasksCrud = createCrudRouter({ table: tasks });

export const tasksRouter = router({
  ...tasksCrud.procedures,

  // 覆盖 list,添加自定义逻辑
  list: protectedProcedure
    .input(listInputSchema)
    .query(async ({ input, ctx }) => {
      // 自定义过滤逻辑
      const customFilters = input.filters?.map(filter => {
        if (filter.id === "customField") {
          return { ...filter, operator: "custom" };
        }
        return filter;
      });

      // 调用原始 list(需要手动实现或使用 db 查询)
      return ctx.db.select().from(tasks).where(...);
    }),
});

🔄 生命周期中间件

使用 middleware 配置在 CRUD 操作前后注入自定义逻辑,类似 tRPC middleware 模式。

完整控制模式

import { createCrudRouter } from "@wordrhyme/auto-crud-server";

const tasksRouter = createCrudRouter({
  table: tasks,

  // 中间件:完全控制操作流程
  middleware: {
    // 创建:修改输入 + 执行副作用
    create: async ({ ctx, input, next }) => {
      // 1. 修改输入数据
      const data = { ...input, slug: slugify(input.title), createdBy: ctx.user.id };

      // 2. 执行核心操作
      const result = await next(data);

      // 3. 执行副作用
      await sendNotification(result);
      await logAudit(ctx.user, "create", result);

      // 4. 返回结果(可修改)
      return result;
    },

    // 更新:可访问原记录
    update: async ({ ctx, id, data, existing, next }) => {
      // 检查权限
      if (existing.ownerId !== ctx.user.id) {
        throw new Error("Forbidden");
      }
      return next(data);
    },

    // 删除:条件拦截
    delete: async ({ ctx, id, existing, next }) => {
      if (existing.status === "locked") {
        throw new Error("Cannot delete locked resource");
      }
      return next();
    },
  },
});

使用便捷工具函数

对于简单场景,使用工具函数简化代码:

import {
  createCrudRouter,
  afterMiddleware,
  beforeMiddleware,
  afterCreate,
  beforeCreate,
} from "@wordrhyme/auto-crud-server";

const tasksRouter = createCrudRouter({
  table: tasks,

  middleware: {
    // 简单副作用:只在操作后执行
    create: afterMiddleware(async (ctx, result) => {
      await sendEmail(result);
      await logAudit(ctx.user, "create", result);
    }),

    // 修改输入:只在操作前执行
    update: beforeMiddleware(async (ctx, data) => {
      return { ...data, updatedAt: new Date(), updatedBy: ctx.user.id };
    }),

    // 类型安全的便捷函数
    delete: afterDelete(async (ctx, deleted) => {
      await cleanupRelatedData(deleted.id);
    }),
  },
});

可用的工具函数

| 函数 | 用途 | 示例 | |------|------|------| | afterMiddleware(fn) | 操作后执行副作用 | 日志、通知、审计 | | afterMiddlewareTransform(fn) | 操作后修改返回值 | 添加计算字段 | | beforeMiddleware(fn) | 操作前修改输入 | 注入用户ID、生成slug | | composeMiddleware(...fns) | 组合多个中间件 | 复杂场景 | | afterList, beforeList | list 操作专用 | 分页后处理 | | afterCreate, beforeCreate | create 操作专用 | 创建通知 | | afterUpdate, afterDelete | update/delete 专用 | 更新/删除通知 |


🔌 与其他库集成

与 Drizzle ORM 集成

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });

// 在 createContext 中传递 db
export const createContext = () => ({ db });

与 Next.js 集成

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => ({ db }),
  });

export { handler as GET, handler as POST };

与 Express 集成

import express from "express";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { appRouter } from "./server/routers";

const app = express();

app.use(
  "/trpc",
  createExpressMiddleware({
    router: appRouter,
    createContext: () => ({ db }),
  })
);

app.listen(3000);

🛠️ 配置选项

自定义 ID 字段

export const usersRouter = createCrudRouter({
  table: users,
  idField: "userId",  // 使用自定义 ID 字段
});

自定义排除字段

export const ordersRouter = createCrudRouter({
  table: orders,
  // 自动派生 schema 时排除这些字段
  omitFields: ["id", "createdAt", "updatedAt", "internalCode"],
});

软删除

export const tasksRouter = createCrudRouter({
  table: tasks,
  // 方式 1:使用默认列名 'deletedAt'
  softDelete: true,

  // 方式 2:指定列名
  softDelete: "deletedAt",

  // 方式 3:完整配置(用于布尔字段)
  softDelete: { column: "isDeleted", value: () => true },
});

批量操作限制

export const tasksRouter = createCrudRouter({
  table: tasks,
  maxBatchSize: 50,  // 默认 100
});

列白名单

export const tasksRouter = createCrudRouter({
  table: tasks,
  filterableColumns: ["title", "status", "priority"],  // 只允许这些列过滤
  sortableColumns: ["title", "createdAt", "priority"], // 只允许这些列排序
});

📦 导出的类型

// 主要导出
export { createCrudRouter } from "./routers/_factory";
export type {
  CrudRouterConfig,
  CrudMiddleware,
  ListInput,
  ListResult,
} from "./types/config";

// Middleware 工具函数
export {
  afterMiddleware,
  afterMiddlewareTransform,
  beforeMiddleware,
  composeMiddleware,
  afterList,
  beforeList,
  afterCreate,
  beforeCreate,
  afterUpdate,
  afterDelete,
} from "./lib/middleware-helpers";

// tRPC 工具
export { router, publicProcedure } from "./trpc";

// 示例路由(可选)
export { appRouter } from "./routers";
export type { AppRouter } from "./routers";

🐛 故障排除

问题 1: 模块找不到

错误: Cannot find module '@wordrhyme/auto-crud-server'

解决方案:

pnpm install @wordrhyme/auto-crud-server

问题 2: 类型错误

错误: Type 'X' is not assignable to type 'Y'

解决方案: 确保 Zod 版本一致

pnpm list zod
# 确保所有包使用相同的 Zod 版本

问题 3: 数据库连接失败

错误: connect ECONNREFUSED

解决方案: 检查数据库连接字符串

// .env
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"

🤝 贡献

欢迎贡献代码!请查看 CONTRIBUTING.md


📄 许可证

MIT © wordrhyme


🔗 相关链接


💡 灵感来源