初次提交
This commit is contained in:
182
src/database.ts
Normal file
182
src/database.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { Pool, PoolClient, QueryResult, QueryResultRow } from "pg";
|
||||
import { BasicEntity } from "./entity";
|
||||
import { DeleteBuilder, IDeleteBuilder, IInsertBuilder, InsertBuilder, ISelectBuilder, IUpdateBuilder, SelectBuilder, UpdateBuilder } from "./query";
|
||||
import { Class } from "./types";
|
||||
import { formatSQL } from "./util";
|
||||
|
||||
interface IPostgresClient {
|
||||
/**
|
||||
* 执行sql语句
|
||||
* @param sql sql语句
|
||||
* @param args sql参数
|
||||
*/
|
||||
query<R>(sql: string, args?: any[]): Promise<QueryResult<R[]>>;
|
||||
|
||||
/** 开启事务 */
|
||||
trans(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 查询实体
|
||||
* @param Entity 实体
|
||||
* @param alias 别名
|
||||
*/
|
||||
select<E extends BasicEntity>(Entity: Class<E>, alias?: string): ISelectBuilder<E>;
|
||||
|
||||
/**
|
||||
* 进行数据库插入
|
||||
* @param Entity 实体
|
||||
*/
|
||||
insert<E extends BasicEntity>(Entity: Class<E>): IInsertBuilder<E>;
|
||||
|
||||
/**
|
||||
* 进行实体更新
|
||||
* @param Entity 实体
|
||||
*/
|
||||
update<E extends BasicEntity>(Entity: Class<E>): IUpdateBuilder<E>;
|
||||
|
||||
/**
|
||||
* 进行实体删除
|
||||
* @param Entity 实体
|
||||
*/
|
||||
del<E extends BasicEntity>(Entity: Class<E>): IDeleteBuilder<E>;
|
||||
}
|
||||
|
||||
interface IPostgresConfig {
|
||||
host?: string
|
||||
port?: number
|
||||
user?: string
|
||||
password?: string
|
||||
database?: string
|
||||
max?: number
|
||||
}
|
||||
|
||||
class PostgresClient implements IPostgresClient {
|
||||
#client: PoolClient
|
||||
#transOn = false;
|
||||
|
||||
constructor(client: PoolClient) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
public query<R>(sql: string, args?: any[] | Record<string, any>): Promise<QueryResult<R[]>> { return this.#client.query(args ? formatSQL(sql, args) : sql); }
|
||||
|
||||
|
||||
public async trans() {
|
||||
await this.query("begin")
|
||||
this.#transOn = true;
|
||||
}
|
||||
|
||||
public get transOn() { return this.#transOn; }
|
||||
|
||||
public release() { this.#client.release(); }
|
||||
|
||||
public select<E extends BasicEntity>(Entity: Class<E>, alias?: string): ISelectBuilder<E> { return new SelectBuilder(this.query.bind(this), Entity, alias); }
|
||||
|
||||
public insert<E extends BasicEntity>(Entity: Class<E>): IInsertBuilder<E> { return new InsertBuilder(this.query.bind(this), Entity); }
|
||||
|
||||
public update<E extends BasicEntity>(Entity: Class<E>): IUpdateBuilder<E> { return new UpdateBuilder(this.query.bind(this), Entity); }
|
||||
|
||||
public del<E extends BasicEntity>(Entity: Class<E>): IDeleteBuilder<E> { return new DeleteBuilder(this.query.bind(this), Entity); }
|
||||
};
|
||||
|
||||
/** 数据库操作相关 */
|
||||
export namespace database {
|
||||
|
||||
let pool: Pool | null = null;
|
||||
let confLoader!: () => IPostgresConfig
|
||||
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
const conf = confLoader();
|
||||
pool = new Pool({
|
||||
host: conf.host,
|
||||
port: conf.port,
|
||||
user: conf.user,
|
||||
password: conf.password,
|
||||
database: conf.database,
|
||||
max: conf.max,
|
||||
ssl: false,
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置数据库
|
||||
* @param loader 配置加载器
|
||||
*/
|
||||
export function config(loader: () => IPostgresConfig) {
|
||||
confLoader = loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接执行sql语句
|
||||
* @param sql sql语句
|
||||
* @param args sql参数
|
||||
*/
|
||||
export function query<R extends QueryResultRow>(sql: string, args?: any[] | Record<string, any>) { return getPool().query<R>(args ? formatSQL(sql, args) : sql); }
|
||||
|
||||
/**
|
||||
* 查询实体
|
||||
* @param Entity 实体
|
||||
* @param alias 别名
|
||||
*/
|
||||
export function select<E extends BasicEntity>(Entity: Class<E>, alias?: string): ISelectBuilder<E> { return new SelectBuilder(database.query, Entity, alias); }
|
||||
|
||||
/**
|
||||
* 进行数据库插入
|
||||
* @param Entity 实体
|
||||
*/
|
||||
export function insert<E extends BasicEntity>(Entity: Class<E>): IInsertBuilder<E> { return new InsertBuilder(database.query, Entity); }
|
||||
|
||||
/**
|
||||
* 进行实体更新
|
||||
* @param Entity 实体
|
||||
*/
|
||||
export function update<E extends BasicEntity>(Entity: Class<E>): IUpdateBuilder<E> { return new UpdateBuilder(database.query, Entity); }
|
||||
|
||||
/**
|
||||
* 进行实体删除
|
||||
* @param Entity 实体
|
||||
*/
|
||||
export function del<E extends BasicEntity>(Entity: Class<E>): IDeleteBuilder<E> { return new DeleteBuilder(database.query, Entity); }
|
||||
|
||||
/**
|
||||
* 连接数据库
|
||||
* @param callback 回调
|
||||
*/
|
||||
export async function connect<R>(callback: (client: PostgresClient) => R | Promise<R>): Promise<R> {
|
||||
const conn = await getPool().connect();
|
||||
const client = new PostgresClient(conn);
|
||||
let ret: R;
|
||||
try {
|
||||
ret = await callback(client);
|
||||
if (client.transOn) await client.query("commit");
|
||||
}
|
||||
catch (err) {
|
||||
if (client.transOn) await client.query("rollback");
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行事务操作
|
||||
* @param callback 回调
|
||||
*/
|
||||
export function transaction<R>(callback: (client: PostgresClient) => R | Promise<R>): Promise<R> {
|
||||
return connect(async (client) => {
|
||||
await client.trans();
|
||||
return callback(client);
|
||||
});
|
||||
};
|
||||
|
||||
/** 关闭数据库 */
|
||||
export function close() {
|
||||
if (pool) pool.end().catch((err) => console.error(err));
|
||||
}
|
||||
}
|
389
src/entity.ts
Normal file
389
src/entity.ts
Normal file
@ -0,0 +1,389 @@
|
||||
import moment from "moment";
|
||||
import { DatabaseError } from "./error";
|
||||
import { TSVector } from "./type";
|
||||
import { escapeID, escapeValue } from "./util";
|
||||
import type { ArrayType, ArrayTypeSelf, BasicFieldName, Class, Decorators, IDLikeFieldName, Values } from "./types";
|
||||
|
||||
export interface IEntityFieldConfig {
|
||||
/** 字段名 */
|
||||
name: string
|
||||
/** 解析后的属性名 */
|
||||
prop: string
|
||||
/** 是否是主键 */
|
||||
primary: boolean
|
||||
/** 编码方式(存入数据库时) */
|
||||
encode: (data: any) => string
|
||||
/** 解码方式(查询数据库时) */
|
||||
decode: (data: any) => any
|
||||
}
|
||||
|
||||
|
||||
export interface IEntityJoinConfig {
|
||||
/** 属性名称 */
|
||||
prop: string
|
||||
/** 是否是many */
|
||||
many: boolean
|
||||
/** 目标表 */
|
||||
entity: () => any
|
||||
/**
|
||||
* join条件生成方法
|
||||
* @param cur 当前表名
|
||||
* @param dst 目标表名
|
||||
*/
|
||||
builder(cur: string, dst: string): string
|
||||
}
|
||||
|
||||
export interface IEntityConfig {
|
||||
/** 类信息 */
|
||||
clazz: any
|
||||
/** 模式名 */
|
||||
schema?: string
|
||||
/** 表名 */
|
||||
table: string
|
||||
/** 字段配置 */
|
||||
fields: IEntityFieldConfig[]
|
||||
/** join信息 */
|
||||
joins: IEntityJoinConfig[]
|
||||
}
|
||||
|
||||
export class BasicEntity {
|
||||
private ___is_db_entity___ = true
|
||||
|
||||
public static make<T extends BasicEntity>(this: Class<T>, data: Partial<Pick<T, BasicFieldName<T>>>, prefix?: string) {
|
||||
const obj = new this();
|
||||
const config = getEntityConfig(this);
|
||||
for (const col of config.fields) {
|
||||
const pname = prefix ? `${prefix}${col.prop}` : col.prop;
|
||||
if (pname in data) (obj as any)[col.prop] = (data as any)[pname];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取指定属性
|
||||
* @param keys 要提取的属性
|
||||
*/
|
||||
public pick<K extends BasicFieldName<this>>(...keys: K[]): Pick<this, K> {
|
||||
const result: any = {};
|
||||
for (const k of keys) {
|
||||
result[k] = this[k];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除指定的属性,提取剩余属性
|
||||
* @param keys 要排除的键
|
||||
*/
|
||||
public omit<K extends BasicFieldName<this>>(...keys: K[]): Omit<Pick<this, BasicFieldName<this>>, K> {
|
||||
const result: any = {};
|
||||
const config = getEntityConfigWithoutCheck(this.constructor)!;
|
||||
for (const col of config.fields) {
|
||||
if (keys.includes(col.prop as K)) continue;
|
||||
result[col.prop] = this[col.prop as K];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function entityConfigOf(target: any): IEntityConfig {
|
||||
const cnf = target.__entity_config__ as IEntityConfig;
|
||||
|
||||
if (!cnf) target.__entity_config__ = {
|
||||
clazz: target,
|
||||
fields: [],
|
||||
joins: []
|
||||
};
|
||||
else if (cnf.clazz != target) target.__entity_config__ = {
|
||||
...cnf,
|
||||
clazz: target,
|
||||
fields: [...cnf.fields],
|
||||
joins: [...cnf.joins],
|
||||
}
|
||||
return target.__entity_config__;
|
||||
}
|
||||
|
||||
function getEntityConfigWithoutCheck(target: any): IEntityConfig | null {
|
||||
return target?.__entity_config__ ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 指定实体的表名
|
||||
* @param table 表名,如果需要指定schema,可以使用"<schema>.<table>"的格式
|
||||
*/
|
||||
export function Table(table: string): Decorators.ClassDecorator<BasicEntity> {
|
||||
return function (target) {
|
||||
const [schema, tbl] = table.split(".").map(s => s.trim()).filter(s => !!s);
|
||||
const conf = entityConfigOf(target);
|
||||
conf.table = table;
|
||||
if (!tbl) {
|
||||
conf.schema = undefined;
|
||||
conf.table = schema;
|
||||
}
|
||||
else {
|
||||
conf.schema = schema;
|
||||
conf.table = tbl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义一个字段
|
||||
* @param name 字段名称
|
||||
* @param encode 字段编码凡是
|
||||
* @param decode 字段解码方式
|
||||
* @param primary 是否是主键
|
||||
*/
|
||||
export function Field(name: string | undefined, encode: IEntityFieldConfig["encode"], decode: IEntityFieldConfig["decode"], primary: boolean): Decorators.PropDecorator<BasicEntity> {
|
||||
return function (target, propertyKey) {
|
||||
const conf = entityConfigOf(target.constructor)
|
||||
conf.fields.push({ name: name ?? propertyKey, prop: propertyKey, encode, decode, primary });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 定义一个整形字段
|
||||
* @param name 字段名称
|
||||
* @param option 字段选项
|
||||
*/
|
||||
export function IntColumn(name: string, option?: { primary: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||
export function IntColumn(option?: { primary: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||
export function IntColumn(name?: any, option?: any) {
|
||||
if (typeof name != "string") {
|
||||
option = name;
|
||||
name = undefined;
|
||||
}
|
||||
return Field(name, v => escapeValue(v), v => v, option?.primary ?? false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 定义浮点数(包括定点小数)
|
||||
* @param name 字段名称
|
||||
*/
|
||||
export function FloatColumn(name?: string) { return Field(name, v => escapeValue(v), v => v, false); }
|
||||
|
||||
|
||||
/**
|
||||
* 定义字符串字段(支持,char varchar text)
|
||||
* @param name 字段名称
|
||||
* @param option 字段选项
|
||||
*/
|
||||
export function StringColumn(name: string, option?: { primary?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||
export function StringColumn(option?: { primary?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||
export function StringColumn(name?: any, option?: any) {
|
||||
if (typeof name != "string") {
|
||||
option = name
|
||||
name = undefined
|
||||
}
|
||||
return Field(name, v => escapeValue(v), v => v, option?.primary ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义布尔字段
|
||||
* @param name 字段名称
|
||||
*/
|
||||
export function BooleanColumn(name?: string) {
|
||||
const checker = (v: any) => {
|
||||
if (typeof v == "boolean") return v;
|
||||
if (typeof v != "string") return false;
|
||||
v = v.toLowerCase();
|
||||
return ["true", "yes", "on"].includes(v)
|
||||
}
|
||||
|
||||
return Field(name, (v) => escapeValue(checker(v)), checker, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义日期字段
|
||||
* @param name 字段名称
|
||||
*/
|
||||
export function DateColumn(name?: string) {
|
||||
return Field(name, v => {
|
||||
if (v instanceof DateColumn) v = moment(v);
|
||||
if (typeof v == "string") v = moment(v);
|
||||
if (!moment.isMoment(v)) return escapeValue(null);
|
||||
return escapeValue(v.format("YYYY-MM-DD"));
|
||||
}, v => {
|
||||
if (typeof v == "string" || (v instanceof global.Date)) return moment(v).format("YYYY-MM-DD");
|
||||
return null;
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义日期时间字段
|
||||
* @param name 字段名称
|
||||
*/
|
||||
export function DateTimeColumn(name?: string) {
|
||||
return Field(name, v => {
|
||||
if (v instanceof DateColumn) v = moment(v);
|
||||
if (typeof v == "string") v = moment(v);
|
||||
if (!moment.isMoment(v)) return escapeValue(null);
|
||||
return escapeValue(v.format("YYYY-MM-DD HH:mm:ss"));
|
||||
}, v => {
|
||||
if (typeof v == "string" || (v instanceof global.Date)) return moment(v).format("YYYY-MM-DD HH:mm:ss");
|
||||
return null;
|
||||
}, false);
|
||||
}
|
||||
|
||||
|
||||
//数组定义
|
||||
function _Array<T>(name: string | undefined, typeName: string, itemEncoder: (v: any) => (string | null), itemDecoder: ((v: any) => (T | null)) | undefined) {
|
||||
return Field(name, v => {
|
||||
if (!(v instanceof Array)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_ARRAY", `字段${name}需要给定一个数组`);
|
||||
return escapeValue(`{${v.map(item => itemEncoder(item)).join(",")}}`) + `::${typeName}`;
|
||||
}, v => {
|
||||
if (!(v instanceof Array)) return null;
|
||||
if (itemDecoder) return v.map(item => itemDecoder(item));
|
||||
else return v;
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义整数数组,可以通过option.bytes来设置整数长度,默认为4
|
||||
* @param name 字段名称
|
||||
* @param option 字段选项
|
||||
*/
|
||||
export function IntArrayColumn(name: string, option?: { bytes?: 2 | 4 | 8 }): Decorators.PropDecorator<BasicEntity>
|
||||
export function IntArrayColumn(option?: { bytes?: 2 | 4 | 8 }): Decorators.PropDecorator<BasicEntity>
|
||||
export function IntArrayColumn(name?: any, option?: any) {
|
||||
if (name && typeof name !== "string") {
|
||||
option = name
|
||||
name = undefined
|
||||
}
|
||||
|
||||
return _Array(name, `int${option?.bytes ?? 4}[]`, item => {
|
||||
item = parseInt(item);
|
||||
if (isNaN(item) || !isFinite(item)) return null;
|
||||
return item.toString();
|
||||
}, item => {
|
||||
item = parseInt(item);
|
||||
if (isNaN(item) || !isFinite(item)) return null;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义字符串数组
|
||||
* @param name 字段名称
|
||||
*/
|
||||
export function StringArrayColumn(name?: string) {
|
||||
return _Array(name, "varchar[]", item => {
|
||||
if (typeof item == "string") return item;
|
||||
else return null;
|
||||
}, item => item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义jsonb字段
|
||||
* @param name 字段名称
|
||||
*/
|
||||
export function JsonColumn(name?: string) {
|
||||
return Field(name, v => escapeValue(JSON.stringify(v)) + "::jsonb", v => v, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义tsvector字段
|
||||
* @param name 字段名称
|
||||
*/
|
||||
export function TSVectorColumn(name?: string) {
|
||||
return Field(name, v => {
|
||||
if (!(v instanceof TSVector)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_TSVECTOR", `字段${name}需要给定一个TSVector`)
|
||||
return `E'${v.value}'::tsvector`;
|
||||
}, v => TSVector.fromValue(v), false);
|
||||
}
|
||||
|
||||
//获取实体配置
|
||||
export function getEntityConfig(clazz: any) {
|
||||
const conf = getEntityConfigWithoutCheck(clazz);
|
||||
if (!conf) throw new DatabaseError("DB_ENTITY_NOT_DECLARE", `实体${clazz.name}未注册`);
|
||||
return conf;
|
||||
}
|
||||
|
||||
//获取表名
|
||||
export function getTableName(clazz: any) {
|
||||
const tname = getEntityConfigWithoutCheck(clazz)?.table;
|
||||
if (!tname) throw new DatabaseError("DB_ENTITY_TABLE_NOT_DECLARE", `实体${clazz.name}缺少表格装饰`);
|
||||
return tname;
|
||||
}
|
||||
|
||||
//获取字段配置
|
||||
export function getField(clazz: any, prop: string) {
|
||||
const field = getEntityConfigWithoutCheck(clazz)?.fields.find(f => f.prop == prop);
|
||||
if (!field) throw new DatabaseError("DB_ENTITY_FIELD_NOT_DECLARE", `实体${clazz.name}缺少字段${prop}的装饰`);
|
||||
return field;
|
||||
}
|
||||
|
||||
//获取字段名
|
||||
export function getFieldName(clazz: any, prop: string) {
|
||||
return getField(clazz, prop).name;
|
||||
}
|
||||
|
||||
//获取主键字段名
|
||||
export function getPrimaryFieldName(clazz: any) {
|
||||
const pname = getEntityConfigWithoutCheck(clazz)?.fields.find(f => f.primary)?.name;
|
||||
if (!pname) throw new DatabaseError("DB_ENTITY_FIELD_NOT_DECLARE", `实体${clazz.name}缺少主键装饰`);
|
||||
return pname;
|
||||
}
|
||||
|
||||
export function getJoin(clazz: any, prop: string) {
|
||||
const join = getEntityConfigWithoutCheck(clazz)?.joins.find(j => j.prop == prop);
|
||||
if (!join) throw new DatabaseError("DB_ENTITY_JOIN_NOT_DECLARE", `实体${clazz.name}的${prop}属性缺少join装饰`);
|
||||
return join;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一对一或多对一Join
|
||||
* @param entity 要Join的实体
|
||||
* @param propOrBuilder Join的字段或Join构造器
|
||||
*/
|
||||
export function JoinOne<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, prop: IDLikeFieldName<T>): Decorators.PropDecorator<T>
|
||||
export function JoinOne<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, prop: IDLikeFieldName<T>, ref: IDLikeFieldName<E>): Decorators.PropDecorator<T>
|
||||
export function JoinOne<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, builder: ((cur: string, dst: string) => string)): Decorators.PropDecorator<T>
|
||||
export function JoinOne<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, propOrBuilder: IDLikeFieldName<T> | ((cur: string, dst: string) => string), ref?: IDLikeFieldName<E>): Decorators.PropDecorator<T> {
|
||||
return function (target, propertyKey) {
|
||||
const conf = entityConfigOf(target.constructor);
|
||||
conf.joins.push({
|
||||
prop: propertyKey,
|
||||
entity: entity,
|
||||
many: false,
|
||||
builder: (cur, dst) => {
|
||||
if (typeof propOrBuilder == "function") return propOrBuilder(cur, dst);
|
||||
const refName = ref ? getFieldName(entity(), ref as string) : getPrimaryFieldName(entity());
|
||||
// return `"${cur}"."${getFieldName(target.constructor, propOrBuilder as string)}"="${dst}"."${getPrimaryFieldName(entity())}"`
|
||||
return `${escapeID(cur, getFieldName(target.constructor, propOrBuilder as string))}=${escapeID(dst, refName)}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 一对多Join
|
||||
* @param entity 要Join的实体
|
||||
* @param prop Join的字段
|
||||
* @param ref 当前表引用的字段
|
||||
* @param builder Join构造器
|
||||
*/
|
||||
export function JoinMany<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, prop: IDLikeFieldName<E>): Decorators.PropDecorator<T>
|
||||
export function JoinMany<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, prop: IDLikeFieldName<E>, ref: IDLikeFieldName<T>): Decorators.PropDecorator<T>
|
||||
export function JoinMany<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, builder: ((cur: string, dst: string) => string)): Decorators.PropDecorator<T>
|
||||
export function JoinMany<T extends BasicEntity, E extends BasicEntity>(entity: () => Class<E>, propOrBuilder: IDLikeFieldName<E> | ((cur: string, dst: string) => string), ref?: IDLikeFieldName<T>): Decorators.PropDecorator<T> {
|
||||
return function (target, propertyKey) {
|
||||
const conf = entityConfigOf(target.constructor);
|
||||
conf.joins.push({
|
||||
prop: propertyKey,
|
||||
entity: entity,
|
||||
many: true,
|
||||
builder: (cur, dst) => {
|
||||
if (typeof propOrBuilder == "function") return propOrBuilder(cur, dst);
|
||||
const refName = ref ? getFieldName(target.constructor, ref as string) : getPrimaryFieldName(target.constructor);
|
||||
// return `"${dst}"."${getFieldName(entity(), propOrBuilder as string)}"="${cur}"."${getPrimaryFieldName(target.constructor)}"`
|
||||
return `${escapeID(dst, getFieldName(entity(), propOrBuilder as string))}=${escapeID(cur, refName)}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
6
src/error.ts
Normal file
6
src/error.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/** 数据库错误 */
|
||||
export class DatabaseError extends Error {
|
||||
constructor(public readonly code: string, message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
13
src/index.ts
Normal file
13
src/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export {
|
||||
Table,
|
||||
Field, IntColumn, FloatColumn, StringColumn, BooleanColumn, DateColumn, DateTimeColumn, JsonColumn,
|
||||
IntArrayColumn, StringArrayColumn,
|
||||
TSVectorColumn,
|
||||
JoinOne, JoinMany,
|
||||
BasicEntity,
|
||||
} from "./entity";
|
||||
|
||||
export * from "./type";
|
||||
export * from "./database";
|
||||
export * from "./error";
|
||||
export * from "./util";
|
856
src/query.ts
Normal file
856
src/query.ts
Normal file
@ -0,0 +1,856 @@
|
||||
import { Pool, PoolClient, QueryResult, QueryResultRow } from "pg";
|
||||
import { BasicEntity, IEntityFieldConfig, IEntityJoinConfig, getEntityConfig, getField, getJoin, getTableName } from "./entity";
|
||||
import type { JoinaFieldName, Class, ArrayTypeSelf, Values } from "./types";
|
||||
import { escapeID, escapeValue, formatSQL } from "./util";
|
||||
|
||||
|
||||
//编译器原因,不加此段代码会出错,具体原因未知,请保持定义和entity.ts重的同名定义一致
|
||||
/** 取基础类型的字段名 */
|
||||
export type BasicFieldName<T extends BasicEntity> = Values<{ [P in keyof T]:
|
||||
//忽略never
|
||||
Exclude<T[P], null | undefined> extends never ? never : (
|
||||
//忽略函数
|
||||
Exclude<T[P], null | undefined> extends Function ? never : (
|
||||
//忽略实体
|
||||
ArrayTypeSelf<Exclude<T[P], null | undefined>> extends BasicEntity ? never : (
|
||||
P
|
||||
)
|
||||
)
|
||||
)
|
||||
}>;
|
||||
|
||||
interface IQueryFunc<R extends QueryResultRow = any> {
|
||||
(sql: string, args?: any[]): Promise<QueryResult<R>>
|
||||
}
|
||||
|
||||
|
||||
type StringArgs<Str extends string, P extends string, S extends string> = Str extends `${string}${P}${infer R}${S}${infer T}` ? (R | StringArgs<T, P, S>) : never;
|
||||
|
||||
|
||||
type BasicWhere<V> = never
|
||||
| { in: V[] } | { nin: V[] }
|
||||
| { ne: V | null } | { gt: V } | { gte: V } | { lt: V } | { lte: V }
|
||||
| { like: string } | { nlike: string }
|
||||
|
||||
type ConditionOption<E extends BasicEntity> = { [P in BasicFieldName<E>]?: Exclude<E[P], null | undefined> | BasicWhere<Exclude<E[P], null | undefined>> | null }
|
||||
|
||||
/** 搜索选项映射关系 */
|
||||
interface ISearchOptionMap {
|
||||
/** tsquery搜索 */
|
||||
tsquery: string | string[] | {
|
||||
/** 关键字 */
|
||||
keywords: string | string[]
|
||||
/** 是否进行绝对匹配,如果为true,则只匹配关键字,否则匹配关键字前缀,默认true */
|
||||
absolute?: boolean
|
||||
/** 匹配方式,默认or */
|
||||
method?: "or" | "and"
|
||||
}
|
||||
/** like搜索 */
|
||||
like: string | {
|
||||
/** 搜索关键字 */
|
||||
keywords: string
|
||||
/** 搜索方式, left表示左匹配,right表示右匹配,both表示两边匹配,默认both */
|
||||
method?: "left" | "right" | "both"
|
||||
}
|
||||
}
|
||||
|
||||
interface IWhereFn<E extends BasicEntity> {
|
||||
/**
|
||||
* 设置where条件
|
||||
* @param option 条件选项
|
||||
* @param sql 自定义sql
|
||||
* @param args sql参数
|
||||
*/
|
||||
where(option: ConditionOption<E>): this
|
||||
where(sql: string, args?: any[]): this
|
||||
where<S extends string>(sql: S, args: { [P in StringArgs<S, "{:", "}">]: any }): this
|
||||
|
||||
/**
|
||||
* 添加搜索条件
|
||||
*
|
||||
* 搜索条件会单独生成一段where语句,并与其他where条件使用AND连接
|
||||
*
|
||||
* @param type 搜索类型
|
||||
* @param field 搜索字段
|
||||
* @param option 搜索选项
|
||||
*/
|
||||
search<T extends keyof ISearchOptionMap>(type: T, field: BasicFieldName<E>, option: ISearchOptionMap[T]): this
|
||||
}
|
||||
|
||||
interface IJoinFn<E extends BasicEntity> {
|
||||
/**
|
||||
* 进行JOIN操作
|
||||
* @param name 字段名称
|
||||
* @param callback Join后的具体操作
|
||||
*/
|
||||
join<N extends JoinaFieldName<E>>(name: N, callback?: (joinner: IJoinBuilder<JoinedEntity<E, N>>) => any): this
|
||||
|
||||
/**
|
||||
* 进行JOIN操作
|
||||
* @param name 字段名称
|
||||
* @param alias JOIN别名
|
||||
* @param callback Join后的具体操作
|
||||
*/
|
||||
join<N extends JoinaFieldName<E>>(name: N, alias: string, callback?: (joinner: IJoinBuilder<JoinedEntity<E, N>>) => any): this
|
||||
}
|
||||
|
||||
interface IFilterFn<E extends BasicEntity> {
|
||||
/**
|
||||
* 查询字段过滤
|
||||
* @param columns 要查询的字段,如果补传递则查询所有字段
|
||||
*/
|
||||
filter(...columns: Array<BasicFieldName<E> | { [P in BasicFieldName<E>]+?: string }>): this
|
||||
/**
|
||||
* 排除字段
|
||||
* @param columns 要排除的字段
|
||||
*/
|
||||
exclude(...columns: Array<BasicFieldName<E>>): this
|
||||
/**
|
||||
* 忽略当前实体的字段
|
||||
*/
|
||||
ignore(): this
|
||||
/**
|
||||
* 添加列
|
||||
* @param column 列名称
|
||||
* @param alias 别名
|
||||
*/
|
||||
add(column: BasicFieldName<E>, alias?: string): this
|
||||
|
||||
/**
|
||||
* 添加自定义列
|
||||
* @param column 自定义列
|
||||
* @param alias 别名
|
||||
*/
|
||||
add(column: () => string, alias: string): this
|
||||
}
|
||||
|
||||
interface IReturningFn<E extends BasicEntity> {
|
||||
/**
|
||||
* 设置返回字段
|
||||
* @param columns 要返回的字段
|
||||
*/
|
||||
returning(...columns: Array<BasicFieldName<E>>): this
|
||||
}
|
||||
|
||||
interface IGroupFn<E extends BasicEntity> {
|
||||
/**
|
||||
* 分组
|
||||
* @param name 分组名称
|
||||
*/
|
||||
group(...name: Array<(keyof E) | (() => string)>): this
|
||||
}
|
||||
|
||||
export interface ISelectBuilder<E extends BasicEntity> extends IWhereFn<E>, IFilterFn<E>, IJoinFn<E> {
|
||||
/**
|
||||
* 设置查询偏移
|
||||
* @param offset 查询偏移
|
||||
*/
|
||||
offset(offset: number): this
|
||||
|
||||
/**
|
||||
* 设置查询数量
|
||||
* @param limit 查询数量
|
||||
*/
|
||||
limit(limit: number): this
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* @param name 排序名称
|
||||
* @param sort 排序方式
|
||||
*/
|
||||
order(name: string, sort: "asc" | "desc"): this
|
||||
/**
|
||||
* 排序
|
||||
* @param name 排序名称
|
||||
* @param sort 排序方式
|
||||
*/
|
||||
order(name: keyof E, sort: "asc" | "desc"): this
|
||||
|
||||
// __sql__(count: boolean): string
|
||||
|
||||
/** 查询数据 */
|
||||
find(): Promise<E[]>
|
||||
|
||||
/** 查询一条数据 */
|
||||
findOne(): Promise<E | null>
|
||||
|
||||
/** 仅查询数量 */
|
||||
count(): Promise<number>
|
||||
|
||||
/** 查询数据和数量 */
|
||||
findAndCount(): Promise<[data: E[], count: number]>
|
||||
|
||||
/** 仅执行sql语句 */
|
||||
query<T>(): Promise<T[]>;
|
||||
}
|
||||
|
||||
export interface IInsertBuilder<E extends BasicEntity> extends IReturningFn<E> {
|
||||
/**
|
||||
* 设置要插入的数据
|
||||
* @param data 要插入的数据
|
||||
*/
|
||||
data<Name extends BasicFieldName<E>>(data: Pick<E, Name> | Pick<E, Name>[]): this;
|
||||
|
||||
/** 执行插入 */
|
||||
query(): Promise<E[]>
|
||||
}
|
||||
|
||||
export interface IUpdateBuilder<E extends BasicEntity> extends IWhereFn<E>, IReturningFn<E> {
|
||||
/**
|
||||
* 设置更新数据
|
||||
* @param data 要更新的数据
|
||||
*/
|
||||
set(data: Partial<Pick<E, BasicFieldName<E>>>): this
|
||||
|
||||
/** 执行更新 */
|
||||
query(): Promise<E[]>
|
||||
}
|
||||
|
||||
export interface IDeleteBuilder<E extends BasicEntity> extends IWhereFn<E>, IReturningFn<E> {
|
||||
/** 执行更新 */
|
||||
query(): Promise<E[]>
|
||||
}
|
||||
|
||||
interface IJoinBuilder<E extends BasicEntity> extends IWhereFn<E>, IFilterFn<E>, IJoinFn<E> {
|
||||
/**
|
||||
* 设置on条件
|
||||
* @param option 条件选项
|
||||
* @param sql 自定义sql
|
||||
* @param args sql参数
|
||||
*/
|
||||
on(option: ConditionOption<E>): this
|
||||
on(sql: string, args?: any[]): this
|
||||
on<S extends string>(sql: S, args: { [P in StringArgs<S, "{:", "}">]: any }): this
|
||||
}
|
||||
|
||||
|
||||
type Real<T> = Exclude<T, null | undefined>
|
||||
type JoinedEntity<E, N extends keyof E> = Real<E[N]> extends Array<infer R> ? (R extends BasicEntity ? R : never) : (Real<E[N]> extends BasicEntity ? Real<E[N]> : never);
|
||||
|
||||
type Constructor<E extends BasicEntity> = new (...args: any[]) => SQLBuilder<E>;
|
||||
|
||||
class SQLBuilderContext {
|
||||
#nameDict: Record<string, number> = {};
|
||||
#groups: string[] = [];
|
||||
#searchs: string[] = [];
|
||||
|
||||
public genName(name: string) {
|
||||
this.#nameDict[name] ??= 0;
|
||||
const idx = ++this.#nameDict[name];
|
||||
return `${name}${idx}`;
|
||||
}
|
||||
|
||||
public addGroup(name: string) { this.#groups.push(name); }
|
||||
|
||||
public get groups() { return this.#groups; }
|
||||
|
||||
public addSearch(search: string) { this.#searchs.push(search); }
|
||||
|
||||
public get searchs() { return this.#searchs; }
|
||||
}
|
||||
|
||||
class SQLBuilder<E extends BasicEntity> {
|
||||
#E: Class<E>
|
||||
#alias: string
|
||||
#ctx: SQLBuilderContext
|
||||
|
||||
constructor(ctx: SQLBuilderContext, entity: Class<E>, alias: string | undefined) {
|
||||
this.#ctx = ctx;
|
||||
this.#E = entity;
|
||||
this.#alias = alias ?? this.tableName;
|
||||
}
|
||||
|
||||
/** 实体类 */
|
||||
public get clazz() { return this.#E; }
|
||||
|
||||
/** 上下文 */
|
||||
public get ctx() { return this.#ctx; }
|
||||
|
||||
/** 实体配置 */
|
||||
public get config() { return getEntityConfig(this.#E); }
|
||||
|
||||
/** 取表别名 */
|
||||
public get alias() { return this.#alias; }
|
||||
|
||||
/** 表名称 */
|
||||
public get tableName() { return getTableName(this.#E); }
|
||||
|
||||
/** 获取表名SQL */
|
||||
public get tableSQL() {
|
||||
const config = getEntityConfig(this.#E);
|
||||
if (config.schema) return escapeID(config.schema, config.table);
|
||||
return escapeID(config.table);
|
||||
}
|
||||
|
||||
/** 字段遍历 */
|
||||
public eachField<R = unknown>(callback: (field: IEntityFieldConfig) => R): Array<R> {
|
||||
const fields = getEntityConfig(this.#E).fields;
|
||||
return fields.map(f => callback(f))
|
||||
}
|
||||
|
||||
/** 获取字段配置 */
|
||||
public fieldc(prop: string) {
|
||||
return getField(this.#E, prop);
|
||||
}
|
||||
|
||||
/** 获取join配置 */
|
||||
public joinc(prop: string) {
|
||||
return getJoin(this.#E, prop);
|
||||
}
|
||||
|
||||
/** 获取主键字段 */
|
||||
public get primaries() {
|
||||
return getEntityConfig(this.#E).fields.filter(f => f.primary);
|
||||
}
|
||||
|
||||
/** 获取主键字段名 */
|
||||
public get primaryNames() {
|
||||
return this.primaries.map(p => p.name);
|
||||
}
|
||||
|
||||
public buildEntity(data: any) {
|
||||
const e = new this.#E();
|
||||
for (const field of this.config.fields) {
|
||||
const val = data[`${this.alias}__${field.prop}`];
|
||||
e[field.prop as keyof E] = (val === null || val === undefined) ? val : field.decode(val);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
#checkPrimary<D>(dataItem: D) {
|
||||
return this.primaries.every(p => {
|
||||
const val = (dataItem as any)[`${this.alias}__${p.prop}`];
|
||||
return val !== null && val !== undefined;
|
||||
})
|
||||
}
|
||||
|
||||
public __build__<D>(data: D[], joinners: Joinner<any>[]) {
|
||||
const map: Array<[E, D[]]> = [];
|
||||
|
||||
//数据分组
|
||||
for (const item of data) {
|
||||
//空主键
|
||||
if (!this.#checkPrimary(item)) continue;
|
||||
let mapItem = map.findLast(([e, d]) => {
|
||||
return this.primaries.every(p => e[p.prop as keyof E] === item[`${this.alias}__${p.prop}` as keyof D] as any)
|
||||
});
|
||||
if (mapItem) mapItem[1].push(item);
|
||||
else map.push([this.buildEntity(item) as E, [item]]);
|
||||
}
|
||||
|
||||
//子数据
|
||||
return map.map(([val, data]) => {
|
||||
for (const joinner of joinners) joinner.build(val, data);
|
||||
return val;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function WithFilter<E extends BasicEntity, B extends Constructor<E>>(Base: B) {
|
||||
return class extends Base implements IFilterFn<E> {
|
||||
#columns?: Array<[string, string]>;
|
||||
#customColumns: Array<[string, string]> = [];
|
||||
|
||||
public filter(...columns: Array<BasicFieldName<E> | { [P in BasicFieldName<E>]+?: string }>) {
|
||||
this.#columns = [];
|
||||
if (!columns.length) columns = this.config.fields.map(f => f.prop as BasicFieldName<E>);
|
||||
for (const col of columns) {
|
||||
if (typeof col == "string") this.#columns.push([escapeID(this.alias, this.fieldc(col).name), escapeID(`${this.alias}__${col}`)]);
|
||||
else for (const [k, asName] of Object.entries(col)) {
|
||||
this.#columns.push([escapeID(this.alias, this.fieldc(k as string).name), asName as string]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public exclude(...columns: Array<BasicFieldName<E>>) {
|
||||
this.#columns = [];
|
||||
for (const col of this.config.fields) {
|
||||
if (columns.includes(col as any)) continue;
|
||||
this.#columns.push([escapeID(this.alias, col.name), escapeID(`${this.alias}__${col.prop}`)]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ignore() {
|
||||
this.#columns = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
public add(column: any, alias?: string) {
|
||||
if (typeof column == "string") this.#customColumns.push([escapeID(column), escapeID(alias ?? column)]);
|
||||
else if (typeof column == "function") this.#customColumns.push([column(), escapeID(alias!)]);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 字段列表 */
|
||||
public get columns() {
|
||||
//如果没有则构建所有字段
|
||||
if (!this.#columns) this.filter(...this.eachField(f => f.prop as BasicFieldName<E>));
|
||||
//返回字段
|
||||
return [...this.#columns!, ...this.#customColumns];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建条件语句,可用于where、having、on等语句
|
||||
* @param builder SQLBuilder
|
||||
* @param option 条件选项或条件语句
|
||||
* @param args 条件语句参数
|
||||
*/
|
||||
function buildCondition<E extends BasicEntity>(builder: SQLBuilder<E>, option: ConditionOption<any> | string, args?: any) {
|
||||
if (typeof option == "string") {
|
||||
if (typeof args === "undefined") return option;
|
||||
else return formatSQL(option, args);
|
||||
}
|
||||
else {
|
||||
const wheres: string[] = [];
|
||||
for (const [col, topVal] of Object.entries(option)) {
|
||||
const field = builder.fieldc(col);
|
||||
const fieldName = escapeID(builder.alias, field.name);
|
||||
if (topVal === null) wheres.push(`${fieldName} is null`);
|
||||
else if (typeof topVal == "object") {
|
||||
for (let [op, opv] of Object.entries(topVal as BasicWhere<any>) as [string, any][]) {
|
||||
switch (op) {
|
||||
case "in":
|
||||
wheres.push(`${fieldName} in (${(opv as any[]).map(v => field.encode(v)).join(",")})`);
|
||||
break;
|
||||
case "nin":
|
||||
wheres.push(`${fieldName} not in (${(opv as any[]).map(v => field.encode(v)).join(",")})`);
|
||||
break
|
||||
case "ne":
|
||||
if (opv === null) wheres.push(`${fieldName} is not null`);
|
||||
else wheres.push(`${fieldName}!=${field.encode(opv)}`);
|
||||
break;
|
||||
case "gt":
|
||||
wheres.push(`${fieldName}>${field.encode(opv)}`);
|
||||
break;
|
||||
case "gte":
|
||||
wheres.push(`${fieldName}>=${field.encode(opv)}`);
|
||||
break;
|
||||
case "lt":
|
||||
wheres.push(`${fieldName}<${field.encode(opv)}`);
|
||||
break;
|
||||
case "lte":
|
||||
wheres.push(`${fieldName}<=${field.encode(topVal)}`);
|
||||
break;
|
||||
case "like":
|
||||
wheres.push(`${fieldName} like ${escapeValue(opv)}`);
|
||||
break;
|
||||
case "nlike":
|
||||
wheres.push(`${fieldName} note like ${escapeValue(opv)}`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (typeof topVal !== "undefined") wheres.push(`${fieldName}=${field.encode(topVal)}`);
|
||||
}
|
||||
return wheres.join(" and ");
|
||||
}
|
||||
}
|
||||
|
||||
/** Where 混入 */
|
||||
function WithWhere<E extends BasicEntity, B extends Constructor<E>>(Base: B) {
|
||||
return class extends Base implements IWhereFn<E> {
|
||||
#wheres: string[] = [];
|
||||
|
||||
public where(option: ConditionOption<any> | string, args?: any) {
|
||||
const where = buildCondition(this, option, args);
|
||||
if (where) this.#wheres.push(where);
|
||||
return this;
|
||||
}
|
||||
|
||||
public search<T extends keyof ISearchOptionMap>(type: T, field: BasicFieldName<E>, option: ISearchOptionMap[T]) {
|
||||
const fieldName = escapeID(this.alias, field as string);
|
||||
switch (type) {
|
||||
case "tsquery": {
|
||||
const opt = option as ISearchOptionMap["tsquery"];
|
||||
let keywords: string[];
|
||||
let method: "or" | "and" = "or";
|
||||
let absolute = true;
|
||||
//处理选项
|
||||
if (typeof opt === "string") keywords = opt.split(/\s+/).map(s => s.trim()).filter(s => s.length);
|
||||
else if (opt instanceof Array) keywords = opt;
|
||||
else {
|
||||
if (typeof opt.keywords === "string") keywords = opt.keywords.split(/\s+/).map(s => s.trim()).filter(s => s.length);
|
||||
else keywords = opt.keywords;
|
||||
if (opt.method) method = opt.method;
|
||||
if (typeof opt.absolute == "boolean") absolute = opt.absolute;
|
||||
}
|
||||
//生成tsquery
|
||||
if (keywords.length) {
|
||||
const sql = keywords.map(k => absolute ? k : `${k}:*`).join(method == "or" ? "|" : "&");
|
||||
this.ctx.addSearch(`${fieldName} @@ to_tsquery(${escapeValue(sql)})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "like": {
|
||||
const opt = option as ISearchOptionMap["like"];
|
||||
const keywords = ((typeof opt === "string") ? opt : opt.keywords).trim();
|
||||
if (keywords.length) {
|
||||
const method = (typeof opt === "object") ? (opt.method ?? "both") : "both";
|
||||
switch (method) {
|
||||
case "left":
|
||||
this.ctx.addSearch(`${fieldName} like ${escapeValue(`${keywords}%`)}`);
|
||||
break;
|
||||
case "right":
|
||||
this.ctx.addSearch(`${fieldName} like ${escapeValue(`%${keywords}`)}`);
|
||||
break;
|
||||
case "both":
|
||||
this.ctx.addSearch(`${fieldName} like ${escapeValue(`%${keywords}%`)}`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public get wheres() { return this.#wheres; }
|
||||
};
|
||||
}
|
||||
|
||||
/** Join 混入 */
|
||||
function WithJoin<E extends BasicEntity, B extends Constructor<E>>(Base: B) {
|
||||
return class extends Base implements IJoinFn<E> {
|
||||
#joinners: Joinner<any>[] = [];
|
||||
|
||||
public join(name: string, alias?: any, callback?: any) {
|
||||
const joinConfig = this.joinc(name as string);
|
||||
const JoinEntity = joinConfig.entity();
|
||||
if (typeof callback == "undefined" && typeof alias == "function") {
|
||||
callback = alias;
|
||||
alias = undefined;
|
||||
}
|
||||
const joinner = new Joinner(this, name, this.alias, JoinEntity, joinConfig, alias ?? name);
|
||||
this.#joinners.push(joinner);
|
||||
if (callback) callback(joinner);
|
||||
return this;
|
||||
}
|
||||
|
||||
public get joinners() { return this.#joinners; }
|
||||
}
|
||||
}
|
||||
|
||||
/** Group by 混入 */
|
||||
function WithGroup<E extends BasicEntity, B extends Constructor<E>>(Base: B) {
|
||||
return class extends Base implements IGroupFn<E> {
|
||||
|
||||
public group(...name: Array<keyof E | (() => string)>) {
|
||||
for (const item of name) {
|
||||
if (typeof item == "function") this.ctx.addGroup(item());
|
||||
else if (typeof item == "string") this.ctx.addGroup(escapeID(this.alias, item));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returning 混入 */
|
||||
function WithReturning<E extends BasicEntity, B extends Constructor<E>>(Base: B) {
|
||||
return class extends Base implements IReturningFn<E> {
|
||||
#returning?: string[]
|
||||
|
||||
public returning(...columns: Array<BasicFieldName<E>>) {
|
||||
if (!columns.length) columns = this.config.fields.map(c => c.name as BasicFieldName<E>);
|
||||
this.#returning = columns.map(c => `${escapeID(this.alias, c as string)} as ${escapeID(`${this.alias}__${c as string}`)}`);
|
||||
return this;
|
||||
}
|
||||
|
||||
public get returnings() { return this.#returning; }
|
||||
|
||||
public get returningsWithDefault() {
|
||||
return this.returnings ?? this.primaries.map(c => `${escapeID(this.alias, c.name)} as ${escapeID(`${this.alias}__${c.prop}`)}`);
|
||||
}
|
||||
|
||||
public buildReturningData<D>(data: D[]) { return data.map(d => this.buildEntity(d)); }
|
||||
}
|
||||
}
|
||||
|
||||
class Joinner<E extends BasicEntity> extends WithGroup(WithJoin(WithWhere(WithFilter(SQLBuilder))))<E> implements IJoinBuilder<E> {
|
||||
#joinName: string
|
||||
#joinConfig: IEntityJoinConfig
|
||||
#baseName: string
|
||||
#origin: SQLBuilder<any>
|
||||
#ons: string[] = [];
|
||||
|
||||
public constructor(origin: SQLBuilder<any>, baseName: string, joinName: string, entity: Class<E>, config: IEntityJoinConfig, alias: string) {
|
||||
super(origin.ctx, entity, alias);
|
||||
this.#origin = origin;
|
||||
this.#baseName = baseName;
|
||||
this.#joinName = joinName;
|
||||
this.#joinConfig = config;
|
||||
}
|
||||
|
||||
public on(option: ConditionOption<any> | string, args?: any) {
|
||||
const onSQL = buildCondition(this, option, args);
|
||||
if (onSQL) this.#ons.push(onSQL);
|
||||
return this;
|
||||
}
|
||||
|
||||
public get __select_columns__(): Array<[name: string, alias: string]> {
|
||||
const result = [...this.columns];
|
||||
this.joinners.forEach(j => result.push(...j.__select_columns__));
|
||||
return result;
|
||||
}
|
||||
|
||||
public get __wheres__(): string[] {
|
||||
const result = [...this.wheres];
|
||||
this.joinners.forEach(j => result.push(...j.__wheres__));
|
||||
return result;
|
||||
}
|
||||
|
||||
public get __join_sql__() {
|
||||
const onSQL = [
|
||||
`${this.#joinConfig.builder(this.#joinName, this.alias)}`,
|
||||
...this.#ons,
|
||||
].join(" and ");
|
||||
return `left join ${this.tableSQL} ${escapeID(this.alias)} on ${onSQL}`;
|
||||
}
|
||||
|
||||
public build<BE extends BasicEntity, D>(base: BE, data: D[]) {
|
||||
const parsed = this.__build__(data, this.joinners);
|
||||
if (this.#joinConfig.many) base[this.#baseName as keyof BE] = parsed as any;
|
||||
else base[this.#baseName as keyof BE] = (parsed[0] ?? null) as any;
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectBuilder<E extends BasicEntity> extends WithGroup(WithJoin(WithWhere(WithFilter(SQLBuilder))))<E> implements ISelectBuilder<E> {
|
||||
#offset?: number
|
||||
#limit?: number
|
||||
#orders?: Array<[name: string, order: "asc" | "desc"]>
|
||||
#query: IQueryFunc<any>
|
||||
#fullJoinners?: Joinner<any>[]
|
||||
|
||||
constructor(query: IQueryFunc<any>, entity: Class<E>, alias?: string) {
|
||||
super(new SQLBuilderContext(), entity, alias);
|
||||
this.#query = query;
|
||||
}
|
||||
|
||||
public offset(offset: number) {
|
||||
this.#offset = offset;
|
||||
return this;
|
||||
}
|
||||
|
||||
public limit(limit: number) {
|
||||
this.#limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public order(name: any, sort: "asc" | "desc") {
|
||||
this.#orders ??= [];
|
||||
const col = this.config.fields.find(f => f.prop === name);
|
||||
if (col) this.#orders.push([escapeID(this.alias, col.name), sort]);
|
||||
else this.#orders.push([name, sort]);
|
||||
return this;
|
||||
}
|
||||
|
||||
public async find() {
|
||||
const sql = this.__sql__(false);
|
||||
// console.log(sql);
|
||||
const result = await this.#query(sql);
|
||||
const ret = this.build(result.rows);
|
||||
return ret as E[];
|
||||
}
|
||||
|
||||
async findOne() { return this.find().then(res => res[0] ?? null); }
|
||||
|
||||
async count() {
|
||||
const sql = `select count(*) as __data_count__ from (\n\t${this.__inner_sql__(false, "\n\t")}\n)`;
|
||||
const res = await this.#query(sql);
|
||||
return parseInt(res.rows[0].__data_count__) as number
|
||||
}
|
||||
|
||||
async findAndCount() {
|
||||
const res = await this.#query(this.__sql__(true));
|
||||
const count = res.rows[0]?.__data_count__ ?? 0;
|
||||
return [this.build(res.rows) as E[], parseInt(count)] as [E[], number];
|
||||
}
|
||||
|
||||
async query<T>(): Promise<T[]> {
|
||||
const sql = this.__sql__(false);
|
||||
const res = await this.#query(sql);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
public get __select_columns__() {
|
||||
const result = [...this.columns];
|
||||
this.joinners.forEach(j => result.push(...j.__select_columns__));
|
||||
return result.map(([c, a]) => `${c} as ${a}`)
|
||||
}
|
||||
|
||||
#whereCache: string | undefined = undefined;
|
||||
public get __wheres__() {
|
||||
if (!this.#whereCache) {
|
||||
const result = [...this.wheres];
|
||||
this.joinners.forEach(j => result.push(...j.__wheres__));
|
||||
if (this.ctx.searchs.length) result.push(this.ctx.searchs.join(" or "))
|
||||
this.#whereCache = result.join(" and ");
|
||||
}
|
||||
return this.#whereCache;
|
||||
}
|
||||
|
||||
private getAllJoinners(joinners: Joinner<any>[]) {
|
||||
const result = [...joinners];
|
||||
joinners.forEach(j => result.push(...this.getAllJoinners(j.joinners)));
|
||||
return result;
|
||||
}
|
||||
|
||||
public get __joins__() {
|
||||
this.#fullJoinners ??= this.getAllJoinners(this.joinners);
|
||||
return this.#fullJoinners.map(j => j.__join_sql__)
|
||||
}
|
||||
|
||||
public __inner_sql__(count: boolean, join = "\n") {
|
||||
//内部的join
|
||||
const innerSQLItems = [
|
||||
`select ${escapeID(this.alias)}.*${count ? `, count(*) over () as __data_count__` : ''}`, //字段列表
|
||||
`from ${this.tableSQL} ${escapeID(this.alias)}`, //表名
|
||||
...this.__joins__, //join
|
||||
...this.__wheres__.length ? [`where ${this.__wheres__}`] : [], //where条件
|
||||
`group by ${this.primaryNames.map(p => escapeID(this.alias, p)).join(",")}`, //内部查询分组
|
||||
...(!count && this.#orders?.length) ? [`order by ${this.#orders.map(([name, order]) => `${name} ${order}`).join(",")}`] : [], //排序
|
||||
...(typeof this.#offset == "number") ? [`offset ${this.#offset}`] : [], //offset
|
||||
...(typeof this.#limit == "number") ? [`limit ${this.#limit}`] : [], //limit
|
||||
];
|
||||
return innerSQLItems.join(join);
|
||||
}
|
||||
|
||||
public __sql__(count: boolean) {
|
||||
//外部的SQL
|
||||
const outterSQLItems = [
|
||||
`select ${this.__select_columns__}${count ? `, ${escapeID(this.alias)}.__data_count__` : ''}`, //字段列表
|
||||
`from (\n\t${this.__inner_sql__(count, "\n\t")}\n) ${escapeID(this.alias)}`, //子查询
|
||||
...this.__joins__, //join
|
||||
...this.__wheres__.length ? [`where ${this.__wheres__}`] : [], //where条件
|
||||
...this.ctx.groups.length ? [`group by ${this.ctx.groups.join(",")}`] : [], //分组
|
||||
...this.#orders ? [`order by ${this.#orders.map(([name, ord]) => `${name} ${ord}`).join(", ")}`] : [], //排序
|
||||
];
|
||||
|
||||
return outterSQLItems.join("\n");
|
||||
}
|
||||
|
||||
public build<D>(data: D[]) {
|
||||
return this.__build__(data, this.joinners);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class InsertBuilder<E extends BasicEntity> extends WithReturning(SQLBuilder)<E> implements IInsertBuilder<E> {
|
||||
#query: IQueryFunc<any>
|
||||
#values: string[] = [];
|
||||
#columns: string[] = [];
|
||||
|
||||
public constructor(query: IQueryFunc<any>, entity: Class<E>) {
|
||||
super(new SQLBuilderContext(), entity, undefined);
|
||||
this.#query = query;
|
||||
}
|
||||
|
||||
public data<Name extends BasicFieldName<E>>(data: Pick<E, Name> | Pick<E, Name>[]) {
|
||||
data = (data instanceof Array) ? data : [data];
|
||||
this.#values = [];
|
||||
this.#columns = [];
|
||||
let columnsSet = false;
|
||||
|
||||
for (const item of data) {
|
||||
const buffer: (string | null)[] = [];
|
||||
for (const col of this.config.fields) {
|
||||
if (!(col.prop in item)) continue;
|
||||
const val = item[col.prop as keyof typeof item];
|
||||
if (val === null || val === undefined) buffer.push("null");
|
||||
else buffer.push(col.encode(val));
|
||||
if (!columnsSet) this.#columns.push(escapeID(col.name));
|
||||
}
|
||||
this.#values.push(`(${buffer.join(",")})`);
|
||||
columnsSet = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async query() {
|
||||
if (!this.#values.length) return [];
|
||||
//构建SQL
|
||||
const sqlItems = [
|
||||
`insert into ${this.tableSQL} as ${escapeID(this.alias)} (${this.#columns.join(",")}) values`,
|
||||
this.#values.join(",\n"),
|
||||
`returning ${this.returningsWithDefault}`,
|
||||
]
|
||||
const sql = sqlItems.join("\n");
|
||||
|
||||
//执行并返回数据
|
||||
const res = await this.#query(sql);
|
||||
return this.buildReturningData(res.rows) as E[];
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateBuilder<E extends BasicEntity> extends WithReturning(WithWhere(SQLBuilder))<E> implements IUpdateBuilder<E> {
|
||||
#query: IQueryFunc<any>
|
||||
#updates?: string[]
|
||||
|
||||
public constructor(query: IQueryFunc<any>, entity: Class<E>) {
|
||||
super(new SQLBuilderContext(), entity, undefined);
|
||||
this.#query = query;
|
||||
}
|
||||
|
||||
public set(data: Partial<Pick<E, BasicFieldName<E>>>) {
|
||||
this.#updates = []
|
||||
for (const col of this.config.fields) {
|
||||
const val = data[col.prop as keyof typeof data];
|
||||
if (val === undefined) continue;
|
||||
if (val === null) this.#updates.push(`${escapeID(col.name)}=null`);
|
||||
else this.#updates.push(`${escapeID(col.name)}=${col.encode(val)}`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public async query() {
|
||||
if (!this.#updates?.length) return [];
|
||||
//构建SQL
|
||||
const wheres = this.wheres;
|
||||
const sqlItems = [
|
||||
`update ${this.tableSQL} ${escapeID(this.alias)}`,
|
||||
`set ${this.#updates.join(",")}`,
|
||||
...wheres.length ? [`where ${wheres.join(" and ")}`] : [],
|
||||
`returning ${this.returningsWithDefault}`,
|
||||
]
|
||||
const sql = sqlItems.join("\n");
|
||||
|
||||
//执行并构建返回数据
|
||||
const res = await this.#query(sql);
|
||||
return this.buildReturningData(res.rows) as E[];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DeleteBuilder<E extends BasicEntity> extends WithReturning(WithWhere(SQLBuilder))<E> implements IDeleteBuilder<E> {
|
||||
#query: IQueryFunc<any>
|
||||
|
||||
public constructor(query: IQueryFunc<any>, entity: Class<E>) {
|
||||
super(new SQLBuilderContext(), entity, undefined);
|
||||
this.#query = query;
|
||||
}
|
||||
|
||||
public async query() {
|
||||
//构建SQL
|
||||
const wheres = this.wheres;
|
||||
const sqlItems = [
|
||||
`delete from ${this.tableSQL} ${escapeID(this.alias)}`,
|
||||
...wheres.length ? [`where ${wheres.join(" and ")}`] : [],
|
||||
`returning ${this.returningsWithDefault}`,
|
||||
]
|
||||
const sql = sqlItems.join("\n");
|
||||
|
||||
//执行并构建返回数据
|
||||
const res = await this.#query(sql);
|
||||
return this.buildReturningData(res.rows) as E[];
|
||||
}
|
||||
}
|
104
src/test.ts
Normal file
104
src/test.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { database } from "./database";
|
||||
import { BasicEntity, DateTimeColumn, IntColumn, JoinOne, StringColumn, Table } from "./entity";
|
||||
|
||||
database.config(() => ({
|
||||
host: "127.0.0.1",
|
||||
port: 5432,
|
||||
user: "postgres",
|
||||
password: "ljk5408828",
|
||||
database: "urnas",
|
||||
max: 100,
|
||||
}));
|
||||
|
||||
|
||||
/** 数据实体 */
|
||||
class DataEntity extends BasicEntity {
|
||||
/** ID */
|
||||
@IntColumn({ primary: true }) declare id: number
|
||||
}
|
||||
|
||||
/** 带有时间的实体 */
|
||||
class TimedEntity extends DataEntity {
|
||||
/** 创建时间 */
|
||||
@DateTimeColumn() declare ctime: string
|
||||
|
||||
/** 修改时间 */
|
||||
@DateTimeColumn() declare utime: string | null
|
||||
|
||||
/** 删除时间 */
|
||||
@DateTimeColumn() declare dtime: string | null
|
||||
}
|
||||
|
||||
|
||||
export enum UserRole {
|
||||
/** 系统管理员 */
|
||||
SystemAdmin = 1 << 0,
|
||||
/** 密码本管理员*/
|
||||
CodeBookAdmin = 1 << 1,
|
||||
/** 相册管理员 */
|
||||
AlbumAdmin = 1 << 2,
|
||||
/** FM管理员 */
|
||||
FMAdmin = 1 << 3,
|
||||
|
||||
/** 超级用户 */
|
||||
Super = SystemAdmin | CodeBookAdmin | AlbumAdmin | FMAdmin,
|
||||
/** 常规用户 */
|
||||
User = 0,
|
||||
};
|
||||
|
||||
/** 用户 */
|
||||
@Table("user.user")
|
||||
export class User extends TimedEntity {
|
||||
/** 账号 */
|
||||
@StringColumn() declare number: string
|
||||
|
||||
/** 密码 */
|
||||
@StringColumn() declare password: string
|
||||
|
||||
/** 昵称 */
|
||||
@StringColumn() declare name: string
|
||||
|
||||
/** 角色 */
|
||||
@IntColumn() declare role: UserRole
|
||||
|
||||
/** 邮箱 */
|
||||
@StringColumn() declare email: string | null
|
||||
|
||||
/** 手机 */
|
||||
@StringColumn() declare phone: string | null
|
||||
|
||||
/** 头像 */
|
||||
@StringColumn() declare avatar: string | null
|
||||
|
||||
/** 登录过期时间 */
|
||||
@DateTimeColumn() declare ttime: string
|
||||
}
|
||||
|
||||
@Table("user.user_login")
|
||||
export class UserLogin extends DataEntity {
|
||||
@IntColumn() declare userID: number
|
||||
|
||||
@JoinOne(() => User, "userID") declare user: User
|
||||
|
||||
@JoinOne(() => User, "userID") declare test: User
|
||||
|
||||
@StringColumn() declare device: string | null
|
||||
|
||||
@StringColumn() declare ip: string
|
||||
|
||||
@DateTimeColumn() declare ctime: string
|
||||
}
|
||||
|
||||
database.select(UserLogin)
|
||||
.join("user")
|
||||
.join("test")
|
||||
.find()
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
console.log("END")
|
||||
database.close();
|
||||
});
|
1
src/type/index.ts
Normal file
1
src/type/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./tsvector";
|
127
src/type/tsvector.ts
Normal file
127
src/type/tsvector.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import pinyin from "pinyin";
|
||||
|
||||
export class TSVector {
|
||||
#items: string[];
|
||||
|
||||
|
||||
private constructor(items: string[]) {
|
||||
this.#items = items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库值
|
||||
*/
|
||||
public get value() {
|
||||
return this.#items.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否包含指定元素
|
||||
* @param item 元素
|
||||
*/
|
||||
public contains(item: string) { return this.#items.includes(item); }
|
||||
|
||||
/** 获取元素列表 */
|
||||
public get items() { return this.#items; }
|
||||
|
||||
/**
|
||||
* 添加元素
|
||||
* @param item 要添加的元素
|
||||
*/
|
||||
public push(item: string) {
|
||||
item = item.replaceAll("'", "");
|
||||
if (this.contains(item)) return;
|
||||
this.#items.push(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除元素
|
||||
* @param item 要删除的元素
|
||||
*/
|
||||
public remove(item: string) {
|
||||
item = item.replaceAll("'", "");
|
||||
const index = this.#items.indexOf(item);
|
||||
if (index >= 0) this.#items.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库值构建
|
||||
* @param value 数据库值
|
||||
*/
|
||||
public static fromValue(value: string) {
|
||||
const items = value.split(" ").map(s => s.trim()).filter(s => !!s).map(s => s.replaceAll("'", ""));
|
||||
return new this(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从关键词构建
|
||||
* @param keywords 关键词列表
|
||||
*/
|
||||
public static fromKeyworlds(keywords: string[]) {
|
||||
const items: string[] = []
|
||||
|
||||
function resolvePinyin(pinyins: string[][]): string[][] {
|
||||
if (!pinyins.length) return [];
|
||||
let [first, ...rest] = pinyins;
|
||||
//后续结果
|
||||
const ties = resolvePinyin(rest);
|
||||
//处理第一个
|
||||
first = first.map(s => s.trim().replace(/[^a-zA-Z0-9 ]/g, "")).filter(s => !!s);
|
||||
if (!first.length) return ties;
|
||||
|
||||
//组合结果
|
||||
const result: string[][] = [];
|
||||
for (let py of first) {
|
||||
let leads = py.split(" "); //这里处理一下英文和数字
|
||||
if (ties.length) {
|
||||
for (const tie of ties) result.push([...leads, ...tie]);
|
||||
}
|
||||
else result.push(leads);
|
||||
}
|
||||
|
||||
//返回
|
||||
return result;
|
||||
}
|
||||
|
||||
//处理关键词
|
||||
keywords = keywords.map(s => s.trim()).filter(s => !!s);
|
||||
|
||||
//屏蔽纯字母数字的关键字
|
||||
keywords = keywords.filter(kw => {
|
||||
if (/^[a-zA-Z0-9]+$/.test(kw)) {
|
||||
items.push(kw);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
//处理给定的每个关键词
|
||||
for (let kw of keywords) {
|
||||
//获取拼音
|
||||
const pinyinList = pinyin(kw, { style: pinyin.STYLE_NORMAL, segment: true, heteronym: true });
|
||||
|
||||
//处理拼音
|
||||
for (const pys of resolvePinyin(pinyinList)) {
|
||||
//基础拼音
|
||||
const basePinyin = pys.join("");
|
||||
if (!items.includes(basePinyin)) items.push(basePinyin);
|
||||
//首字母
|
||||
const simplePinyin = pys.map(p => {
|
||||
if (/^\d+$/.test(p)) return p;
|
||||
return p[0];
|
||||
}).join("");
|
||||
if (!items.includes(simplePinyin)) items.push(simplePinyin);
|
||||
}
|
||||
|
||||
//关键字本身
|
||||
kw = kw.replaceAll(" ", '');
|
||||
if (!items.includes(kw)) items.push(kw);
|
||||
}
|
||||
|
||||
//完成
|
||||
return new this(items.map(s => s.replaceAll("'", "")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// console.log(TSVector.fromKeyworlds(["yizhi kangkang"]))
|
62
src/types.ts
Normal file
62
src/types.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import type { BasicEntity } from "./entity";
|
||||
|
||||
export type Class<T> = new (...args: any[]) => T;
|
||||
|
||||
export type ArrayType<T> = T extends Array<infer U> ? U : never;
|
||||
export type ArrayTypeSelf<T> = T extends Array<infer U> ? U : T;
|
||||
|
||||
export type Values<T> = T[keyof T];
|
||||
|
||||
export namespace Decorators {
|
||||
export type ClassDecorator<T> = (target: Class<T>) => any;
|
||||
export type PropDecorator<T> = (target: T, propertyKey: string) => any;
|
||||
export type MethodDecorator<T> = (target: T, propertyKey: string, descriptor: PropertyDescriptor) => any;
|
||||
export type ParamDecorator<T> = (target: T, propertyKey: string, parameterIndex: number) => any;
|
||||
}
|
||||
|
||||
|
||||
/** 取基础类型的字段名 */
|
||||
export type BasicFieldName<T extends BasicEntity> = Values<{ [P in keyof T]:
|
||||
//忽略never
|
||||
Exclude<T[P], null | undefined> extends never ? never : (
|
||||
//忽略函数
|
||||
Exclude<T[P], null | undefined> extends Function ? never : (
|
||||
//忽略实体
|
||||
ArrayTypeSelf<Exclude<T[P], null | undefined>> extends BasicEntity ? never : (
|
||||
P
|
||||
)
|
||||
)
|
||||
)
|
||||
}>;
|
||||
|
||||
|
||||
/** 可以用作ID的字段名称 */
|
||||
export type IDLikeFieldName<T extends BasicEntity> = Values<{ [P in keyof T]:
|
||||
Exclude<T[P], null | undefined> extends never ? never : (
|
||||
string extends Exclude<T[P], null | undefined> ? P : (
|
||||
number extends Exclude<T[P], null | undefined> ? P : never
|
||||
)
|
||||
)
|
||||
}>;
|
||||
|
||||
|
||||
/** 获取可用于ManyJoin的字段名 */
|
||||
export type ManyJoinFieldName<T extends BasicEntity> = Values<{ [P in keyof T]:
|
||||
//忽略never
|
||||
ArrayType<Exclude<T[P], null | undefined>> extends never ? never : (
|
||||
//只取Array实体
|
||||
ArrayType<Exclude<T[P], null | undefined>> extends BasicEntity ? P : never
|
||||
)
|
||||
}>;
|
||||
|
||||
/** 获取可以用户OneJoin的字段名 */
|
||||
export type OneJoinFieldName<T extends BasicEntity> = Values<{ [P in keyof T]:
|
||||
//忽略never
|
||||
Exclude<T[P], null | undefined> extends never ? never : (
|
||||
//只取Array实体
|
||||
Exclude<T[P], null | undefined> extends BasicEntity ? P : never
|
||||
)
|
||||
}>;
|
||||
|
||||
/** 可以Join的字段名 */
|
||||
export type JoinaFieldName<T extends BasicEntity> = OneJoinFieldName<T> | ManyJoinFieldName<T>;
|
35
src/util.ts
Normal file
35
src/util.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { DatabaseError } from "./error";
|
||||
|
||||
export function escapeID(...keys: string[]) {
|
||||
return keys.map(key => `"${key}"`).join(".");
|
||||
}
|
||||
|
||||
export function escapeValue(value: any) {
|
||||
if (typeof value === "number") return value.toString();
|
||||
else if (typeof value === "string") return `E'${(value).replaceAll("'", "\\x27")}'`;
|
||||
else if (typeof value === "boolean") return value.toString();
|
||||
else if (typeof value === "bigint") return value.toString();
|
||||
else if (value === null) return "null";
|
||||
else throw new DatabaseError("DB_QUERY_ESCAPE_VALUE_ERROR", "无法转义的值");
|
||||
}
|
||||
|
||||
|
||||
export function formatSQL(sql: string, args: any[] | Record<string, any>) {
|
||||
if (args instanceof Array) {
|
||||
const _args = [...args];
|
||||
return sql.replace(/\?/g, () => {
|
||||
const v = _args.shift();
|
||||
if (v === undefined) return "null";
|
||||
else if (v instanceof Array) return v.map(escapeValue).join(",");
|
||||
else return escapeValue(v);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return sql.replace(/\{:(\w+)\}/g, (match, key) => {
|
||||
const v = args[key];
|
||||
if (v === undefined) return "null";
|
||||
else if (v instanceof Array) return v.map(escapeValue).join(",");
|
||||
else return escapeValue(v);
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user