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

statuscompliance / status-backend / 14621848543

23 Apr 2025 03:18PM UTC coverage: 51.212% (+7.1%) from 44.064%
14621848543

Pull #139

github

web-flow
Merge 4d453dad8 into a5c98d299
Pull Request #139: test(utils): added sqlQueryBuilder

395 of 844 branches covered (46.8%)

Branch coverage included in aggregate %.

159 of 172 new or added lines in 1 file covered. (92.44%)

66 existing lines in 1 file now uncovered.

936 of 1755 relevant lines covered (53.33%)

6.06 hits per line

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

86.25
/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'; // Default logic
3✔
148

149
  if (!whereContentPart || whereContentPart.trim() === '') {
3!
NEW
150
    return;
×
151
  }
152

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

155
  const tokens = conditionsString.split(/\s+/);
3✔
156

157
  const currentConditionTokens = [];
3✔
158
  const conditionsAndLogicTokens = []; 
3✔
159

160
  for (const token of tokens) {
3✔
161
    const upperToken = token.toUpperCase();
30✔
162
    if (upperToken === 'AND' || upperToken === 'OR') {
30✔
163
      if (currentConditionTokens.length > 0) {
5!
164
        conditionsAndLogicTokens.push(currentConditionTokens.join(' '));
5✔
165
        currentConditionTokens.length = 0;
5✔
166
      }
167
      conditionsAndLogicTokens.push(upperToken);
5✔
168
    } else if (token) {
25!
169
      currentConditionTokens.push(token);
25✔
170
    }
171
  }
172
  if (currentConditionTokens.length > 0) {
3!
173
    conditionsAndLogicTokens.push(currentConditionTokens.join(' '));
3✔
174
  }
175

176
  if (conditionsAndLogicTokens.length === 0) {
3!
NEW
177
    return;
×
178
  }
179

180
  const firstLogicToken = conditionsAndLogicTokens.find((_, index) => index % 2 !== 0);
6✔
181
  if (firstLogicToken) {
3!
182
    result.whereLogic = firstLogicToken;
3✔
183
  }
184

185
  conditionsAndLogicTokens.forEach((part, index) => {
3✔
186
    if (index % 2 === 0) {
13✔
187
      const conditionString = part;
8✔
188

189
      const conditionTrimmed = conditionString.trim();
8✔
190

191
      if (!conditionTrimmed) return; // Skip empty strings
8!
192

193
      const operatorRegex = /\s*(>=|<=|!=|=|>|<|LIKE)\s*/i;
8✔
194

195
      const operatorMatch = operatorRegex.exec(conditionTrimmed);
8✔
196

197
      if (operatorMatch) {
8!
198

199
        const fullMatch = operatorMatch[0];
8✔
200
        const operator = operatorMatch[1];
8✔
201
        const matchIndex = operatorMatch.index;
8✔
202

203
        const key = conditionTrimmed.substring(0, matchIndex).trim();
8✔
204
        const valueString = conditionTrimmed.substring(matchIndex + fullMatch.length).trim();
8✔
205

206
        const rawOperatorFound = operator.toUpperCase();
8✔
207
        const isValidOperator = ['>=', '<=', '!=', '=', '>', '<', 'LIKE'].includes(rawOperatorFound);
8✔
208

209

210
        if (key && isValidOperator && valueString !== '') {
8!
211
          result.whereConditions.push({
8✔
212
            key: key,
213
            operator: rawOperatorFound,
214
            value: parseWhereValue(valueString),
215
          });
216
        } else {
NEW
217
          console.warn(`WARNING: Malformed condition part after finding operator "${operator}": "${conditionTrimmed}"`);
×
218
        }
219

220
      } else {
NEW
221
        console.warn(`WARNING: No valid operator found in condition part: "${conditionTrimmed}"`);
×
222
      }
223

224
    }
225
  });
226
}
227

228
function parseGroupByClause(groupByPart, result) {
229
  result.groupBy = null;
1✔
230

231
  if (!groupByPart || groupByPart.trim() === '') {
1!
NEW
232
    return;
×
233
  }
234

235
  const groupByPartTrimmed = groupByPart.trim();
1✔
236

237
  const columnMatch = groupByPartTrimmed.match(/^(\w+)$/);
1✔
238
  if (columnMatch && columnMatch[1]) {
1!
239
    result.groupBy = columnMatch[1];
1✔
240
  } else {
NEW
241
    console.warn(`WARNING: Unexpected GROUP BY format: "${groupByPartTrimmed}", expecting single identifier.`);
×
242
  }
243
}
244

245
function parseOrderByClause(orderByPart, result) {
246
  result.orderByAttr = null;
1✔
247
  result.orderDirection = null;
1✔
248

249
  if (!orderByPart || orderByPart.trim() === '') {
1!
NEW
250
    return;
×
251
  }
252

253
  const orderByPartTrimmed = orderByPart.trim();
1✔
254

255
  const orderByMatch = orderByPartTrimmed.match(/^([a-zA-Z0-9_.]+)\s*(ASC|DESC)?$/i);
1✔
256

257
  if (orderByMatch && orderByMatch[1]) {
1!
258
    result.orderByAttr = orderByMatch[1];
1✔
259
    result.orderDirection = orderByMatch[2]?.toUpperCase() || 'ASC';
1!
260
  } else {
NEW
261
    console.warn(`WARNING: Unexpected ORDER BY format: "${orderByPartTrimmed}", expecting identifier [ASC|DESC].`);
×
262
  }
263
}
264

265
function parseSQLQuery(query) {
266

267
  const result = {
5✔
268
    aggregations: [],
269
    columns: [],
270
    whereConditions: [],
271
    whereLogic: 'AND',
272
    groupBy: null,
273
    orderByAttr: null,
274
    orderDirection: null,
275
    table: 'computation',
276
  };
277

278
  if (!query || typeof query !== 'string') {
5!
NEW
279
    return result;
×
280
  }
281

282
  const upperQuery = query.toUpperCase();
5✔
283

284
  const keywords = ['SELECT ', ' FROM ', ' WHERE ', ' GROUP BY ', ' ORDER BY '];
5✔
285
  const indices = {};
5✔
286
  let currentPos = 0;
5✔
287

288
  for (const keyword of keywords) {
5✔
289
    const keywordUpper = keyword.toUpperCase();
19✔
290
    const index = upperQuery.indexOf(keywordUpper, currentPos);
19✔
291

292
    if (index !== -1) {
19✔
293
      indices[keyword.trim()] = index;
15✔
294
      currentPos = index + keyword.length;
15✔
295
    } else {
296
      console.warn(`WARNING: Keyword "${keyword.trim()}" not found after position ${currentPos}`);
4✔
297
      break;
4✔
298
    }
299
  }
300

301
  if (indices['SELECT'] === 0) {
5!
302
    const endSelect = indices['FROM'] !== undefined ? indices['FROM'] : query.length;
5!
303
    const selectPart = query.substring(indices['SELECT'] + keywords[0].length, endSelect).trim();
5✔
304
    parseSelectClause(selectPart, result);
5✔
305
  }
306

307
  if (indices['FROM'] !== undefined) {
5!
308
    const endFrom = indices['WHERE'] !== undefined ? indices['WHERE'] : indices['GROUP BY'] !== undefined ? indices['GROUP BY'] : indices['ORDER BY'] !== undefined ? indices['ORDER BY'] : query.length;
5!
309
    const fromPart = query.substring(indices['FROM'] + keywords[1].length, endFrom).trim();
5✔
310
    parseFromClause(fromPart, result);
5✔
311
  }
312

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

317
    const whereContentMatch = wherePartWithParens.match(/^\s*\((.+?)\)$/);
3✔
318
    if (whereContentMatch && whereContentMatch[1]) {
3!
319
      const whereContent = whereContentMatch[1].trim();
3✔
320
      parseWhereClause(whereContent, result);
3✔
321
    } else {
NEW
322
      console.warn(`WARNING: WHERE clause format unexpected (missing or malformed parentheses): "${wherePartWithParens}"`);
×
323
    }
324
  }
325

326
  if (indices['GROUP BY'] !== undefined) {
5✔
327
    const endGroupBy = indices['ORDER BY'] !== undefined ? indices['ORDER BY'] : query.length;
1!
328
    const groupByPart = query.substring(indices['GROUP BY'] + keywords[3].length, endGroupBy).trim();
1✔
329
    parseGroupByClause(groupByPart, result);
1✔
330
  }
331

332
  if (indices['ORDER BY'] !== undefined) {
5✔
333
    const orderByPart = query.substring(indices['ORDER BY'] + keywords[4].length).trim(); // Hasta el final
1✔
334
    parseOrderByClause(orderByPart, result);
1✔
335
  }
336

337
  return result;
5✔
338
}
339

340
function parseWhereValue(value) {
341
  if (value === 'true' || value === 'false') {
8✔
342
    return value === 'true';
1✔
343
  }
344
  if (!isNaN(value)) {
7✔
345
    return Number(value);
2✔
346
  }
347
  const quotedMatch = value.match(/^'(.*)'$/);
5✔
348
  return quotedMatch ? quotedMatch[1] : value;
5!
349
}
350

351
//Helpers
352
function sanitizeIdentifier(identifier) {
353
  return identifier.replace(/[^a-zA-Z0-9_]/g, '');
24✔
354
}
355

356
function sanitizeValue(value) {
357
  if (typeof value === 'number' || typeof value === 'boolean') {
6✔
358
    return String(value);
3✔
359
  }
360
  return String(value).replace(/['"]/g, '');
3✔
361
}
362

363
function sanitizeOperator(operator) {
364
  const validOperators = ['=', '>', '<', '>=', '<=', '!=', 'LIKE'];
6✔
365
  return validOperators.includes(operator.toUpperCase()) ? operator : '=';
6!
366
}
367

368
export { createSQLQuery, parseSQLQuery };
369

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