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

p-api-orm

v1.2.19

Published

基于 [Knex](https://knexjs.org/) + [Zod](https://zod.dev/) 的 TypeScript ORM 封装库,提供表模型(`Model`)、视图模型(`ViewModel`)以及 Zod 扩展(`Zod`)三个核心模块。

Downloads

811

Readme

p-api-orm

基于 Knex + Zod 的 TypeScript ORM 封装库,提供表模型(Model)、视图模型(ViewModel)以及 Zod 扩展(Zod)三个核心模块。


安装

npm install p-api-orm

快速开始

import { Model, Zod } from 'p-api-orm';
import knex from 'knex';

const db = knex({ client: 'mysql2', connection: { ... } });

// 1. 定义 Schema
const UserSchema = Zod.object({
  id:         Zod.string().label('用户ID'),
  name:       Zod.string().label('姓名'),
  age:        Zod.number().label('年龄'),
  is_delete:  Zod.number().label('是否删除'),
});

// 2. 创建 Model
const UserModel = new Model(
  'User',          // 表名称(用于提示)
  'users',         // 实际数据库表名
  UserSchema,      // Zod Schema
  {},              // MAPS(字段映射,可为空对象)
  {},              // ENUM(枚举值,可为空对象)
);

// 3. 使用
const users = await UserModel.get(db, { name: '张三' });

Model(表模型)

构造函数

new Model(TABLE_NAME, TABLE, SCHEMA, MAPS, ENUM)

| 参数 | 类型 | 说明 | |------|------|------| | TABLE_NAME | string | 表的描述名 | | TABLE | string | 数据库中的实际表名 | | SCHEMA | z.ZodObject | Zod Schema,定义字段及校验规则 | | MAPS | any | 字段映射对象 | | ENUM | any | 枚举值对象 |

属性

| 属性 | 类型 | 说明 | |------|------|------| | JSON_FIELDS | string[] | 需要自动序列化/反序列化的 JSON 字段名列表 | | before | Function \| null | 写入前的数据处理钩子 | | after | Function \| null | 读取后的数据处理钩子 |

UserModel.JSON_FIELDS = ['extra_info'];  // extra_info 字段将自动 JSON.stringify/parse
UserModel.after = (row) => ({ ...row, full_name: row.name + '先生' });

查询方法

get(db, query, sub?)

按条件查询,返回数组。

// 简单条件
const list = await UserModel.get(db, { age: 18, is_delete: 0 });

// 分页
const list = await UserModel.get(db, { p: 1, pc: 10 });

// 指定返回字段
const list = await UserModel.get(db, { rt: ['id', 'name'] });

// 排除字段
const list = await UserModel.get(db, { nrt: ['is_delete'] });

pages(db, query, sub?)

分页查询,返回分页结构。

const result = await UserModel.pages(db, { p: 1, pc: 20, is_delete: 0 });
// 返回:
// {
//   data: [...],
//   count: 100,
//   pages: { total: 5, now: 1 }
// }

默认值:p=1, pc=20, is_delete=0


count(db, query?, sub?)

统计条数。

const total = await UserModel.count(db, { is_delete: 0 });

sum(db, field, query?, sub?)

对字段求和。

const total = await UserModel.sum(db, 'age', { is_delete: 0 });

avg(db, field, query?, sub?)

对字段求平均值。

const avg = await UserModel.avg(db, 'score', { is_delete: 0 });

group_sum(db, field, group, query?, sub?)

分组求和。

const result = await UserModel.group_sum(db, 'amount', ['city'], { is_delete: 0 });
// 返回:[{ city: '北京', sum: 1000 }, ...]

get_by_ids(db, ids, lock?)

按 ID(或 ID 数组)查询。传入单个 ID 返回单条,传入数组返回数组。

const user  = await UserModel.get_by_ids(db, '123');
const users = await UserModel.get_by_ids(db, ['123', '456']);

// 加锁(SELECT ... FOR UPDATE)
const user  = await UserModel.get_by_ids(db, '123', true);

is_exist(db, condition, err?)

查询是否存在,存在则返回该记录,不存在且传了 err 则抛出错误。

const user = await UserModel.is_exist(db, { name: '张三' }, '用户不存在');

check_repeat(db, condition, err)

检查是否重复,存在则抛出错误。

await UserModel.check_repeat(db, { name: '张三' }, '用户名已存在');

lock(db, ids, query?, err?)

行锁查询(SELECT ... FOR UPDATE)。

const rows = await UserModel.lock(db, ['123', '456'], { rt: ['id', 'balance'] });

写入方法

create(db, data)

插入一条或多条记录,自动校验 Schema 并自动补充 is_delete: 0

await UserModel.create(db, { id: '1', name: '张三', age: 20, is_delete: 0 });

// 批量插入
await UserModel.create(db, [
  { id: '1', name: '张三', age: 20, is_delete: 0 },
  { id: '2', name: '李四', age: 25, is_delete: 0 },
]);

create_or_update(db, uniq_key, data)

插入或更新(ON CONFLICT MERGE),基于唯一键。

await UserModel.create_or_update(db, 'id', { id: '1', name: '张三', age: 21, is_delete: 0 });

update_by_ids(db, ids, data)

按 ID 更新字段,只校验传入的字段。

await UserModel.update_by_ids(db, '123', { name: '新名字' });
await UserModel.update_by_ids(db, ['123', '456'], { age: 30 });

update_by_query(db, query, data)

按条件更新,可能锁表,慎用

await UserModel.update_by_query(db, { name: '张三' }, { age: 99 });

update_field_number(db, ids, field_info)

对数值字段做原子自增/自减。

// score + 10
await UserModel.update_field_number(db, '123', { score: 10 });

// score - 5
await UserModel.update_field_number(db, '123', { score: -5 });

update_field_offset(db, ids, field, base_field, offset)

设置 field = base_field + offset有并发问题,需加锁

// balance = point + 100
await UserModel.update_field_offset(db, '123', 'balance', 'point', 100);

删除方法

delete_by_ids(db, ids)

按 ID 物理删除。

await UserModel.delete_by_ids(db, '123');
await UserModel.delete_by_ids(db, ['123', '456']);

delete_by_query(db, query)

按条件物理删除。

await UserModel.delete_by_query(db, { name: '张三' });

向量搜索(仅 PostgreSQL)

get_by_embedding(db, query)

使用 emb_ 前缀传入向量进行相似度搜索(余弦距离)。

const results = await UserModel.get_by_embedding(db, {
  is_delete: 0,
  pc: 5,
  emb_vector_field: [0.1, 0.2, 0.3, ...]  // emb_ 前缀 + 字段名
});
// 返回结果包含 distance 字段(相似度距离)

查询前缀(Query Prefixes)

所有查询方法的 query 参数均支持以下字段前缀,可灵活组合查询条件:

| 前缀 | 示例 | 说明 | |------|------|------| | (无前缀) | { name: '张三' } | 精确匹配 WHERE name = '张三' | | lik_ | { lik_name: '张' } | LIKE 模糊匹配 WHERE name LIKE '%张%' | | inc_ | { inc_id: ['1','2'] } | IN 查询 WHERE id IN (...) | | nin_ | { nin_id: ['1','2'] } | NOT IN 查询 | | gt_ | { gt_age: 18 } | 大于 WHERE age > 18 | | lt_ | { lt_age: 60 } | 小于 WHERE age < 60 | | min_ | { min_age: 18 } | 大于等于 WHERE age >= 18 | | max_ | { max_age: 60 } | 小于等于 WHERE age <= 60 | | bet_ | { bet_age: '18,60' } | BETWEEN WHERE age BETWEEN 18 AND 60 | | non_ | { non_deleted_at: true } | IS NULL WHERE deleted_at IS NULL | | ntn_ | { ntn_deleted_at: true } | IS NOT NULL | | jlik_ | { jlik_info.city: '北' } | JSON 字段 LIKE | | jbet_ | { jbet_info.score: '60,100' } | JSON 字段 BETWEEN | | jlt_ | { jlt_info.score: 60 } | JSON 字段小于 | | jgt_ | { jgt_info.score: 60 } | JSON 字段大于 | | jnin_ | { jnin_info.tag: ['a','b'] } | JSON 字段 NOT IN | | has_ | { has_tags: 'vue' } | JSON 数组包含某值(单个) | | hsm_ | { hsm_tags: ['vue','react'] } | JSON 数组包含多值(OR) | | mls_ | { mls_tags: 2 } | JSON 数组长度 >= N | | ftl_ | { ftl_content: '关键词' } | 全文索引(自然语言模式) | | ftb_ | { ftb_content: '+关键词' } | 全文索引(布尔模式) |

分页 / 排序 / 字段控制保留字

| 字段 | 说明 | 示例 | |------|------|------| | p | 当前页码(从 1 开始) | p: 2 | | pc | 每页条数 | pc: 20 | | od | 排序字段,- 前缀表示降序 | od: ['-created_at', 'name']od: '-created_at,name' | | rt | 仅返回指定字段 | rt: ['id', 'name']rt: 'id,name',传 '*' 返回全部 | | nrt | 排除指定字段 | nrt: ['is_delete', 'password'] | | sub | 子查询表 | 见高级用法 |

OR 条件组

使用 sub 字段传入 OR 条件数组:

await UserModel.get(db, {
  is_delete: 0,
  sub: [
    { name: '张三' },
    { name: '李四' },
  ]
});
// WHERE is_delete = 0 AND (name = '张三' OR name = '李四')

ViewModel(视图模型)

用于多表 JOIN 场景,通过声明关联关系自动构建视图 SQL。

import { ViewModel, Zod } from 'p-api-orm';

const ViewSchema = Zod.object({
  id:        Zod.string().label('用户ID').meta({ table: 'users', field: 'id' }),
  name:      Zod.string().label('姓名').meta({ table: 'users', field: 'name' }),
  role_name: Zod.string().label('角色名').meta({ table: 'roles', field: 'name' }),
});

const UserRoleView = new ViewModel(ViewSchema, {
  form: 'users',
  joins: [
    {
      type: 'left',
      table: 'roles',
      on: [
        { table: 'users', field: 'role_id' },
        { table: 'roles', field: 'id' },
      ]
    }
  ]
});

ViewModel 支持的方法

Model 相同,均支持:get, pages, count, is_exist, check_repeat

const list = await UserRoleView.get(db, { is_delete: 0 });
const page = await UserRoleView.pages(db, { p: 1, pc: 10, lik_name: '张' });

Zod 扩展

p-api-orm 对 Zod 增加了三个扩展方法:

.label(text)

为字段设置中文标签,校验失败时错误信息会使用该标签。

Zod.string().label('用户名')
// 校验失败时提示:"用户名为必填项" 而非 "Required"

.valid(enumObj)

从对象的 key 生成枚举类型,常用于配合枚举常量。

const STATUS = { active: 1, inactive: 0 };
const statusField = Zod.string().valid(STATUS);
// 等价于 z.enum(['active', 'inactive'])

.meta(info)

为字段附加元数据,常用于 ViewModel 中声明字段所属表与列名。

Zod.string().meta({ table: 'users', field: 'user_name' })

完整使用示例

import { Model, ViewModel, Zod } from 'p-api-orm';
import knex from 'knex';

const db = knex({ client: 'mysql2', connection: { host: 'localhost', database: 'test', user: 'root', password: '' } });

// === 定义 Schema ===
const ArticleSchema = Zod.object({
  id:         Zod.string().label('文章ID'),
  title:      Zod.string().label('标题'),
  content:    Zod.string().label('内容'),
  tags:       Zod.string().label('标签').optional(),
  view_count: Zod.number().label('浏览数').default(0),
  is_delete:  Zod.number().label('删除标记').default(0),
});

// === 创建 Model ===
const ArticleModel = new Model('Article', 'articles', ArticleSchema, {}, {});

// JSON 字段自动序列化
ArticleModel.JSON_FIELDS = ['tags'];

// === CRUD 操作 ===

// 创建
await ArticleModel.create(db, { id: '1', title: 'Hello', content: '...', tags: ['vue', 'ts'], view_count: 0, is_delete: 0 });

// 分页查询(模糊标题 + 标签包含 vue)
const page = await ArticleModel.pages(db, {
  p: 1,
  pc: 10,
  lik_title: 'Hello',
  has_tags: 'vue',
  od: ['-view_count'],
  nrt: ['is_delete'],
});

// 更新浏览数 +1
await ArticleModel.update_field_number(db, '1', { view_count: 1 });

// 软删除
await ArticleModel.update_by_ids(db, '1', { is_delete: 1 });

// 物理删除
await ArticleModel.delete_by_ids(db, '1');

错误处理

Schema 校验失败时会直接 throw 一条中文字符串,建议在业务层统一捕获:

try {
  await ArticleModel.create(db, { title: '' });
} catch (err) {
  // err: "标题为必填项"
  console.error(err);
}