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);
}