UNPKG

@jakub.knejzlik/ts-query

Version:

TypeScript implementation of SQL builder

314 lines (278 loc) 8.33 kB
import { Condition } from "./Condition"; import { Expression } from "./Expression"; import { ISQLFlavor } from "./Flavor"; import { Q, SelectQuery, Table } from "./Query"; import { MySQLFlavor } from "./flavors/mysql"; import { IMetadata, ISequelizable, ISequelizableOptions, ISerializable, MetadataOperationType, OperationType, } from "./interfaces"; type RowRecord = Record<string, Expression>; type RowRecordInput = Record<string, Expression | any>; export class MutationBase { protected _table: Table; constructor(table: string, alias?: string) { this._table = new Table(table, alias); } public getTableNames(): string[] { return [this._table.getTableName()]; } public clone(): this { const clone = new (this.constructor as any)(); clone._table = this._table.clone(); return clone; } static deserialize(json: string) { const parsed = JSON.parse(json); switch (parsed.type) { case OperationType.DELETE: return DeleteMutation.fromJSON(parsed); case OperationType.INSERT: return InsertMutation.fromJSON(parsed); case OperationType.UPDATE: return UpdateMutation.fromJSON(parsed); default: throw new Error("Unknown mutation type"); } } } export class DeleteMutation extends MutationBase implements ISerializable, ISequelizable, IMetadata { protected _where: Condition[] = []; public getOperationType(): MetadataOperationType { return MetadataOperationType.DELETE; } public clone(): this { const clone = super.clone(); clone._where = [...this._where]; return clone; } where(condition: Condition): this { const clone = this.clone(); clone._where.push(condition); return clone; } toSQL( flavor: ISQLFlavor = new MySQLFlavor(), options?: ISequelizableOptions, transformed = false ): string { if (options?.transformDeleteMutation && !transformed) { return options.transformDeleteMutation(this).toSQL(flavor, options, true); } let sql = `DELETE FROM ${this._table.toSQL(flavor)}`; if (this._where.length) { sql += ` WHERE ${this._where .map((condition) => condition.toSQL(flavor)) .join(" AND ")}`; } return sql; } serialize(): string { return JSON.stringify(this.toJSON()); } toJSON() { return { type: OperationType.DELETE, table: this._table.toJSON(), where: this._where.map((condition) => condition.toJSON()), }; } static fromJSON({ table, where }: any): DeleteMutation { const deleteMutation = new DeleteMutation(table.source, table.alias); deleteMutation._where = where.map((condition: any) => Condition.fromJSON(condition) ); return deleteMutation; } } export class InsertMutation extends MutationBase implements ISerializable, ISequelizable, IMetadata { protected _values?: RowRecord[]; protected _selectWithColumns: [SelectQuery, string[] | undefined]; public getOperationType(): MetadataOperationType { return MetadataOperationType.INSERT; } public clone(): this { const clone = super.clone(); clone._values = this._values && [...this._values]; return clone; } removeAllValues(): this { const clone = this.clone(); clone._values = undefined; return clone; } allValues(): RowRecord[] { return this._values; } values(values: RowRecordInput[]): this { const clone = this.clone(); if (clone._selectWithColumns) throw new Error("select already set"); clone._values = [...(clone._values ?? []), ...values]; return clone; } select(query: SelectQuery, columns?: string[]): this { const clone = this.clone(); if (clone._values) throw new Error("values already set"); clone._selectWithColumns = [query, columns]; return clone; } toSQL( flavor: ISQLFlavor = new MySQLFlavor(), options?: ISequelizableOptions, transformed = false ): string { if (options?.transformInsertMutation && !transformed) { return options.transformInsertMutation(this).toSQL(flavor, options, true); } if (this._values) { return `INSERT INTO ${this._table.toSQL(flavor)} (${Object.keys( this._values[0] ) .map((k) => flavor.escapeColumn(k)) .join(", ")}) VALUES ${this._values .map( (value) => `(${Object.values(value) .map((v) => flavor.escapeValue(v)) .join(", ")})` ) .join(", ")}`; } if (this._selectWithColumns) { const [query, columns] = this._selectWithColumns; return `INSERT INTO ${this._table.toSQL(flavor)}${ columns ? ` (${columns.map((k) => flavor.escapeColumn(k)).join(", ")})` : "" } ${query.toSQL(flavor)}`; } throw new Error("values or select must be set for insert query"); } serialize(): string { return JSON.stringify(this.toJSON()); } toJSON() { return { type: OperationType.INSERT, table: this._table.toJSON(), values: this._values, select: this._selectWithColumns && [ this._selectWithColumns[0].toJSON(), this._selectWithColumns[1], ], }; } static fromJSON({ table, values, select }: any): InsertMutation { const insertMutation = new InsertMutation(table.source, table.alias); insertMutation._values = values; if (select) { insertMutation._selectWithColumns = [ SelectQuery.fromJSON(select[0]), select[1], ]; } return insertMutation; } } export class UpdateMutation extends MutationBase implements ISerializable, ISequelizable, IMetadata { protected _values: RowRecord = {}; protected _where: Condition[] = []; public getOperationType(): MetadataOperationType { return MetadataOperationType.UPDATE; } public clone(): this { const clone = super.clone(); clone._values = { ...this._values }; clone._where = [...this._where]; return clone; } set(values: RowRecordInput): this { const clone = this.clone(); const _values = {}; for (const [key, value] of Object.entries(values)) { if (value === null) { _values[key] = Q.null(); continue; } if (value instanceof SelectQuery) { _values[key] = value; continue; } _values[key] = value instanceof Expression ? value : Q.exprValue(value); } clone._values = { ...clone._values, ..._values }; return clone; } where(condition: Condition): this { const clone = this.clone(); clone._where.push(condition); return clone; } toSQL( flavor: ISQLFlavor = new MySQLFlavor(), options?: ISequelizableOptions, transformed = false ): string { if (options?.transformUpdateMutation && !transformed) { return options.transformUpdateMutation(this).toSQL(flavor, options, true); } if (!this._values) throw new Error("No values to update"); let sql = `UPDATE ${this._table.toSQL(flavor)} SET ${Object.entries( this._values ) .map(([key, value]) => { return `${flavor.escapeColumn(key)} = ${ value instanceof SelectQuery ? `(${value.toSQL(flavor)})` : value.toSQL(flavor) }`; }) .join(", ")}`; if (this._where.length) { sql += ` WHERE ${this._where .map((condition) => condition.toSQL(flavor)) .join(" AND ")}`; } return sql; } serialize(): string { return JSON.stringify(this.toJSON()); } toJSON() { const _values = {}; for (const [key, value] of Object.entries(this._values)) { _values[key] = value.serialize(); } return { type: OperationType.UPDATE, table: this._table.toJSON(), values: _values, where: this._where.map((condition) => condition.toJSON()), }; } static fromJSON({ table, values, where }: any): UpdateMutation { const updateMutation = new UpdateMutation(table.source, table.alias); const _values = {}; for (const [key, value] of Object.entries(values)) { _values[key] = Q.deserializeRaw(value as string); } updateMutation._values = _values; updateMutation._where = where.map((condition: any) => Condition.fromJSON(condition) ); return updateMutation; } }