adaptus2-orm
Version:
A multi-database ORM supporting MySQL, PostgreSQL, MongoDB, and Snowflake
495 lines (429 loc) • 14 kB
JavaScript
// src/core/QueryBuilder.js
class QueryBuilder {
constructor() {
this.reset();
}
reset() {
this.queryType = null;
this.tableName = null;
this.selectFields = ['*'];
this.whereConditions = [];
this.joinClauses = [];
this.groupByFields = [];
this.havingConditions = [];
this.orderByFields = [];
this.limitValue = null;
this.offsetValue = null;
this.insertData = null;
this.updateData = null;
this.params = [];
this.databaseType = null;
return this;
}
database(type) {
this.databaseType = type.toLowerCase();
return this;
}
table(name) {
this.tableName = name;
return this;
}
select(fields = '*') {
this.queryType = 'SELECT';
if (Array.isArray(fields)) {
this.selectFields = fields;
} else if (typeof fields === 'string') {
this.selectFields = fields === '*' ? ['*'] : [fields];
}
return this;
}
insert(data) {
this.queryType = 'INSERT';
this.insertData = data;
return this;
}
update(data) {
this.queryType = 'UPDATE';
this.updateData = data;
return this;
}
delete() {
this.queryType = 'DELETE';
return this;
}
where(field, operator = '=', value = null) {
// Support for object syntax: .where({ field: value })
if (typeof field === 'object' && field !== null) {
Object.entries(field).forEach(([key, val]) => {
this.whereConditions.push({
field: key,
operator: '=',
value: val,
type: 'AND'
});
this.params.push(val);
});
} else {
// Handle different argument patterns
if (arguments.length === 2 && operator !== '=' && operator !== '!=' && operator !== '>' && operator !== '<' && operator !== '>=' && operator !== '<=') {
// Two arguments: field and value (default operator is =)
value = operator;
operator = '=';
}
this.whereConditions.push({
field: field,
operator: operator,
value: value,
type: 'AND'
});
this.params.push(value);
}
return this;
}
orWhere(field, operator = '=', value = null) {
if (arguments.length === 2) {
value = operator;
operator = '=';
}
this.whereConditions.push({
field: field,
operator: operator,
value: value,
type: 'OR'
});
this.params.push(value);
return this;
}
whereIn(field, values) {
this.whereConditions.push({
field: field,
operator: 'IN',
value: values,
type: 'AND'
});
this.params.push(...values);
return this;
}
whereNotIn(field, values) {
this.whereConditions.push({
field: field,
operator: 'NOT IN',
value: values,
type: 'AND'
});
this.params.push(...values);
return this;
}
whereLike(field, pattern) {
this.whereConditions.push({
field: field,
operator: 'LIKE',
value: pattern,
type: 'AND'
});
this.params.push(pattern);
return this;
}
whereNull(field) {
this.whereConditions.push({
field: field,
operator: 'IS NULL',
value: null,
type: 'AND'
});
return this;
}
whereNotNull(field) {
this.whereConditions.push({
field: field,
operator: 'IS NOT NULL',
value: null,
type: 'AND'
});
return this;
}
join(table, firstField, operator = '=', secondField = null) {
if (arguments.length === 3) {
secondField = operator;
operator = '=';
}
this.joinClauses.push({
type: 'INNER',
table: table,
firstField: firstField,
operator: operator,
secondField: secondField
});
return this;
}
leftJoin(table, firstField, operator = '=', secondField = null) {
if (arguments.length === 3) {
secondField = operator;
operator = '=';
}
this.joinClauses.push({
type: 'LEFT',
table: table,
firstField: firstField,
operator: operator,
secondField: secondField
});
return this;
}
rightJoin(table, firstField, operator = '=', secondField = null) {
if (arguments.length === 3) {
secondField = operator;
operator = '=';
}
this.joinClauses.push({
type: 'RIGHT',
table: table,
firstField: firstField,
operator: operator,
secondField: secondField
});
return this;
}
groupBy(fields) {
if (Array.isArray(fields)) {
this.groupByFields = fields;
} else {
this.groupByFields = [fields];
}
return this;
}
having(field, operator = '=', value = null) {
if (arguments.length === 2) {
value = operator;
operator = '=';
}
this.havingConditions.push({
field: field,
operator: operator,
value: value,
type: 'AND'
});
this.params.push(value);
return this;
}
orderBy(field, direction = 'ASC') {
this.orderByFields.push({
field: field,
direction: direction.toUpperCase()
});
return this;
}
limit(value) {
this.limitValue = parseInt(value);
return this;
}
offset(value) {
this.offsetValue = parseInt(value);
return this;
}
toSQL() {
if (!this.databaseType) {
throw new Error('Database type must be specified using .database() method');
}
switch (this.queryType) {
case 'SELECT':
return this._buildSelectQuery();
case 'INSERT':
return this._buildInsertQuery();
case 'UPDATE':
return this._buildUpdateQuery();
case 'DELETE':
return this._buildDeleteQuery();
default:
throw new Error('Query type must be specified');
}
}
_buildSelectQuery() {
let query = `SELECT ${this.selectFields.join(', ')} FROM ${this.tableName}`;
// Add JOINs
this.joinClauses.forEach(join => {
query += ` ${join.type} JOIN ${join.table} ON ${join.firstField} ${join.operator} ${join.secondField}`;
});
// Add WHERE clauses
if (this.whereConditions.length > 0) {
query += ' WHERE ';
const whereString = this._buildWhereClause();
query += whereString;
}
// Add GROUP BY
if (this.groupByFields.length > 0) {
query += ` GROUP BY ${this.groupByFields.join(', ')}`;
}
// Add HAVING
if (this.havingConditions.length > 0) {
query += ' HAVING ';
const havingString = this._buildHavingClause();
query += havingString;
}
// Add ORDER BY
if (this.orderByFields.length > 0) {
const orderString = this.orderByFields.map(item =>
`${item.field} ${item.direction}`
).join(', ');
query += ` ORDER BY ${orderString}`;
}
// Add LIMIT and OFFSET
if (this.limitValue !== null) {
query += ` LIMIT ${this.limitValue}`;
if (this.offsetValue !== null) {
query += ` OFFSET ${this.offsetValue}`;
}
}
return {
sql: query,
params: this.params
};
}
_buildInsertQuery() {
if (!this.insertData) {
throw new Error('Insert data must be specified');
}
const fields = Object.keys(this.insertData);
const values = Object.values(this.insertData);
let placeholders;
switch (this.databaseType) {
case 'mysql':
placeholders = fields.map(() => '?').join(', ');
break;
case 'postgresql':
placeholders = fields.map((_, index) => `$${index + 1}`).join(', ');
break;
case 'snowflake':
placeholders = fields.map((_, index) => `:${index + 1}`).join(', ');
break;
default:
placeholders = fields.map(() => '?').join(', ');
}
const query = `INSERT INTO ${this.tableName} (${fields.join(', ')}) VALUES (${placeholders})`;
return {
sql: query,
params: values
};
}
_buildUpdateQuery() {
if (!this.updateData) {
throw new Error('Update data must be specified');
}
const fields = Object.keys(this.updateData);
const values = Object.values(this.updateData);
let setClause;
switch (this.databaseType) {
case 'mysql':
setClause = fields.map(field => `${field} = ?`).join(', ');
break;
case 'postgresql':
setClause = fields.map((field, index) => `${field} = $${index + 1}`).join(', ');
break;
case 'snowflake':
setClause = fields.map((field, index) => `${field} = :${index + 1}`).join(', ');
break;
default:
setClause = fields.map(field => `${field} = ?`).join(', ');
}
let query = `UPDATE ${this.tableName} SET ${setClause}`;
// Add WHERE clauses
if (this.whereConditions.length > 0) {
query += ' WHERE ';
const whereString = this._buildWhereClause();
query += whereString;
}
return {
sql: query,
params: [...values, ...this.params]
};
}
_buildDeleteQuery() {
let query = `DELETE FROM ${this.tableName}`;
// Add WHERE clauses
if (this.whereConditions.length > 0) {
query += ' WHERE ';
const whereString = this._buildWhereClause();
query += whereString;
}
return {
sql: query,
params: this.params
};
}
_buildWhereClause() {
let whereString = '';
let paramCounter = 0;
this.whereConditions.forEach((condition, index) => {
if (index > 0) {
whereString += ` ${condition.type} `;
}
if (condition.operator === 'IN' || condition.operator === 'NOT IN') {
const placeholderCount = condition.value.length;
let placeholders;
switch (this.databaseType) {
case 'mysql':
placeholders = Array(placeholderCount).fill('?').join(', ');
break;
case 'postgresql':
placeholders = Array(placeholderCount).fill().map((_, i) => `$${paramCounter + i + 1}`).join(', ');
break;
case 'snowflake':
placeholders = Array(placeholderCount).fill().map((_, i) => `:${paramCounter + i + 1}`).join(', ');
break;
default:
placeholders = Array(placeholderCount).fill('?').join(', ');
}
whereString += `${condition.field} ${condition.operator} (${placeholders})`;
paramCounter += placeholderCount;
} else if (condition.operator === 'IS NULL' || condition.operator === 'IS NOT NULL') {
whereString += `${condition.field} ${condition.operator}`;
} else {
let placeholder;
switch (this.databaseType) {
case 'mysql':
placeholder = '?';
break;
case 'postgresql':
placeholder = `$${paramCounter + 1}`;
break;
case 'snowflake':
placeholder = `:${paramCounter + 1}`;
break;
default:
placeholder = '?';
}
whereString += `${condition.field} ${condition.operator} ${placeholder}`;
paramCounter++;
}
});
return whereString;
}
_buildHavingClause() {
let havingString = '';
this.havingConditions.forEach((condition, index) => {
if (index > 0) {
havingString += ` ${condition.type} `;
}
havingString += `${condition.field} ${condition.operator} ${condition.value}`;
});
return havingString;
}
// Convenience methods for common queries
static create(databaseType) {
return new QueryBuilder().database(databaseType);
}
get() {
return this.toSQL();
}
first() {
return this.limit(1).toSQL();
}
count(field = '*') {
this.selectFields = [`COUNT(${field}) as count`];
return this.toSQL();
}
exists() {
return this.limit(1).count().toSQL();
}
}
module.exports = QueryBuilder;