miniorm
Version:
A lightweight, minimal TypeScript ORM & query‑builder for MySQL, PostgreSQL and SQLite
458 lines (457 loc) • 20.6 kB
JavaScript
;
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;