初次提交
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
|
/typing
|
||||||
|
/package-lock.json
|
||||||
|
/.vscode
|
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@yizhi/database",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "typing/index.d.ts",
|
||||||
|
"scripts": {},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"pinyin": "^4.0.0-alpha.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.8.1",
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
|
"@types/pinyin": "^2.10.2",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
100
tsconfig.json
Normal file
100
tsconfig.json
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
"experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "Node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
/* Emit */
|
||||||
|
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
"inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
"declarationDir": "./typing", /* Specify the output directory for generated declaration files. */
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user