增加范围类型支持
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yizhi/postgres",
|
"name": "@yizhi/postgres",
|
||||||
"version": "1.0.13",
|
"version": "1.0.14",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "typing/index.d.ts",
|
"types": "typing/index.d.ts",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
|
114
src/entity.ts
114
src/entity.ts
@ -1,10 +1,12 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { DatabaseError } from "./error";
|
import { DatabaseError } from "./error";
|
||||||
import { TSVector } from "./type";
|
import { DateRange, DateTimeRange, FloatRange, Int4Range, TSVector } from "./type";
|
||||||
import { escapeID, escapeValue } from "./util";
|
import { escapeID, escapeValue } from "./util";
|
||||||
import type { Class, Decorators, IDLikeFieldName } from "./types";
|
import type { Class, Decorators, IDLikeFieldName } from "./types";
|
||||||
|
|
||||||
export interface IEntityFieldConfig {
|
export interface IEntityFieldConfig {
|
||||||
|
/** 类型名称 */
|
||||||
|
typename: string
|
||||||
/** 字段名 */
|
/** 字段名 */
|
||||||
name: string
|
name: string
|
||||||
/** 解析后的属性名 */
|
/** 解析后的属性名 */
|
||||||
@ -17,6 +19,8 @@ export interface IEntityFieldConfig {
|
|||||||
encode: (data: any) => string
|
encode: (data: any) => string
|
||||||
/** 解码方式(查询数据库时) */
|
/** 解码方式(查询数据库时) */
|
||||||
decode: (data: any) => any
|
decode: (data: any) => any
|
||||||
|
/** 字段选项 */
|
||||||
|
option?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -136,16 +140,18 @@ export function Table(table: string): Decorators.ClassDecorator<BasicEntity> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义一个字段
|
* 定义一个字段
|
||||||
|
* @param typename 字段类型名称
|
||||||
* @param name 字段名称
|
* @param name 字段名称
|
||||||
* @param encode 字段编码凡是
|
* @param encode 字段编码凡是
|
||||||
* @param decode 字段解码方式
|
* @param decode 字段解码方式
|
||||||
* @param primary 是否是主键
|
* @param primary 是否是主键
|
||||||
* @param virtual 是否是虚拟字段
|
* @param virtual 是否是虚拟字段
|
||||||
|
* @param option 字段选项
|
||||||
*/
|
*/
|
||||||
export function Field(name: string | undefined, encode: IEntityFieldConfig["encode"], decode: IEntityFieldConfig["decode"], primary: boolean, virtual: boolean): Decorators.PropDecorator<BasicEntity> {
|
export function Field(typename: string, name: string | undefined, encode: IEntityFieldConfig["encode"], decode: IEntityFieldConfig["decode"], primary: boolean, virtual: boolean, option?: Record<string, any>): Decorators.PropDecorator<BasicEntity> {
|
||||||
return function (target, propertyKey) {
|
return function (target, propertyKey) {
|
||||||
const conf = entityConfigOf(target.constructor)
|
const conf = entityConfigOf(target.constructor)
|
||||||
conf.fields.push({ name: name ?? propertyKey, prop: propertyKey, encode, decode, primary, virtual });
|
conf.fields.push({ typename, name: name ?? propertyKey, prop: propertyKey, encode, decode, primary, virtual, option: option });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +168,7 @@ export function IntColumn(name?: any, option?: any) {
|
|||||||
option = name;
|
option = name;
|
||||||
name = undefined;
|
name = undefined;
|
||||||
}
|
}
|
||||||
return Field(name, v => escapeValue(v), v => v, option?.primary ?? false, option?.virtual ?? false);
|
return Field("int", name, v => escapeValue(v), v => v, option?.primary ?? false, option?.virtual ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -179,7 +185,7 @@ export function FloatColumn(name?: any, option?: any) {
|
|||||||
name = undefined;
|
name = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Field(name, v => escapeValue(v), v => parseFloat(v), false, option?.virtual ?? false);
|
return Field("float", name, v => escapeValue(v), v => parseFloat(v), false, option?.virtual ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -195,7 +201,7 @@ export function StringColumn(name?: any, option?: any) {
|
|||||||
option = name
|
option = name
|
||||||
name = undefined
|
name = undefined
|
||||||
}
|
}
|
||||||
return Field(name, v => escapeValue(v), v => v, option?.primary ?? false, option?.virtual ?? false);
|
return Field("string", name, v => escapeValue(v), v => v, option?.primary ?? false, option?.virtual ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -218,7 +224,7 @@ export function BooleanColumn(name?: any, option?: any) {
|
|||||||
return ["true", "yes", "on"].includes(v)
|
return ["true", "yes", "on"].includes(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Field(name, (v) => escapeValue(checker(v)), checker, false, option?.virtual ?? false);
|
return Field("boolean", name, (v) => escapeValue(checker(v)), checker, false, option?.virtual ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -234,7 +240,7 @@ export function DateColumn(name?: any, option?: any) {
|
|||||||
name = undefined
|
name = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return Field(name, v => {
|
return Field("date", name, v => {
|
||||||
if (v instanceof DateColumn) v = moment(v);
|
if (v instanceof DateColumn) v = moment(v);
|
||||||
if (typeof v == "string") v = moment(v);
|
if (typeof v == "string") v = moment(v);
|
||||||
if (!moment.isMoment(v)) return escapeValue(null);
|
if (!moment.isMoment(v)) return escapeValue(null);
|
||||||
@ -257,7 +263,7 @@ export function DateTimeColumn(name?: any, option?: any) {
|
|||||||
name = undefined
|
name = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return Field(name, v => {
|
return Field("datetime", name, v => {
|
||||||
if (v instanceof DateColumn) v = moment(v);
|
if (v instanceof DateColumn) v = moment(v);
|
||||||
if (typeof v == "string") v = moment(v);
|
if (typeof v == "string") v = moment(v);
|
||||||
if (!moment.isMoment(v)) return escapeValue(null);
|
if (!moment.isMoment(v)) return escapeValue(null);
|
||||||
@ -270,15 +276,15 @@ export function DateTimeColumn(name?: any, option?: any) {
|
|||||||
|
|
||||||
|
|
||||||
//数组定义
|
//数组定义
|
||||||
function _Array<T>(name: string | undefined, typeName: string, itemEncoder: (v: any) => (string | null), itemDecoder: ((v: any) => (T | null)) | undefined, virtual: boolean) {
|
function _Array<T>(baseTypename: string, name: string | undefined, typeName: string, itemEncoder: (v: any) => (string | null), itemDecoder: ((v: any) => (T | null)) | undefined, virtual: boolean, option?: Record<string, any>) {
|
||||||
return Field(name, v => {
|
return Field(`${baseTypename}[]`, name, v => {
|
||||||
if (!(v instanceof Array)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_ARRAY", `字段${name}需要给定一个数组`);
|
if (!(v instanceof Array)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_ARRAY", `字段${name}需要给定一个数组`);
|
||||||
return escapeValue(`{${v.map(item => itemEncoder(item)).join(",")}}`) + `::${typeName}`;
|
return escapeValue(`{${v.map(item => itemEncoder(item)).join(",")}}`) + `::${typeName}`;
|
||||||
}, v => {
|
}, v => {
|
||||||
if (!(v instanceof Array)) return null;
|
if (!(v instanceof Array)) return null;
|
||||||
if (itemDecoder) return v.map(item => itemDecoder(item));
|
if (itemDecoder) return v.map(item => itemDecoder(item));
|
||||||
else return v;
|
else return v;
|
||||||
}, false, virtual);
|
}, false, virtual, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -294,7 +300,9 @@ export function IntArrayColumn(name?: any, option?: any) {
|
|||||||
name = undefined
|
name = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return _Array(name, `int${option?.bytes ?? 4}[]`, item => {
|
const bytes = option?.bytes ?? 4;
|
||||||
|
|
||||||
|
return _Array("int", name, `int${bytes}[]`, item => {
|
||||||
item = parseInt(item);
|
item = parseInt(item);
|
||||||
if (isNaN(item) || !isFinite(item)) return null;
|
if (isNaN(item) || !isFinite(item)) return null;
|
||||||
return item.toString();
|
return item.toString();
|
||||||
@ -302,7 +310,7 @@ export function IntArrayColumn(name?: any, option?: any) {
|
|||||||
item = parseInt(item);
|
item = parseInt(item);
|
||||||
if (isNaN(item) || !isFinite(item)) return null;
|
if (isNaN(item) || !isFinite(item)) return null;
|
||||||
return item;
|
return item;
|
||||||
}, option?.virtual ?? false);
|
}, option?.virtual ?? false, { bytes });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -318,7 +326,7 @@ export function StringArrayColumn(name?: any, option?: any) {
|
|||||||
name = undefined
|
name = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return _Array(name, "varchar[]", item => {
|
return _Array("string", name, "varchar[]", item => {
|
||||||
if (typeof item == "string") return item;
|
if (typeof item == "string") return item;
|
||||||
else return null;
|
else return null;
|
||||||
}, item => item, option?.virtual ?? false);
|
}, item => item, option?.virtual ?? false);
|
||||||
@ -336,7 +344,7 @@ export function JsonColumn(name?: any, option?: any) {
|
|||||||
name = undefined
|
name = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return Field(name, v => escapeValue(JSON.stringify(v)) + "::jsonb", v => v, false, option?.virtual ?? false);
|
return Field("json", name, v => escapeValue(JSON.stringify(v)) + "::jsonb", v => v, false, option?.virtual ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -344,12 +352,84 @@ export function JsonColumn(name?: any, option?: any) {
|
|||||||
* @param name 字段名称
|
* @param name 字段名称
|
||||||
*/
|
*/
|
||||||
export function TSVectorColumn(name?: string) {
|
export function TSVectorColumn(name?: string) {
|
||||||
return Field(name, v => {
|
return Field("tsvector", name, v => {
|
||||||
if (!(v instanceof TSVector)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_TSVECTOR", `字段${name}需要给定一个TSVector`)
|
if (!(v instanceof TSVector)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_TSVECTOR", `字段${name}需要给定一个TSVector`)
|
||||||
return `E'${v.value}'::tsvector`;
|
return `E'${v.value}'::tsvector`;
|
||||||
}, v => TSVector.fromValue(v), false, false);
|
}, v => TSVector.fromValue(v), false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义int4range字段
|
||||||
|
* @param name 字段名称
|
||||||
|
* @param option 字段选项
|
||||||
|
*/
|
||||||
|
export function Int4RangeColumn(option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function Int4RangeColumn(name: string, option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function Int4RangeColumn(name?: any, option?: any) {
|
||||||
|
if (typeof name != "string") {
|
||||||
|
option = name
|
||||||
|
name = undefined
|
||||||
|
}
|
||||||
|
return Field("range", name, v => {
|
||||||
|
if (!(v instanceof Int4Range)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_INT4RANGE", `字段${name}需要给定一个Int4Range`);
|
||||||
|
return v.toString();
|
||||||
|
}, v => Int4Range.create(v), false, option?.virtual ?? false, { type: "int4" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义numrange字段
|
||||||
|
* @param name 字段名称
|
||||||
|
* @param option 字段选项
|
||||||
|
*/
|
||||||
|
export function FloatRangeColumn(option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function FloatRangeColumn(name: string, option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function FloatRangeColumn(name?: any, option?: any) {
|
||||||
|
if (typeof name != "string") {
|
||||||
|
option = name
|
||||||
|
name = undefined
|
||||||
|
}
|
||||||
|
return Field("range", name, v => {
|
||||||
|
if (!(v instanceof FloatRange)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_NUMRANGE", `字段${name}需要给定一个NumRange`);
|
||||||
|
return v.toString();
|
||||||
|
}, v => FloatRange.create(v), false, option?.virtual ?? false, { type: "float" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义daterange字段
|
||||||
|
* @param name 字段名称
|
||||||
|
* @param option 字段选项
|
||||||
|
*/
|
||||||
|
export function DateRangeColumn(option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function DateRangeColumn(name: string, option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function DateRangeColumn(name?: any, option?: any) {
|
||||||
|
if (typeof name != "string") {
|
||||||
|
option = name
|
||||||
|
name = undefined
|
||||||
|
}
|
||||||
|
return Field("range", name, v => {
|
||||||
|
if (!(v instanceof DateRange)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_DATERANGE", `字段${name}需要给定一个DateRange`);
|
||||||
|
return v.toString();
|
||||||
|
}, v => DateRange.create(v), false, option?.virtual ?? false, { type: "date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义tsrange字段
|
||||||
|
* @param name 字段名称
|
||||||
|
* @param option 字段选项
|
||||||
|
*/
|
||||||
|
export function DateTimeRangeColumn(option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function DateTimeRangeColumn(name: string, option?: { virtual?: boolean }): Decorators.PropDecorator<BasicEntity>
|
||||||
|
export function DateTimeRangeColumn(name?: any, option?: any) {
|
||||||
|
if (typeof name != "string") {
|
||||||
|
option = name
|
||||||
|
name = undefined
|
||||||
|
}
|
||||||
|
return Field("range", name, v => {
|
||||||
|
if (!(v instanceof DateTimeRange)) throw new DatabaseError("DB_ENTITY_FIELD_NOT_DATETIMERANGE", `字段${name}需要给定一个DateTimeRange`);
|
||||||
|
return v.toString();
|
||||||
|
}, v => DateTimeRange.create(v), false, option?.virtual ?? false, { type: "datetime" });
|
||||||
|
}
|
||||||
|
|
||||||
//获取实体配置
|
//获取实体配置
|
||||||
export function getEntityConfig(clazz: any) {
|
export function getEntityConfig(clazz: any) {
|
||||||
const conf = getEntityConfigWithoutCheck(clazz);
|
const conf = getEntityConfigWithoutCheck(clazz);
|
||||||
|
@ -5,6 +5,7 @@ export {
|
|||||||
Field, IntColumn, FloatColumn, StringColumn, BooleanColumn, DateColumn, DateTimeColumn, JsonColumn,
|
Field, IntColumn, FloatColumn, StringColumn, BooleanColumn, DateColumn, DateTimeColumn, JsonColumn,
|
||||||
IntArrayColumn, StringArrayColumn,
|
IntArrayColumn, StringArrayColumn,
|
||||||
TSVectorColumn,
|
TSVectorColumn,
|
||||||
|
Int4RangeColumn, FloatRangeColumn, DateRangeColumn, DateTimeRangeColumn,
|
||||||
JoinOne, JoinMany,
|
JoinOne, JoinMany,
|
||||||
BasicEntity,
|
BasicEntity,
|
||||||
IEntityConfig,
|
IEntityConfig,
|
||||||
|
64
src/query.ts
64
src/query.ts
@ -2,6 +2,7 @@ import { QueryResult, QueryResultRow } from "pg";
|
|||||||
import { BasicEntity, IEntityFieldConfig, IEntityJoinConfig, getEntityConfig, getField, getJoin, getTableName } from "./entity";
|
import { BasicEntity, IEntityFieldConfig, IEntityJoinConfig, getEntityConfig, getField, getJoin, getTableName } from "./entity";
|
||||||
import type { JoinaFieldName, Class, ArrayTypeSelf, Values, StringArgs, SQLStringArgs } from "./types";
|
import type { JoinaFieldName, Class, ArrayTypeSelf, Values, StringArgs, SQLStringArgs } from "./types";
|
||||||
import { escapeID, escapeValue, formatSQL } from "./util";
|
import { escapeID, escapeValue, formatSQL } from "./util";
|
||||||
|
import { RangeBase } from "./type/range";
|
||||||
|
|
||||||
|
|
||||||
//编译器原因,不加此段代码会出错,具体原因未知,请保持定义和types.ts重的同名定义一致
|
//编译器原因,不加此段代码会出错,具体原因未知,请保持定义和types.ts重的同名定义一致
|
||||||
@ -26,13 +27,36 @@ interface IQueryFunc<R extends QueryResultRow = any> {
|
|||||||
type DataOrCustom<T> = T | (() => string);
|
type DataOrCustom<T> = T | (() => string);
|
||||||
|
|
||||||
|
|
||||||
|
/** 基础的where条件 */
|
||||||
|
type BasicWhere<F> = F extends string | number | boolean ?
|
||||||
|
| {
|
||||||
|
in?: F[]
|
||||||
|
nin?: F[]
|
||||||
|
ne?: F | null
|
||||||
|
gt?: F
|
||||||
|
gte?: F
|
||||||
|
lt?: F
|
||||||
|
lte?: F
|
||||||
|
} & (F extends string ? {
|
||||||
|
like?: string
|
||||||
|
nlike?: string
|
||||||
|
} : {}) : {};
|
||||||
|
|
||||||
type BasicWhere<V> = never
|
/** 范围操作where条件 */
|
||||||
| { in: V[] } | { nin: V[] }
|
type RangeWhere<F> = F extends RangeBase<infer R> ? {
|
||||||
| { ne: V | null } | { gt: V } | { gte: V } | { lt: V } | { lte: V }
|
/** 范围是否包含元素或另一个范围 `@>` */
|
||||||
| { like: string } | { nlike: string }
|
rangeContains?: R | F | null | undefined,
|
||||||
|
/** 当前范围是否被包含于另一个范围 `<@` */
|
||||||
|
rangeContained?: F | null | undefined,
|
||||||
|
/** 范围是否和另一个范围重叠 `&&` */
|
||||||
|
rangeOverlaps?: F | null | undefined,
|
||||||
|
} : {};
|
||||||
|
|
||||||
type ConditionOption<E extends BasicEntity> = { [P in BasicFieldName<E>]?: Exclude<E[P], null | undefined> | BasicWhere<Exclude<E[P], null | undefined>> | null }
|
type ConditionOption<E extends BasicEntity> = {
|
||||||
|
[P in BasicFieldName<E>]?:
|
||||||
|
Exclude<E[P], null | undefined> | null |
|
||||||
|
(BasicWhere<Exclude<E[P], null | undefined>> & RangeWhere<Exclude<E[P], null | undefined>>)
|
||||||
|
}
|
||||||
|
|
||||||
/** 搜索选项映射关系 */
|
/** 搜索选项映射关系 */
|
||||||
interface ISearchOptionMap {
|
interface ISearchOptionMap {
|
||||||
@ -370,7 +394,7 @@ function WithFilter<E extends BasicEntity, B extends Constructor<E>>(Base: B) {
|
|||||||
for (const col of columns) {
|
for (const col of columns) {
|
||||||
if (typeof col == "string") this.#columns.push([escapeID(this.alias, this.fieldc(col).name), escapeID(`${this.alias}__${col}`)]);
|
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)) {
|
else for (const [k, asName] of Object.entries(col)) {
|
||||||
this.#columns.push([escapeID(this.alias, this.fieldc(k as string).name), asName as string]);
|
this.#columns.push([escapeID(this.alias, this.fieldc(k as string).name), escapeID(asName)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
@ -455,6 +479,34 @@ function buildCondition<E extends BasicEntity>(builder: SQLBuilder<E>, option: C
|
|||||||
case "nlike":
|
case "nlike":
|
||||||
wheres.push(`${fieldName} note like ${escapeValue(opv)}`);
|
wheres.push(`${fieldName} note like ${escapeValue(opv)}`);
|
||||||
break;
|
break;
|
||||||
|
case "rangeContains": {
|
||||||
|
if (opv === null || opv === undefined) break;
|
||||||
|
if (opv instanceof RangeBase) wheres.push(`${fieldName} @> ${opv.toString()}`);
|
||||||
|
else if (field.typename === "range") {
|
||||||
|
switch (field.option?.type) {
|
||||||
|
case "int4":
|
||||||
|
case "float":
|
||||||
|
wheres.push(`${fieldName} @> ${opv.toString()}`);
|
||||||
|
break;
|
||||||
|
case "date":
|
||||||
|
wheres.push(`${fieldName} @> ${escapeValue(opv)}::date`);
|
||||||
|
break;
|
||||||
|
case "datetime":
|
||||||
|
wheres.push(`${fieldName} @> ${escapeValue(opv)}::timestamp`);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "rangeContained":
|
||||||
|
if (opv === null || opv === undefined) break;
|
||||||
|
if (opv instanceof RangeBase) wheres.push(`${fieldName} <@ ${opv.toString()}`);
|
||||||
|
break
|
||||||
|
case "rangeOverlaps":
|
||||||
|
if (opv === null || opv === undefined) break;
|
||||||
|
if (opv instanceof RangeBase) wheres.push(`${fieldName} && ${opv.toString()}`);
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from "./tsvector";
|
export * from "./tsvector";
|
||||||
|
export { Int4Range, FloatRange, DateRange, DateTimeRange } from "./range";
|
199
src/type/range.ts
Normal file
199
src/type/range.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
export type RangeBound = "[]" | "()" | "[)" | "(]"
|
||||||
|
|
||||||
|
|
||||||
|
function parseRange(rangeStr: string) {
|
||||||
|
const beg = rangeStr.at(0);
|
||||||
|
const end = rangeStr.at(-1);
|
||||||
|
if (beg !== "[" && beg !== "(") return null;
|
||||||
|
if (end !== "]" && end !== ")") return null;
|
||||||
|
const content = rangeStr.slice(1, - 1).trim();
|
||||||
|
if (!content.length) return null;
|
||||||
|
const items = content.split(",").map(s => s.trim());
|
||||||
|
if (items.length !== 2) return null;
|
||||||
|
return {
|
||||||
|
bound: (beg + end) as RangeBound,
|
||||||
|
begin: items[0],
|
||||||
|
end: items[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class RangeBase<T> {
|
||||||
|
#begin: T | null;
|
||||||
|
#end: T | null;
|
||||||
|
#bound: RangeBound;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建范围,系统会对范围值进行简单校验,如果校验失败则返回null
|
||||||
|
* @param begin 开始值
|
||||||
|
* @param end 结束值
|
||||||
|
* @param bound 边界信息,默认`[)`
|
||||||
|
*/
|
||||||
|
public static create<T extends RangeBase<any>>(this: new (...args: any) => T, begin: string, end: string, bound?: RangeBound): T | null;
|
||||||
|
/**
|
||||||
|
* 创建范围,系统会对范围值进行简单校验,如果校验失败则返回null
|
||||||
|
* @param value 范围值,例如`[1,2)`
|
||||||
|
*/
|
||||||
|
public static create<T extends RangeBase<any>>(this: new (...args: any) => T, value: string): T | null;
|
||||||
|
public static create<T extends RangeBase<any>>(this: new (...args: any) => T, beginOrValue: string, end?: string, bound?: RangeBound): T | null {
|
||||||
|
let range: T | null = null;
|
||||||
|
if (typeof end === "string") {
|
||||||
|
range = new this(beginOrValue, end, bound ?? "[)");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const info = parseRange(beginOrValue);
|
||||||
|
if (!info) return null;
|
||||||
|
range = new this(info.begin, info.end, info.bound);
|
||||||
|
}
|
||||||
|
if (!range.validate()) return null;
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(begin: T | null, end: T | null, bound: RangeBound) {
|
||||||
|
this.#begin = begin;
|
||||||
|
this.#end = end;
|
||||||
|
this.#bound = bound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开始值 */
|
||||||
|
public get begin() { return this.#begin; }
|
||||||
|
|
||||||
|
/** 结束值 */
|
||||||
|
public get end() { return this.#end; }
|
||||||
|
|
||||||
|
/** 边界信息 */
|
||||||
|
public get bound() { return this.#bound; }
|
||||||
|
|
||||||
|
/** 转换为SQL字符串 */
|
||||||
|
public toString() {
|
||||||
|
const boundBegin = this.bound.at(0);
|
||||||
|
const boundEnd = this.bound.at(-1);
|
||||||
|
return `'${boundBegin}${this.begin ?? ''},${this.end ?? ''}${boundEnd}'::${this.typename()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 范围是否包含值
|
||||||
|
* @param value 值
|
||||||
|
*/
|
||||||
|
public contains(value: T) {
|
||||||
|
if (this.begin !== null) {
|
||||||
|
if (this.bound.at(0) === "(") {
|
||||||
|
if (this.valueCompare(value, this.begin) <= 0) return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (this.valueCompare(value, this.begin) < 0) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.end !== null) {
|
||||||
|
if (this.bound.at(-1) === ")") {
|
||||||
|
if (this.valueCompare(value, this.end) >= 0) return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (this.valueCompare(value, this.end) > 0) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract validate(): boolean;
|
||||||
|
|
||||||
|
protected abstract typename(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 值比较, 如果value1大于value2,返回1,如果value1等于value2,返回0,如果value1小于value2,返回-1
|
||||||
|
* @param value1 值1
|
||||||
|
* @param value2 值2
|
||||||
|
*/
|
||||||
|
protected abstract valueCompare(value1: T, value2: T): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 32位整数范围
|
||||||
|
*/
|
||||||
|
export class Int4Range extends RangeBase<number> {
|
||||||
|
public constructor(begin: string | null, end: string | null, bound: RangeBound) {
|
||||||
|
super(
|
||||||
|
begin === null ? null : parseFloat(begin),
|
||||||
|
end === null ? null : parseFloat(end),
|
||||||
|
bound
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate() { return this.#checkInt(this.begin) && this.#checkInt(this.end); }
|
||||||
|
|
||||||
|
protected typename() { return "int4range"; }
|
||||||
|
|
||||||
|
protected valueCompare(value1: number, value2: number) {
|
||||||
|
return value1 - value2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkInt(value: number | null) {
|
||||||
|
if (value === null) return true;
|
||||||
|
return Number.isInteger(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小数范围
|
||||||
|
*/
|
||||||
|
export class FloatRange extends RangeBase<number> {
|
||||||
|
public constructor(begin: string | null, end: string | null, bound: RangeBound) {
|
||||||
|
super(
|
||||||
|
begin === null ? null : parseFloat(begin),
|
||||||
|
end === null ? null : parseFloat(end),
|
||||||
|
bound
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate() { return this.#checkFloat(this.begin) && this.#checkFloat(this.end); }
|
||||||
|
|
||||||
|
protected typename() { return "numrange"; }
|
||||||
|
|
||||||
|
protected valueCompare(value1: number, value2: number) {
|
||||||
|
return value1 - value2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkFloat(value: number | null) {
|
||||||
|
if (value === null) return true;
|
||||||
|
return Number.isFinite(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期范围
|
||||||
|
*/
|
||||||
|
export class DateRange extends RangeBase<string> {
|
||||||
|
|
||||||
|
protected validate() { return this.#checkDate(this.begin) && this.#checkDate(this.end); }
|
||||||
|
|
||||||
|
protected typename() { return "daterange"; }
|
||||||
|
|
||||||
|
protected valueCompare(value1: string, value2: string) {
|
||||||
|
return moment(value1).diff(value2, "days");
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkDate(date: string | null) {
|
||||||
|
if (date === null) return true;
|
||||||
|
return /^\d{4}-\d{2}-\d{2}$/.test(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期时间范围(也就是时间戳范围)
|
||||||
|
*/
|
||||||
|
export class DateTimeRange extends RangeBase<string> {
|
||||||
|
protected validate() { return this.#checkDatetime(this.begin) && this.#checkDatetime(this.end); }
|
||||||
|
|
||||||
|
protected typename() { return "tsrange"; }
|
||||||
|
|
||||||
|
protected valueCompare(value1: string, value2: string) {
|
||||||
|
return moment(value1).diff(value2, "seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkDatetime(datetime: string | null) {
|
||||||
|
if (datetime === null) return true;
|
||||||
|
return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(datetime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user