• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

statuscompliance / status-backend / 14617704584

23 Apr 2025 12:06PM UTC coverage: 50.781% (+6.7%) from 44.064%
14617704584

Pull #139

github

web-flow
Merge 156bdabe3 into 03aa4daf9
Pull Request #139: test(utils): added sqlQueryBuilder

385 of 828 branches covered (46.5%)

Branch coverage included in aggregate %.

139 of 152 new or added lines in 1 file covered. (91.45%)

1 existing line in 1 file now uncovered.

915 of 1732 relevant lines covered (52.83%)

5.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.01
/src/utils/sqlQueryBuilder.js
1
/**
2
 * Genera una consulta SQL dinámica basada en los parámetros proporcionados.
3
 *
4
 * @param {Object} params - Objeto con los parámetros para construir la consulta.
5
 * @param {Object} [params.select] - Objeto que define la parte SELECT de la consulta.
6
 * @param {Array<{func: string, attr: string}>} [params.select.aggregations=[]] - Lista de agregaciones, cada una con su función (COUNT, AVG, etc.) y el atributo correspondiente.
7
 * @param {Array<string>} [params.select.columns=[]] - Lista de columnas a seleccionar en la consulta.
8
 * @param {Object} [params.from] - Objeto que define la parte FROM de la consulta.
9
 * @param {string} [params.from.table='computation'] - Nombre de la tabla.
10
 * @param {Object} [params.where] - Objeto que define la parte WHERE de la consulta.
11
 * @param {Array<{key: string, operator: string, value: any}>} [params.where.conditions=[]] - Lista de condiciones para la cláusula WHERE.
12
 * @param {string} [params.where.logic='AND'] - Operador lógico que une las condiciones WHERE ('AND' o 'OR').
13
 * @param {string} [params.groupBy] - Columna por la que se agruparán los resultados.
14
 * @param {Object} [params.orderBy] - Objeto que define la parte ORDER BY de la consulta.
15
 * @param {string} [params.orderBy.attr] - Columna por la que se ordenarán los resultados.
16
 * @param {string} [params.orderBy.direction='ASC'] - Dirección de orden (ASC o DESC).
17
 *
18
 * @returns {string} - La consulta SQL generada.
19
 */
20
function createSQLQuery(params = {}) {
×
21
  const {
22
    select = { aggregations: [], columns: [] },
3✔
23
    from = { table: 'computation' },
4✔
24
    where = { conditions: [], logic: 'AND' },
3✔
25
    groupBy,
26
    orderBy = {}
3✔
27
  } = params;
5✔
28

29
  const selectClause = buildSelectClause(select.aggregations, select.columns);
5✔
30
  const fromClause = buildFromClause(from.table);
5✔
31
  const whereClause = buildWhereClause(where.conditions, where.logic);
5✔
32
  const groupByClause = buildGroupByClause(groupBy);
5✔
33
  const orderByClause = buildOrderByClause(orderBy.attr, orderBy.direction);
5✔
34

35
  return [
5✔
36
    selectClause,
37
    fromClause,
38
    whereClause,
39
    groupByClause,
40
    orderByClause
41
  ].filter(clause => clause !== '').join(' ');
25✔
42
}
43

44
function buildSelectClause(aggregations, columns) {
45
  let selectClause = 'SELECT ';
5✔
46
  const aggs = Array.isArray(aggregations) ? aggregations : [];
5!
47
  const cols = Array.isArray(columns) ? columns : [];
5!
48

49
  if (aggs.length === 0 && cols.length === 0) {
5✔
50
    selectClause += '*';
3✔
51
  } else {
52
    const selectParts = [];
2✔
53
    aggs.forEach(agg => {
2✔
54
      const attr = agg.func.toUpperCase() === 'COUNT' && agg.attr === '*' ? '*' : sanitizeIdentifier(agg.attr);
4✔
55
      selectParts.push(`${sanitizeIdentifier(agg.func)}(${attr})`);
4✔
56
    });
57
    cols.forEach(col => {
2✔
58
      selectParts.push(sanitizeIdentifier(col));
3✔
59
    });
60
    selectClause += selectParts.join(', ');
2✔
61
  }
62
  return selectClause;
5✔
63
}
64

65
function buildFromClause(table) {
66
  return `FROM statusdb.${sanitizeIdentifier(table)}`;
5✔
67
}
68

69
function buildWhereClause(conditions, logic) {
70
  if (conditions.length === 0) {
5✔
71
    return '';
3✔
72
  }
73
  const whereClauses = conditions.map(condition => {
2✔
74
    const value = sanitizeValue(condition.value);
6✔
75
    const operator = sanitizeOperator(condition.operator);
6✔
76
    const key = sanitizeIdentifier(condition.key);
6✔
77
    return `${key} ${operator} ${value}`;
6✔
78
  });
79
  const separator = ` ${logic} `;
2✔
80

81
  return `WHERE (${whereClauses.join(separator)})`;
2✔
82
}
83

84
function buildGroupByClause(groupBy) {
85
  if (!groupBy) {
5✔
86
    return '';
3✔
87
  }
88
  return `GROUP BY ${sanitizeIdentifier(groupBy)}`;
2✔
89
}
90

91
function buildOrderByClause(attr, direction = 'ASC') {
3✔
92
  if (!attr) {
5✔
93
    return '';
3✔
94
  }
95
  const orderDirection = direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
2✔
96
  return `ORDER BY ${sanitizeIdentifier(attr)} ${orderDirection}`;
2✔
97
}
98

99
function parseSelectClause(selectPart, result) {
100
  result.aggregations = [];
5✔
101
  result.columns = [];
5✔
102

103
  if (!selectPart || selectPart.trim() === '' || selectPart.trim() === '*') {
5✔
104
    return;
3✔
105
  }
106

107
  const selectFields = selectPart.split(',');
2✔
108

109
  selectFields.forEach(field => {
2✔
110
    field = field.trim();
7✔
111
    const aggMatch = field.match(/^(\w+)\(([^)]+)\)$/i);
7✔
112

113
    if (aggMatch && aggMatch[1] && aggMatch[2] !== undefined) {
7✔
114
      const funcName = aggMatch[1].toUpperCase();
4✔
115
      const attrName = aggMatch[2].trim();
4✔
116
      if (funcName === 'COUNT' && attrName === '*') {
4✔
117
        result.aggregations.push({ func: 'COUNT', attr: '*' });
1✔
118
      } else {
119
        result.aggregations.push({ func: funcName, attr: attrName });
3✔
120
      }
121
    } else if (field) {
3!
122
      result.columns.push(field);
3✔
123
    }
124
  });
125
}
126

127
function parseFromClause(fromPart, result) {
128
  result.table = 'computation';
5✔
129

130
  if (!fromPart || fromPart.trim() === '') {
5!
NEW
131
    console.warn('WARNING: FROM part empty, using default table.');
×
NEW
132
    return;
×
133
  }
134

135
  const fromPartTrimmed = fromPart.trim();
5✔
136

137
  const tableMatch = fromPartTrimmed.match(/^statusdb\.(\w+)$/i);
5✔
138
  if (tableMatch && tableMatch[1]) {
5!
139
    result.table = tableMatch[1];
5✔
140
  } else {
NEW
141
    console.warn(`WARNING: Unexpected FROM format: "${fromPartTrimmed}", using default table.`);
×
142
  }
143
}
144

145
function parseWhereClause(whereContentPart, result) {
146
  result.whereConditions = [];
3✔
147
  result.whereLogic = 'AND';
3✔
148

149
  if (!whereContentPart || whereContentPart.trim() === '') {
3!
NEW
150
    console.warn('WARNING: WHERE content empty.');
×
NEW
151
    return;
×
152
  }
153

154
  const conditionsString = whereContentPart.trim();
3✔
155

156
  const conditionPartsAndLogic = conditionsString.split(/\s+(AND|OR)\s+/i);
3✔
157

158
  const logicOperatorsFound = conditionPartsAndLogic.filter((_, index) => index % 2 !== 0);
13✔
159

160
  if (logicOperatorsFound.length > 0) result.whereLogic = logicOperatorsFound[0].toUpperCase();
3!
161

162

163
  const conditionStringsOnly = conditionPartsAndLogic.filter((_, index) => index % 2 === 0);
13✔
164

165
  conditionStringsOnly.forEach(condition => {
3✔
166
    const conditionTrimmed = condition.trim();
8✔
167
    if (!conditionTrimmed) return;
8!
168

169
    const parts = conditionTrimmed.split(/\s*(>=|<=|!=|=|>|<|LIKE)\s*/i);
8✔
170

171
    if (parts.length >= 2) {
8!
172
      const key = parts[0].trim();
8✔
173
      const operator = parts[1].trim();
8✔
174
      const valueParts = parts.slice(2);
8✔
175
      const valueString = valueParts.join(' ').trim();
8✔
176

177
      if (key && operator && valueString !== '') {
8!
178
        result.whereConditions.push({
8✔
179
          key: key,
180
          operator: operator.toUpperCase(),
181
          value: parseWhereValue(valueString),
182
        });
183
      } else {
NEW
184
        console.warn(`WARNING: Malformed condition part: "${conditionTrimmed}"`);
×
185
      }
186
    } else {
NEW
187
      console.warn(`WARNING: Malformed condition part (operator missing?): "${conditionTrimmed}"`);
×
188
    }
189
  });
190
}
191

192
function parseGroupByClause(groupByPart, result) {
193
  result.groupBy = null;
1✔
194

195
  if (!groupByPart || groupByPart.trim() === '') {
1!
NEW
196
    return;
×
197
  }
198

199
  const groupByPartTrimmed = groupByPart.trim();
1✔
200

201
  const columnMatch = groupByPartTrimmed.match(/^(\w+)$/);
1✔
202
  if (columnMatch && columnMatch[1]) {
1!
203
    result.groupBy = columnMatch[1];
1✔
204
  } else {
NEW
205
    console.warn(`WARNING: Unexpected GROUP BY format: "${groupByPartTrimmed}", expecting single identifier.`);
×
206
  }
207
}
208

209
function parseOrderByClause(orderByPart, result) {
210
  result.orderByAttr = null;
1✔
211
  result.orderDirection = null;
1✔
212

213
  if (!orderByPart || orderByPart.trim() === '') {
1!
NEW
214
    return;
×
215
  }
216

217
  const orderByPartTrimmed = orderByPart.trim();
1✔
218

219
  const orderByMatch = orderByPartTrimmed.match(/^([a-zA-Z0-9_.]+)\s*(ASC|DESC)?$/i); // Usar $ para asegurar que es el fin de la parte
1✔
220

221
  if (orderByMatch && orderByMatch[1]) {
1!
222
    result.orderByAttr = orderByMatch[1];
1✔
223
    result.orderDirection = orderByMatch[2]?.toUpperCase() || 'ASC';
1!
224
  } else {
NEW
UNCOV
225
    console.warn(`WARNING: Unexpected ORDER BY format: "${orderByPartTrimmed}", expecting identifier [ASC|DESC].`);
×
226
  }
227
}
228

229
function parseSQLQuery(query) {
230

231
  const result = {
5✔
232
    aggregations: [],
233
    columns: [],
234
    whereConditions: [],
235
    whereLogic: 'AND',
236
    groupBy: null,
237
    orderByAttr: null,
238
    orderDirection: null,
239
    table: 'computation',
240
  };
241

242
  if (!query || typeof query !== 'string') {
5!
NEW
243
    return result;
×
244
  }
245

246
  const upperQuery = query.toUpperCase();
5✔
247

248
  const keywords = ['SELECT ', ' FROM ', ' WHERE ', ' GROUP BY ', ' ORDER BY '];
5✔
249
  const indices = {};
5✔
250
  let currentPos = 0;
5✔
251

252
  for (const keyword of keywords) {
5✔
253
    const keywordUpper = keyword.toUpperCase();
19✔
254
    const index = upperQuery.indexOf(keywordUpper, currentPos);
19✔
255

256
    if (index !== -1) {
19✔
257
      indices[keyword.trim()] = index;
15✔
258
      currentPos = index + keyword.length;
15✔
259
    } else {
260
      console.warn(`WARNING: Keyword "${keyword.trim()}" not found after position ${currentPos}`);
4✔
261
      break;
4✔
262
    }
263
  }
264

265
  if (indices['SELECT'] === 0) {
5!
266
    const endSelect = indices['FROM'] !== undefined ? indices['FROM'] : query.length;
5!
267
    const selectPart = query.substring(indices['SELECT'] + keywords[0].length, endSelect).trim();
5✔
268
    parseSelectClause(selectPart, result);
5✔
269
  }
270

271
  if (indices['FROM'] !== undefined) {
5!
272
    const endFrom = indices['WHERE'] !== undefined ? indices['WHERE'] : indices['GROUP BY'] !== undefined ? indices['GROUP BY'] : indices['ORDER BY'] !== undefined ? indices['ORDER BY'] : query.length;
5!
273
    const fromPart = query.substring(indices['FROM'] + keywords[1].length, endFrom).trim();
5✔
274
    parseFromClause(fromPart, result);
5✔
275
  }
276

277
  if (indices['WHERE'] !== undefined) {
5✔
278
    const endWhere = indices['GROUP BY'] !== undefined ? indices['GROUP BY'] : indices['ORDER BY'] !== undefined ? indices['ORDER BY'] : query.length;
3!
279
    const wherePartWithParens = query.substring(indices['WHERE'] + keywords[2].length, endWhere).trim();
3✔
280

281
    const whereContentMatch = wherePartWithParens.match(/^\s*\((.+?)\)$/);
3✔
282
    if (whereContentMatch && whereContentMatch[1]) {
3!
283
      const whereContent = whereContentMatch[1].trim();
3✔
284
      parseWhereClause(whereContent, result);
3✔
285
    } else {
NEW
286
      console.warn(`WARNING: WHERE clause format unexpected (missing or malformed parentheses): "${wherePartWithParens}"`);
×
287
    }
288
  }
289

290
  if (indices['GROUP BY'] !== undefined) {
5✔
291
    const endGroupBy = indices['ORDER BY'] !== undefined ? indices['ORDER BY'] : query.length;
1!
292
    const groupByPart = query.substring(indices['GROUP BY'] + keywords[3].length, endGroupBy).trim();
1✔
293
    parseGroupByClause(groupByPart, result);
1✔
294
  }
295

296
  if (indices['ORDER BY'] !== undefined) {
5✔
297
    const orderByPart = query.substring(indices['ORDER BY'] + keywords[4].length).trim(); // Hasta el final
1✔
298
    parseOrderByClause(orderByPart, result);
1✔
299
  }
300

301
  return result;
5✔
302
}
303

304
function parseWhereValue(value) {
305
  if (value === 'true' || value === 'false') {
8✔
306
    return value === 'true';
1✔
307
  }
308
  if (!isNaN(value)) {
7✔
309
    return Number(value);
2✔
310
  }
311
  const quotedMatch = value.match(/^'(.*)'$/);
5✔
312
  return quotedMatch ? quotedMatch[1] : value;
5!
313
}
314

315
//Helpers
316
function sanitizeIdentifier(identifier) {
317
  return identifier.replace(/[^a-zA-Z0-9_]/g, '');
24✔
318
}
319

320
function sanitizeValue(value) {
321
  if (typeof value === 'number' || typeof value === 'boolean') {
6✔
322
    return String(value);
3✔
323
  }
324
  return String(value).replace(/['"]/g, '');
3✔
325
}
326

327
function sanitizeOperator(operator) {
328
  const validOperators = ['=', '>', '<', '>=', '<=', '!=', 'LIKE'];
6✔
329
  return validOperators.includes(operator.toUpperCase()) ? operator : '=';
6!
330
}
331

332
export { createSQLQuery, parseSQLQuery };
333

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc