|
1 | 1 | import dayjs from 'dayjs'; |
2 | | -import { AdminForthResource, IAdminForthSingleFilter, IAdminForthAndOrFilter, IAdminForthDataSourceConnector, AdminForthConfig } from '../types/Back.js'; |
| 2 | +import { AdminForthResource, IAdminForthSingleFilter, IAdminForthAndOrFilter, IAdminForthDataSourceConnector, AdminForthConfig, IAggregationRule, IGroupByRule, IGroupByDateTrunc, IGroupByField } from '../types/Back.js'; |
3 | 3 | import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, } from '../types/Common.js'; |
4 | 4 | import AdminForthBaseConnector from './baseConnector.js'; |
5 | 5 | import mysql from 'mysql2/promise'; |
@@ -338,6 +338,82 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS |
338 | 338 | } : { sql: '', values: [] }; |
339 | 339 | } |
340 | 340 |
|
| 341 | + private calculateMedian(values: number[]): number | null { |
| 342 | + if (!values.length) return null; |
| 343 | + const sorted = values.sort((a, b) => a - b); |
| 344 | + const mid = Math.floor(sorted.length / 2); |
| 345 | + return sorted.length % 2 === 0 |
| 346 | + ? (sorted[mid - 1] + sorted[mid]) / 2 |
| 347 | + : sorted[mid]; |
| 348 | + } |
| 349 | + |
| 350 | + async getAggregateWithOriginalTypes({ resource, filters, aggregations, groupBy }: { |
| 351 | + resource: AdminForthResource; |
| 352 | + filters: IAdminForthAndOrFilter; |
| 353 | + aggregations: { [alias: string]: IAggregationRule }; |
| 354 | + groupBy?: IGroupByRule; |
| 355 | + }): Promise<Array<{ group?: string, [key: string]: any }>> { |
| 356 | + const tableName = resource.table; |
| 357 | + const selectParts: string[] = []; |
| 358 | + const medianAliases: string[] = []; |
| 359 | + let groupExpr: string | null = null; |
| 360 | + |
| 361 | + if (groupBy?.type === 'field') { |
| 362 | + groupExpr = `\`${groupBy.field}\``; |
| 363 | + selectParts.push(`${groupExpr} AS \`group\``); |
| 364 | + } else if (groupBy?.type === 'date_trunc') { |
| 365 | + const g = groupBy as IGroupByDateTrunc; |
| 366 | + const tz = g.timezone ?? 'UTC'; |
| 367 | + |
| 368 | + const innerExpr = `COALESCE(CONVERT_TZ(\`${g.field}\`, 'UTC', '${tz}'), \`${g.field}\`)`; |
| 369 | + |
| 370 | + switch (g.truncation) { |
| 371 | + case 'day': groupExpr = `DATE_FORMAT(${innerExpr}, '%Y-%m-%d')`; break; |
| 372 | + case 'month': groupExpr = `DATE_FORMAT(${innerExpr}, '%Y-%m-01')`; break; |
| 373 | + case 'year': groupExpr = `DATE_FORMAT(${innerExpr}, '%Y-01-01')`; break; |
| 374 | + case 'week': groupExpr = `DATE_FORMAT(DATE_SUB(${innerExpr}, INTERVAL WEEKDAY(${innerExpr}) DAY), '%Y-%m-%d')`; break; |
| 375 | + } |
| 376 | + |
| 377 | + selectParts.push(`${groupExpr} AS \`group\``); |
| 378 | + } |
| 379 | + |
| 380 | + for (const [alias, rule] of Object.entries(aggregations)) { |
| 381 | + const f = `\`${rule.field}\``; |
| 382 | + switch (rule.operation) { |
| 383 | + case 'sum': selectParts.push(`SUM(${f}) AS \`${alias}\``); break; |
| 384 | + case 'count': selectParts.push(`COUNT(*) AS \`${alias}\``); break; |
| 385 | + case 'avg': selectParts.push(`AVG(${f}) AS \`${alias}\``); break; |
| 386 | + case 'min': selectParts.push(`MIN(${f}) AS \`${alias}\``); break; |
| 387 | + case 'max': selectParts.push(`MAX(${f}) AS \`${alias}\``); break; |
| 388 | + case 'median': |
| 389 | + selectParts.push(`GROUP_CONCAT(${f}) AS \`${alias}\``); |
| 390 | + medianAliases.push(alias); |
| 391 | + break; |
| 392 | + } |
| 393 | + } |
| 394 | + |
| 395 | + const { sql: where, values: filterValues } = this.whereClauseAndValues(filters); |
| 396 | + let query = `SELECT ${selectParts.join(', ')} FROM \`${tableName}\` ${where}`; |
| 397 | + if (groupExpr) query += ` GROUP BY ${groupExpr} ORDER BY ${groupExpr} ASC`; |
| 398 | + |
| 399 | + await this.client.execute("SET SESSION group_concat_max_len = 1000000;"); |
| 400 | + |
| 401 | + const [rows]: any = await this.client.execute(query, filterValues); |
| 402 | + |
| 403 | + if (medianAliases.length > 0) { |
| 404 | + return rows.map(row => { |
| 405 | + medianAliases.forEach(alias => { |
| 406 | + const raw = row[alias]; |
| 407 | + const nums = raw ? raw.split(',').map(Number).filter(n => !isNaN(n)) : []; |
| 408 | + row[alias] = this.calculateMedian(nums); |
| 409 | + }); |
| 410 | + return row; |
| 411 | + }); |
| 412 | + } |
| 413 | + |
| 414 | + return rows; |
| 415 | + } |
| 416 | + |
341 | 417 | async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }): Promise<any[]> { |
342 | 418 | const columns = resource.dataSourceColumns.map((col) => `${col.name}`).join(', '); |
343 | 419 | const tableName = resource.table; |
|
0 commit comments