初次提交

This commit is contained in:
2024-10-30 14:31:08 +08:00
commit ebfefc2295
13 changed files with 1902 additions and 0 deletions

182
src/database.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
export * from "./tsvector";

127
src/type/tsvector.ts Normal file
View 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
View 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
View 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);
});
}
}