UNPKG

@firstfleet/ffmsnodesqlv8

Version:
578 lines (489 loc) 19.5 kB
import {SqlClient, Error, Meta, EventColumnCb, PreparedStatement, Query, Connection} from "msnodesqlv8"; /** * Created by Stephen on 1/22/2017. */ export module MsNodeSqlWrapperModule { const legacyDriver: SqlClient = require('msnodesqlv8'); export class SqlModuleWrapperError implements Error { constructor(public message: string) { } sqlstate: string = ""; code: number = 0; } export interface queryCb<T> { (v: T): void } export enum SqlCommandType { None, QueryObjectFormat, QueryRawFormat, StoredProcedure, PreparingStatement, PreparedStatement } export class SqlCommand { constructor(public connection: SqlConnection, public id ?: string, public commandType: SqlCommandType = SqlCommandType.QueryObjectFormat) { } _driverTimeoutMs: number | undefined; _wrapperTimeoutMs: number | undefined; _sql: string | undefined; _procedure: string | undefined; _inputParams: any[] | undefined; _onMeta: queryCb<Meta> | undefined; _onColumn: EventColumnCb | undefined; _onRowCount: queryCb<number> | undefined; _onRow: queryCb<number> | undefined; _onDone: queryCb<any> | undefined; _onSubmitted: queryCb<any> | undefined; _onError: queryCb<string> | undefined; _onClosed: queryCb<string> | undefined; _query: Query | undefined; _preparedStatement: PreparedStatement | undefined; public isPrepared(): boolean { return this.commandType == SqlCommandType.PreparingStatement; } public sql(s: string): SqlCommand { this._sql = s; if (this.commandType == SqlCommandType.None) this.commandType = SqlCommandType.QueryObjectFormat; return this; } public params(v: any[]): SqlCommand { this._inputParams = v; return this; } public param(v: any): SqlCommand { if (this._inputParams == null) { this._inputParams = []; } this._inputParams.push(v); return this; } public procedure(s: string): SqlCommand { this._procedure = s; this.commandType = SqlCommandType.StoredProcedure; this.unsubscribe(); return this; } public rawFormat(): SqlCommand { this.commandType = SqlCommandType.QueryRawFormat; return this; } public wrapperTimeoutMs(to: number): SqlCommand { this._wrapperTimeoutMs = to; return this; } public driverTimeoutMs(to: number): SqlCommand { this._driverTimeoutMs = to; return this; } public onMeta(cb: queryCb<Meta>): SqlCommand { this._onMeta = cb; return this; } public onColumn(cb: EventColumnCb): SqlCommand { this._onColumn = cb; return this; } public onRowCount(cb: queryCb<number>): SqlCommand { this._onRowCount = cb; return this; } public onRow(cb: queryCb<number>): SqlCommand { this._onRow = cb; return this; } public onDone(cb: queryCb<any>): SqlCommand { this._onDone = cb; return this; } public onSubmitted(cb: queryCb<any>): SqlCommand { this._onSubmitted = cb; return this; } public onError(cb: queryCb<string>): SqlCommand { this._onError = cb; return this; } public onClosed(cb: queryCb<string>): SqlCommand { this._onClosed = cb; return this; } public unsubscribe(): void { this._onMeta = undefined; this._onColumn = undefined; this._onRowCount = undefined; this._onRow = undefined; this._onDone = undefined; this._onError = undefined; this._onClosed = undefined; } private subscribe(): void { let query = this._query; if (query) { if (this._onMeta != null) { query.on('meta', (m:any) => this._onMeta(m)); } if (this._onColumn != null) { query.on('column', (c:any, d:any, m:any) => this._onColumn(c, d, m)); } if (this._onRowCount != null) { query.on('rowCount', (m:any) => this._onRowCount(m)); } if (this._onRow != null) { query.on('row', (m:any) => this._onRow(m)); } if (this._onDone != null) { query.on('done', m => this._onDone(m)); } if (this._onError != null) { query.on('error', m => this._onError(m)); } if (this._onClosed != null) { query.on('closed', m => this._onClosed(m)); } if (this._onSubmitted != null) { query.on('submitted', m => this._onSubmitted(m)); } } } public subscribing(): boolean { return this._onMeta != null || this._onSubmitted != null || this._onColumn != null || this._onRowCount != null || this._onRow != null || this._onDone != null || this._onError != null || this._onClosed != null } private execProcedure(resolve: Function, reject: Function, res: SqlCommandResponse): void { let timeout = this._driverTimeoutMs > 0 ? this._driverTimeoutMs / 1000 : 0; let pm = this.connection.legacy_conn.procedureMgr(); pm.setTimeout(timeout); pm.callproc(this._procedure, this._inputParams, (err?: Error, rows?: any[], outputParams?: any[]) => { if (err) { res.error = err; reject(res); } else { res.aggregate(rows); res.outputParams = outputParams; resolve(res); } }); } private execQuery(resolve: Function, reject: Function, res: SqlCommandResponse): void { let timeout = this._driverTimeoutMs > 0 ? this._driverTimeoutMs / 1000 : 0; this._query = this.connection.legacy_conn.query({ query_str: this._sql, query_timeout: timeout }, this._inputParams, (err: Error, rows: any[], more: boolean) => { if (err) { res.error = err; reject(res); } else { res.aggregate(rows); if (!more) resolve(res); } }); } private execQueryRaw(resolve: Function, reject: Function, res: SqlCommandResponse): void { let timeout = this._driverTimeoutMs > 0 ? this._driverTimeoutMs / 1000 : 0; this._query = this.connection.legacy_conn.queryRaw({ query_str: this._sql, query_timeout: timeout }, this._inputParams, (err?: Error, rawData?: RawData, more?: boolean) => { if (err) { res.error = err; reject(res); } else { res.aggregateRaw(rawData); if (!more) resolve(res); } }); } private execPrepared(resolve: Function, reject: Function, res: SqlCommandResponse): void { this._preparedStatement.preparedQuery( this._inputParams, (err: Error, rows: any[], more: boolean) => { if (err) { res.error = err; reject(res); } else { res.aggregate(rows); if (!more) resolve(res); } }); } public cancel(): Promise<SqlCommand> { return new Promise((resolve, reject) => { let inst = this; if (!this._query) { reject(new SqlModuleWrapperError('can only cancel a submitted query.')); return; } this._query.cancelQuery(err => { if (!err) reject(new SqlModuleWrapperError(`failed to cancel: ${err.message}`)); resolve(inst); }) }); } public freePrepared(): Promise<SqlCommand> { return new Promise((resolve, reject) => { let inst = this; if (this.commandType != SqlCommandType.PreparedStatement) { reject(new SqlModuleWrapperError('freePrepared must be called on prepared command.')); return; } this._preparedStatement.free(() => { inst._preparedStatement = null; inst.commandType = SqlCommandType.None; resolve(inst); }) }); } public prepare(): Promise<SqlCommand> { return new Promise((resolve, reject) => { if (this._sql == null) { reject(new SqlModuleWrapperError('prepare must be called after sql() with sql to prepare.')); return; } if (this.commandType == SqlCommandType.PreparingStatement) { reject(new SqlModuleWrapperError('prepare is preparing and must be called once only on a command.')); return; } if (this._preparedStatement != null) { reject(new SqlModuleWrapperError('this command has previously been prepared.')); return; } this.commandType = SqlCommandType.PreparingStatement; this.unsubscribe(); let inst = this; this.connection.legacy_conn.prepare(this._sql, (err?: Error, statement?: PreparedStatement) => { if (err) { reject(err); inst.commandType = SqlCommandType.None; } else { inst._preparedStatement = statement; inst.commandType = SqlCommandType.PreparedStatement; resolve(inst); } }) }); } private dispatchCommandType(resolve: Function, reject: Function, res: SqlCommandResponse): void { switch (this.commandType) { case SqlCommandType.QueryObjectFormat: { this.execQuery(resolve, reject, res); break; } case SqlCommandType.QueryRawFormat: { this.execQueryRaw(resolve, reject, res); break; } case SqlCommandType.StoredProcedure: { this.execProcedure(resolve, reject, res); break; } case SqlCommandType.PreparingStatement: { res.error = new SqlModuleWrapperError(`statement not yet prepared.`); break; } case SqlCommandType.PreparedStatement: { this.execPrepared(resolve, reject, res); break; } default: { res.error = new SqlModuleWrapperError(`${this.commandType} is not valid value.`); break; } } } public execute(): Promise<SqlCommandResponse> { return new Promise((resolve, reject) => { let res = new SqlCommandResponse(); let to = this._wrapperTimeoutMs; if (to > 0) { setTimeout(() => { res.error = new SqlModuleWrapperError(`wrapper timeout ${to} expired.`); reject(res); }, to); } this.dispatchCommandType(resolve, reject, res); if (res.error != null) { reject(res); } else if (this.subscribing()) { this.subscribe() } }) } } export class RawData implements RawData { public meta: Meta[] = []; public rows: Array<any[]> []; } export class SqlCommandResponse { public aggregateRaw(raw: RawData) { let rd = this.rawData; if (rd == null) { this.rawData = rd = new RawData(); rd.meta = raw.meta; rd.rows = []; } raw.rows.forEach(row => rd.rows.push(row)); } public aggregate(rows: any[]) { if (this.asObjects == null) { this.asObjects = []; } rows.forEach(r => this.asObjects.push(r)); } public error: Error | undefined; public asObjects: any[] = []; public outputParams: any[] | undefined; public rawData: RawData | undefined; } export interface dictIteratorCb<T> { (key: string, val: T): void } export class Dictionary<T> { container: {[id: string]: T;} = {}; public count(): number { let keys = Object.keys(this.container); return keys.length; } public values(): T[] { let va: T[] = []; let keys = Object.keys(this.container); keys.forEach(k => va.push(this.container[k])); return va; } public keys(): string[] { return Object.keys(this.container); } public containsKey(key: string): boolean { return this.container[key] != null; } public add(key: string, v: T): void { if (this.containsKey(key)) throw new Error(`duplicate key ${key}`); this.container[key] = v; } public remove(key: string): void { delete this.container[key]; } public get(key: string): T { return this.container[key]; } public forEach(cb: dictIteratorCb<T>) { Object.keys(this.container).forEach((k: string) => cb(k, this.container[k])); } } export class ConnectionPool { connections: Dictionary<Connection> = new Dictionary<Connection>(); } export class CommandCache { public CachedCommands: Dictionary<SqlCommand> = new Dictionary<SqlCommand>(); constructor(public connection: SqlConnection) { } public get(id?: string): SqlCommand { if (id == null) return new SqlCommand(this.connection); let cached = this.CachedCommands.get(id); if (cached == null) { cached = new SqlCommand(this.connection); this.CachedCommands.add(id, cached); } return cached; } public free(commandId?: string): Promise<string> { return new Promise((resolve, reject) => { let c: SqlCommand = this.CachedCommands.get(commandId); if (c == null) { reject(false); return; } if (c.isPrepared()) { c.freePrepared().then(() => { this.CachedCommands.remove(commandId); resolve(commandId); }); } else { this.CachedCommands.remove(commandId); resolve(commandId); } }); } public deleteAll(): Promise<boolean> { return new Promise((resolve, reject) => { this.CachedCommands.forEach((id, c) => { this.free(id).then(()=> { if (this.CachedCommands.count() == 0) { resolve(true); } }).catch(e => { reject(e); }) }); }); } } export class SqlConnection { public CommandCache: CommandCache; constructor(public legacy_conn: Connection) { this.CommandCache = new CommandCache(this); } public id(): string { return this.legacy_conn.id.toString(); } public getCommand() : SqlCommand { return new SqlCommand(this); } public static getLegacy(): SqlClient { return legacyDriver; } public close(): Promise<any> { return new Promise((resolve, reject) => { this.legacy_conn.close((err: Error) => { if (err) reject(err); else resolve(null); }) }); } } export class Sql { constructor(public connStr: string) { } public execute(sql:string, params:any = [], raw:boolean = false) : Promise<SqlCommandResponse> { return new Promise((resolve, reject) => { this.open().then( (connection : SqlConnection) => { let command = new SqlCommand(connection); if (raw) command = command.rawFormat(); command.sql(sql).params(params).execute().then(res=> { connection.close().then(() => { resolve(res); }).catch(e=>{ reject(e); }); }).catch(e=> { reject(e); }) }).catch(e=> { reject(e); }); }); } public open(timeout: number = 0): Promise<SqlConnection> { return new Promise((resolve, reject) => { legacyDriver.open({ conn_str: this.connStr, conn_timeout: timeout }, (err: Error, legacy: any) => { if (err) { reject(err); } else { let connection = new SqlConnection(legacy); resolve(connection); } }); }); } } }