@jakub.knejzlik/ts-query
Version:
TypeScript implementation of SQL builder
411 lines (366 loc) • 11.6 kB
text/typescript
import { Dayjs } from "dayjs";
import { ExpressionBase, ExpressionValue } from "./Expression";
import { ISQLFlavor } from "./Flavor";
import { Q } from "./Query";
import { ISequelizable, ISerializable } from "./interfaces";
type ConditionValue = ExpressionValue | Dayjs;
export class Condition implements ISequelizable, ISerializable {
toSQL(flavor: ISQLFlavor): string {
throw new Error("Method not implemented.");
}
toJSON(): any {
throw new Error("Method not implemented.");
}
serialize(): string {
return JSON.stringify(this.toJSON());
}
static fromJSON(json: any): Condition {
switch (json.type) {
case "BinaryCondition":
return BinaryCondition.fromJSON(json);
case "LogicalCondition":
return LogicalCondition.fromJSON(json);
case "BetweenCondition":
return BetweenCondition.fromJSON(json);
case "InCondition":
return InCondition.fromJSON(json);
case "NotInCondition":
return NotInCondition.fromJSON(json);
case "NullCondition":
return NullCondition.fromJSON(json);
case "LikeCondition":
return LikeCondition.fromJSON(json);
case "ColumnComparisonCondition":
return ColumnComparisonCondition.fromJSON(json);
default:
throw new Error(
`Unknown condition type: ${json.type} (${JSON.stringify(json)})`
);
}
}
static deserialize(value: Condition | string): Condition | null {
if (typeof value === "string") {
try {
return Condition.fromJSON(JSON.parse(value));
} catch {
return null;
}
}
return value;
}
}
type Operator = "=" | "!=" | ">" | "<" | ">=" | "<=";
class BinaryCondition extends Condition {
key: ExpressionBase;
value: ExpressionBase;
operator: Operator;
constructor(key: ConditionValue, value: ConditionValue, operator: Operator) {
super();
this.key = Q.expr(key);
this.value = Q.value(value);
this.operator = operator;
}
toSQL(flavor: ISQLFlavor): string {
return `${this.key.toSQL(flavor)} ${this.operator} ${this.value.toSQL(
flavor
)}`;
}
// serialization
toJSON(): any {
return {
type: "BinaryCondition",
key: this.key.serialize(),
value: this.value.serialize(),
operator: this.operator,
};
}
static fromJSON(json: any): BinaryCondition {
return new BinaryCondition(
ExpressionBase.deserialize(json.key),
ExpressionBase.deserializeValue(json.value),
json.operator
);
}
}
class LogicalCondition extends Condition {
conditions: Condition[];
operator: "AND" | "OR";
constructor(conditions: Condition[], operator: "AND" | "OR") {
super();
this.conditions = conditions;
this.operator = operator;
}
toSQL(flavor: ISQLFlavor): string {
return `(${this.conditions
.map((c) => c.toSQL(flavor))
.join(` ${this.operator} `)})`;
}
// serialization
toJSON(): any {
return {
type: "LogicalCondition",
conditions: this.conditions.map((condition) => condition.toJSON()),
operator: this.operator,
};
}
static fromJSON(json: any): LogicalCondition {
const conditions = json.conditions.map(Condition.fromJSON);
return new LogicalCondition(conditions, json.operator);
}
}
class BetweenCondition extends Condition {
key: ExpressionBase;
from: ExpressionBase;
to: ExpressionBase;
constructor(key: ConditionValue, from: ConditionValue, to: ConditionValue) {
super();
this.key = Q.expr(key);
this.from = Q.expr(from);
this.to = Q.expr(to);
}
toSQL(flavor: ISQLFlavor): string {
return `${this.key.toSQL(flavor)} BETWEEN ${this.from.toSQL(
flavor
)} AND ${this.to.toSQL(flavor)}`;
}
// serialization
toJSON(): any {
return {
type: "BetweenCondition",
key: this.key.serialize(),
from: this.from.serialize(),
to: this.to.serialize(),
};
}
static fromJSON(json: any): BetweenCondition {
return new BetweenCondition(
ExpressionBase.deserialize(json.key),
ExpressionBase.deserializeValue(json.from),
ExpressionBase.deserializeValue(json.to)
);
}
}
class InCondition extends Condition {
key: ExpressionBase;
values: ExpressionBase[];
constructor(key: ConditionValue, values: ConditionValue[]) {
super();
this.key = Q.expr(key);
this.values = values.map((v) => Q.exprValue(v));
}
toSQL(flavor: ISQLFlavor): string {
return `${this.key.toSQL(flavor)} IN (${this.values
.map((v) => v.toSQL(flavor))
.join(", ")})`;
}
// serialization
toJSON(): any {
return {
type: "InCondition",
key: this.key.serialize(),
values: this.values.map((v) => v.serialize()),
};
}
static fromJSON(json: any): InCondition {
return new InCondition(
ExpressionBase.deserialize(json.key),
json.values.map(ExpressionBase.deserializeValue)
);
}
}
class NotInCondition extends Condition {
key: ExpressionBase;
values: ExpressionBase[];
constructor(key: ConditionValue, values: ConditionValue[]) {
super();
this.key = Q.expr(key);
this.values = values.map((v) => Q.expr(v));
}
toSQL(flavor: ISQLFlavor): string {
return `${this.key.toSQL(flavor)} NOT IN (${this.values
.map((v) => v.toSQL(flavor))
.join(", ")})`;
}
// serialization
toJSON(): any {
return {
type: "NotInCondition",
key: this.key.serialize(),
values: this.values.map((v) => v.serialize()),
};
}
static fromJSON(json: any): NotInCondition {
return new NotInCondition(
ExpressionBase.deserialize(json.key),
json.values.map(ExpressionBase.deserializeValue)
);
}
}
class NullCondition extends Condition {
key: ExpressionBase;
isNull: boolean;
constructor(key: ConditionValue, isNull: boolean) {
super();
this.key = Q.expr(key);
this.isNull = isNull;
}
toSQL(flavor: ISQLFlavor): string {
return `${this.key.toSQL(flavor)} IS ${this.isNull ? "" : "NOT "}NULL`;
}
// serialization
toJSON(): any {
return {
type: "NullCondition",
key: this.key.serialize(),
isNull: this.isNull,
};
}
static fromJSON(json: any): NullCondition {
return new NullCondition(ExpressionBase.deserialize(json.key), json.isNull);
}
}
class LikeCondition extends Condition {
key: ExpressionBase;
pattern: string;
isLike: boolean;
constructor(key: ConditionValue, pattern: string, isLike: boolean) {
super();
this.key = Q.expr(key);
this.pattern = pattern;
this.isLike = isLike;
}
toSQL(flavor: ISQLFlavor): string {
return `${this.key.toSQL(flavor)} ${this.isLike ? "" : "NOT "}LIKE \'${
this.pattern
}\'`;
}
// serialization
toJSON(): any {
return {
type: "LikeCondition",
key: this.key.serialize(),
pattern: this.pattern,
isLike: this.isLike,
};
}
static fromJSON(json: any): LikeCondition {
return new LikeCondition(
ExpressionBase.deserialize(json.key),
json.pattern,
json.isLike
);
}
}
class ColumnComparisonCondition extends Condition {
leftKey: ExpressionBase;
rightKey: ExpressionBase;
operator: Operator;
constructor(
leftKey: ConditionValue,
rightKey: ConditionValue,
operator: Operator
) {
super();
this.leftKey = Q.expr(leftKey);
this.rightKey = Q.expr(rightKey);
this.operator = operator;
}
toSQL(flavor: ISQLFlavor): string {
return `${this.leftKey.toSQL(flavor)} ${
this.operator
} ${this.rightKey.toSQL(flavor)}`;
}
// serialization
toJSON(): any {
return {
type: "ColumnComparisonCondition",
leftKey: this.leftKey.serialize(),
rightKey: this.rightKey.serialize(),
operator: this.operator,
};
}
static fromJSON(json: any): ColumnComparisonCondition {
return new ColumnComparisonCondition(
ExpressionBase.deserialize(json.leftKey),
ExpressionBase.deserialize(json.rightKey),
json.operator
);
}
}
export const Conditions = {
fromString: (column: string, value: string | number): Condition => {
const str = `${value}`;
if (str.indexOf(">=") === 0) {
return Conditions.greaterThanOrEqual(column, str.substring(2));
}
if (str.indexOf("<=") === 0) {
return Conditions.lessThanOrEqual(column, str.substring(2));
}
if (str.indexOf(">") === 0) {
return Conditions.greaterThan(column, str.substring(1));
}
if (str.indexOf("<") === 0) {
return Conditions.lessThan(column, str.substring(1));
}
if (str.indexOf("~") === 0) {
return Conditions.or([
Conditions.like(column, `${str.substring(1)}%`),
Conditions.like(column, `% ${str.substring(1)}%`),
]);
}
if (str.indexOf("!~") === 0) {
return Conditions.and([
Conditions.notLike(column, `${str.substring(2)}%`),
Conditions.notLike(column, `% ${str.substring(2)}%`),
]);
}
if (str.indexOf("!") === 0) {
return Conditions.notEqual(column, `${str.substring(1)}%`);
}
return Conditions.equal(column, str);
},
equal: (key: ConditionValue, value: ConditionValue) =>
new BinaryCondition(key, value, "="),
notEqual: (key: ConditionValue, value: ConditionValue) =>
new BinaryCondition(key, value, "!="),
greaterThan: (key: ConditionValue, value: ConditionValue) =>
new BinaryCondition(key, value, ">"),
lessThan: (key: ConditionValue, value: ConditionValue) =>
new BinaryCondition(key, value, "<"),
greaterThanOrEqual: (key: ConditionValue, value: ConditionValue) =>
new BinaryCondition(key, value, ">="),
lessThanOrEqual: (key: ConditionValue, value: ConditionValue) =>
new BinaryCondition(key, value, "<="),
between: (key: ConditionValue, values: [ConditionValue, ConditionValue]) =>
new BetweenCondition(key, Q.exprValue(values[0]), Q.exprValue(values[1])),
in: (key: string, values: ConditionValue[] | null) =>
values && values.length > 0 ? new InCondition(key, values) : null,
notIn: (key: ConditionValue, values: ConditionValue[]) =>
new NotInCondition(key, values),
and: (conditions: (Condition | null)[] | null) => {
const _c = (conditions || []).filter((c) => c !== null);
return _c.length > 0 ? new LogicalCondition(_c, "AND") : null;
},
or: (conditions: (Condition | null)[] | null) => {
const _c = (conditions || []).filter((c) => c !== null);
return _c.length > 0 ? new LogicalCondition(_c, "OR") : null;
},
null: (key: string) => new NullCondition(key, true),
notNull: (key: string) => new NullCondition(key, false),
like: (key: string, pattern: string) => new LikeCondition(key, pattern, true),
notLike: (key: string, pattern: string) =>
new LikeCondition(key, pattern, false),
columnEqual: (leftKey: string, rightKey: string) =>
new ColumnComparisonCondition(leftKey, rightKey, "="),
columnNotEqual: (leftKey: string, rightKey: string) =>
new ColumnComparisonCondition(leftKey, rightKey, "!="),
columnGreaterThan: (leftKey: string, rightKey: string) =>
new ColumnComparisonCondition(leftKey, rightKey, ">"),
columnLessThan: (leftKey: string, rightKey: string) =>
new ColumnComparisonCondition(leftKey, rightKey, "<"),
columnGreaterThanOrEqual: (leftKey: string, rightKey: string) =>
new ColumnComparisonCondition(leftKey, rightKey, ">="),
columnLessThanOrEqual: (leftKey: string, rightKey: string) =>
new ColumnComparisonCondition(leftKey, rightKey, "<="),
};
export { Conditions as Cond };