UNPKG

miniorm

Version:

A lightweight, minimal TypeScript ORM & query‑builder for MySQL, PostgreSQL and SQLite

458 lines (457 loc) 20.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryBuilder = exports.BaseEntityClass = void 0; const chalk_1 = require("chalk"); const core_1 = require("../core"); const DeclarativeTableBuilder_1 = require("./DeclarativeTableBuilder"); class BaseEntityClass { } exports.BaseEntityClass = BaseEntityClass; _a = BaseEntityClass; BaseEntityClass.$table = _a.name; class QueryBuilder { constructor(dbName, table, opt) { var _b; this.sql = ''; this.extra = ''; this.data = []; this.is_typeorm = false; this.where_values = []; this.isDebug = false; this.debugFunc = undefined; this.dbName = dbName; if (typeof table == 'string') this.tableName = table; else { this.tableName = (_b = table.$table) !== null && _b !== void 0 ? _b : table.name.toLowerCase(); this.entity = table; } this.dbType = core_1.MiniOrm.dbs[dbName].config.type; this.is_typeorm = this.dbType.startsWith('typeorm_'); // this.dbs = MiniOrm.dbs[dbName].db // if (this.is_typeorm) this.dbs = this.dbs.createQueryRunner().manager // else this.Model = MiniOrm if (opt && opt.clone == true) { if (opt.sql) this.sql = opt.sql; if (opt.data) this.data = opt.data; if (opt.entity) this.entity = opt.entity; if (opt.extra) this.extra = opt.extra; } } //! SELECT *|'id','name' FROM users select(fields = []) { const fields_ = fields.length > 0 ? fields.join(',') : '*'; if (this.sql == '') { this.sql = `SELECT * FROM ${this.tableName}`; if (fields.length > 0) this.fields(fields); } else if (this.sql.startsWith('SELECT')) this.sql = this.sql.replace(/SELECT .* FROM/, `SELECT ${fields_} FROM`); else if (/^ WHERE /.test(this.sql)) this.sql = `SELECT ${fields_} FROM ${this.tableName}`.trim() + this.sql; else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); return this; } fields(fields) { if (this.isIgnoreField(fields)) { if (fields.length > 0 && this.sql.startsWith('SELECT *')) this.sql = this.sql.replace('SELECT *', `SELECT ${fields.join(',')}`); else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); } } order(orderby, type) { if (this.isIgnoreField(orderby)) { this.extra = this.extra.trimRight(); if (/SELECT .* FROM/.test(this.sql)) { if (orderby && orderby.length > 0) { if (!this.extra.startsWith('ORDER BY')) this.extra = `ORDER BY ${orderby.join(',')} ${type !== null && type !== void 0 ? type : ''} ${this.extra} `; else if (this.extra.includes('ORDER BY')) this.extra = this.extra.replace(/\s*ORDER BY\s+[^ ]+(\s+(ASC|DESC))?/i, `ORDER BY ${orderby.join(',')} ${type !== null && type !== void 0 ? type : ''}`); } } else if (this.sql == '') { this.select(); this.order(orderby, type); } else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); } return this; } limit(limit) { if (this.isIgnoreField([limit])) { let offset = ''; this.extra = this.extra.trimRight(); if (this.sql.startsWith('SELECT')) { if (this.extra.includes('OFFSET ') && !this.extra.includes('LIMIT ')) { offset = this.extra.split('OFFSET')[1]; this.extra = this.extra.replace(/\s*OFFSET \d+\s*/, `LIMIT ${limit} OFFSET ${offset.trim()}`); } else if (this.extra.includes('OFFSET ') && this.extra.includes('LIMIT ')) { offset = this.extra.split('OFFSET')[1]; this.extra = this.extra.replace(/\s*LIMIT \d+\s*/g, '').trim(); this.extra = this.extra.replace(/\s*OFFSET \d+\s*/, ` LIMIT ${limit} OFFSET ${offset.trim()}`); } else if (!this.extra.includes('LIMIT ')) this.extra += ` LIMIT ${limit}`; else if (this.extra.includes('LIMIT ')) this.extra = this.extra.replace(/LIMIT \d+\s*/, `LIMIT ${limit}`); } else if (this.sql == '') { this.select(); this.limit(limit); } else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); } return this; } offset(offset) { if (this.isIgnoreField([offset])) { this.extra = this.extra.trimRight(); if (this.sql.startsWith('SELECT') /* && this.extra.includes('LIMIT') */) { if (!this.extra.includes('OFFSET ')) this.extra += ` OFFSET ${offset}`; else if (this.extra.includes('OFFSET ')) this.extra = this.extra.replace(/\s*OFFSET \d+\s*/, ` OFFSET ${offset}`); } else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); } return this; } first() { return __awaiter(this, void 0, void 0, function* () { if (this.sql.startsWith('SELECT')) { this.limit(1); return (yield this.run())[0]; } else if (this.sql == '') { this.select(); this.limit(1); return (yield this.run())[0]; } else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); }); } end() { return __awaiter(this, void 0, void 0, function* () { if (this.sql.startsWith('SELECT')) return (yield this.run()).slice(-1)[0]; else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); }); } selectMethods(method, fields) { return __awaiter(this, void 0, void 0, function* () { if (this.isIgnoreField(Array.isArray(fields) ? fields : [fields])) { let [sql] = this.toSQL(); const newFields = fields ? fields : '*'; if (sql == '') { this.select(fields ? Array.isArray(fields) ? fields : [fields] : undefined); let [sql] = this.toSQL(); this.sql = sql.replace('SELECT', `SELECT ${method.toUpperCase()}(`).replace('FROM', ') FROM'); } else if (/(WHERE|ORDER|LIMIT|DELETE)/.test(sql)) sql = `SELECT ${method.toUpperCase()}(${newFields}) FROM (${sql.replace(/;/g, '').replace(/SELECT .* FROM/, `SELECT ${newFields} FROM`)})${this.dbType == 'postgresql' ? '' : ` as ${method.toLowerCase()}`}`; else if (sql.startsWith('SELECT')) sql = sql.replace(/SELECT .* FROM/, `SELECT ${method.toUpperCase()}(${fields != undefined ? fields : '*'}) FROM`); else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(sql)}`); sql != '' ? this.sql = sql : undefined; const res = yield this.run(); return Object.values((res !== null && res !== void 0 ? res : {})[0])[0]; } return NaN; }); } count(fields) { return __awaiter(this, void 0, void 0, function* () { return +(yield this.selectMethods('count', fields)); }); } max(field) { return __awaiter(this, void 0, void 0, function* () { return +(yield this.selectMethods('max', field)); }); } min(field) { return __awaiter(this, void 0, void 0, function* () { return +(yield this.selectMethods('min', field)); }); } avg(field) { return __awaiter(this, void 0, void 0, function* () { return +(yield this.selectMethods('avg', field)); }); } sum(field) { return __awaiter(this, void 0, void 0, function* () { return +(yield this.selectMethods('sum', field)); }); } //! UPDATE users SET id=1 OR name='ahmet' update(set_values) { if (this.sql.length == 0) this.sql = `UPDATE ${this.tableName}`; else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); this.set(set_values); return this; } set(set_values) { if (this.sql.startsWith(`UPDATE ${this.tableName}`)) { this.sql += ' SET ' + Object.keys(set_values).map((k) => `${k} = ?`).join(','); Object.values(set_values).forEach(v => this.isIgnoreField([v]) ? this.data.push(v) : undefined); } else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); } delete() { if (this.sql === '') this.sql = `DELETE FROM ${this.tableName}`; else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); return this; } where(...where_values) { where_values = where_values.flatMap(w => Array.isArray(w) ? w : [w]); if (this.sql == '') this.select(); if (Object.keys(where_values).length === 0) { this.where_values = []; if (this.sql.includes(' WHERE ')) this.sql = this.sql.replace(/WHERE .* END_WHERE /, `END_WHERE`); } else if (where_values.some(w => typeof w == 'object' && Object.keys(where_values).length > 0)) { this.where_values = []; const sql = (where_values === null || where_values === void 0 ? void 0 : where_values.map((w, i) => { if (typeof w === 'object' && Object.keys(w).length > 0) { const where_operator = w['_OR'] ? 'OR' : 'AND'; delete w[`_${where_operator}`]; return Object.entries(w).map(([k, v]) => { var _b; let isNullOperator = undefined; if (String(v).endsWith('NULL') && String(v).startsWith('IS')) isNullOperator = v; if (this.isIgnoreField(Array.isArray(v) ? v : [v])) { const regex = /(<=|>=|!=|<>|=|<|>| IS NOT | IS | LIKE | NOT LIKE | IN )/, operator = (_b = k.match(/(<=|>=|!=|<>|=|<|>| IS NOT | IS | LIKE | NOT LIKE | IN )/i)) === null || _b === void 0 ? void 0 : _b[0]; isNullOperator != undefined ? undefined : Array.isArray(v) ? v.forEach(k => this.where_values.push(k)) : this.where_values.push(String(v).trim()); if (this.isIgnoreField([k.split(operator)[0].trim()])) return `${operator == ' LIKE ' && this.dbType == 'postgresql' ? `CAST(${k.replace(regex, '').trim()} AS TEXT)` : k.replace(regex, '').trim()} ${!isNullOperator ? operator == ' IN ' ? `IN( ${'?, '.repeat(Array.isArray(v) ? v.length : 0).slice(0, -2)} )` : operator ? operator.trim() + ' ?' : '= ?' : isNullOperator}`; else throw new Error(((0, chalk_1.yellow) `'${(0, chalk_1.red)(k)}' there is a risk in it`)); } else { } }).join(` ${where_operator} `) + `${typeof where_values[i + 1] === 'string' || where_values[i + 1] == undefined ? '' : ' OR'}`; } else if (typeof w == 'string') { if (w.toLowerCase() == 'and' || w.toLowerCase() == 'or') return w.toUpperCase(); else return 'OR'; } }).join(' ')) + ' END_WHERE '; if (sql.length > 1) { if (this.sql.includes(' WHERE ')) this.sql = this.sql.replace(/WHERE .* END_WHERE /, `WHERE ${sql}`); else this.sql += ` WHERE ${sql}`; } } /* else throw new Error(`SQL Syntax Error: ${red(this.sql)}`) */ return this; } //! INSERT INTO table_name (column names) VALUES(values) insert(into_values) { return __awaiter(this, void 0, void 0, function* () { if (Object.keys(into_values).length === 0) this.sql = `INSERT INTO ${this.tableName} DEFAULT VALUES`; else { this.sql = `INSERT INTO ${this.tableName}`; this.values(into_values); } try { yield this.run(); return true; } catch (err) { return false; } }); } values(into_values) { if (this.sql.startsWith(`INSERT INTO ${this.tableName}`)) { this.sql += `( ${Object.keys(into_values).map((k) => this.isIgnoreField([k]) ? `${k}` : undefined).join(',')} ) VALUES( ${('?,'.repeat(Object.keys(into_values).length)).slice(0, -1)} )`; Object.values(into_values).forEach(v => this.isIgnoreField([v]) ? this.data.push(v) : undefined); } else throw new Error('SQL Syntax Error'); } isIgnoreValue(value) { const s = String(value); const blacklist = [ /--/, /;\s*$/, /\/\*/, /\*\//, /['"]/, /\b0x[0-9A-F]+\b/i, /\b(UNION|SELECT|INSERT|UPDATE|DELETE|DROP|EXEC|ALTER|CREATE)\b/i, /\/\*.*\*\//, /[`´]/, /\\x[0-9A-F]{2}/i, /\\u[0-9A-F]{4}/i, /[\u00A0\u2000-\u200F\u202F\u3000]/, /[\x00-\x08\x0B\x0C\x0E-\x1F]/, /(?<!\w)\d{1,2}\s*=\s*\d{1,2}(?!\w)/, /\b(eval|Function|constructor|window|document|globalThis)\b/i, ]; for (const re of blacklist) { if (re.test(s)) { console.log((0, chalk_1.red)(`SQL injection risk detected in: ${s}`)); return false; } } return true; } isIgnoreField(fields) { for (let field of fields) if (!this.isIgnoreValue(field) || !/^[a-zA-Z0-9_]+$/.test(field)) throw new Error(`'${(0, chalk_1.red)(field)}' SQL Injection Risk.`); return true; } debug(type = 'now', func) { if (type == 'now') { const [sql, data] = this.toSQL(); if (func == undefined) { console.log((0, chalk_1.yellow)('[DEBUG SQL]'), (0, chalk_1.blue)(sql)); data.length > 0 ? console.log((0, chalk_1.yellow)('[DEBUG DATA]'), `[${(0, chalk_1.white)(data.join(', '))}]`) : undefined; } else func(sql, data); } else { this.isDebug = true; if (func != undefined) this.debugFunc = func; } return this; } exists() { return __awaiter(this, void 0, void 0, function* () { if (this.sql.startsWith('SELECT')) { this.sql = this.sql.replace(/SELECT .* FROM/, 'SELECT 1 FROM'); this.limit(1); const res = yield this.run(); return Array.isArray(res) && res.length > 0; } else throw new Error('This function only uses select queries'); }); } clone(opt = {}) { var _b, _c; const tableName = typeof opt.table == 'string' ? opt.table : (_b = opt.table.name) !== null && _b !== void 0 ? _b : opt.table.$table; return new QueryBuilder((_c = opt.db) !== null && _c !== void 0 ? _c : this.dbName, opt.table, { clone: true, sql: this.sql.replace(this.tableName, tableName), data: this.data, entity: this.entity, extra: this.extra }); } raw(query_1) { return __awaiter(this, arguments, void 0, function* (query, values = []) { query = query.replace('<table>', this.tableName); return yield core_1.MiniOrm.runQuery(this.dbName, query, values); }); } toSQL() { this.extra = this.extra.trimRight(); this.extra = this.extra.includes('OFFSET') && !this.extra.includes('LIMIT') ? this.extra.replace(/OFFSET \d+/, '') : this.extra; const data = this.data.concat(this.where_values); let idx = 0, sql = (this.sql.trim() + `${this.sql.startsWith('SELECT') ? `${this.extra.length == 0 || /(MIN|MAX|SUM|AVG|COUNT)/.test(this.sql) ? '' : this.extra.trimRight()}` : ''}`).trim(); if (this.dbType.endsWith('postgresql')) sql = sql.replace(/\?/g, () => `$${++idx}`); sql = sql.replace(/\s+END_WHERE/g, ' '); return [sql, data]; } pluck(field) { return __awaiter(this, void 0, void 0, function* () { if (this.sql.startsWith('SELECT')) this.sql = this.sql.replace(/SELECT .* FROM/, `SELECT ${field} FROM`); else if (this.sql == '') this.select([field]); else if (this.sql.startsWith(' WHERE')) this.sql = `SELECT ${field} FROM ${this.tableName}${this.sql}`; else throw new Error(`SQL Syntax Error: ${(0, chalk_1.red)(this.sql)}`); return (yield this.run()).map((v) => v[field]); }); } between(field, start, end) { return __awaiter(this, void 0, void 0, function* () { if (this.isIgnoreField([field])) { this.sql = ``; this.select(); this.sql += ` WHERE ${field} BETWEEN ? AND ?`; [start, end].forEach(n => this.isIgnoreField([n]) ? this.data.push(n) : undefined); return yield this.run(); } else throw new Error('SQL Injection Risk'); }); } disconnect() { return __awaiter(this, void 0, void 0, function* () { yield core_1.MiniOrm.disconnect(this.dbName); }); } reconnect() { return __awaiter(this, void 0, void 0, function* () { yield core_1.MiniOrm.reconnect(this.dbName); return this; }); } run() { return __awaiter(this, void 0, void 0, function* () { let res = undefined; const table_exists = yield DeclarativeTableBuilder_1.DeclarativeTableBuilder.checkTableExists(core_1.MiniOrm.dbs[this.dbName].db, this.dbType, this.tableName); if (table_exists) { const [sql, data] = this.toSQL(); this.sql = sql; this.data = data; if (this.isDebug) { if (this.debugFunc != undefined) this.debugFunc(sql, data); else { console.log((0, chalk_1.yellow)('[FINAL DEBUG SQL]'), (0, chalk_1.blue)(this.sql)); data.length > 0 ? console.log((0, chalk_1.yellow)('[FINAL DEBUG DATA]'), `[${(0, chalk_1.white)(this.data.join(', '))}]`) : undefined; } } res = yield core_1.MiniOrm.runQuery(this.dbName, this.sql, this.data); this.sql = ''; this.extra = ''; this.data = []; this.where_values = []; } else console.log(`${(0, chalk_1.red)(this.tableName)} cannot exists in the ${(0, chalk_1.yellow)(this.dbName)}.`); return res; }); } } exports.QueryBuilder = QueryBuilder;