Skip to content

Commit e436009

Browse files
committed
feat: 移除 FinOps 模块并为查询任务添加可见性设置
1 parent 6ee71bc commit e436009

23 files changed

Lines changed: 319 additions & 594 deletions

backend/app/controllers/ai_fin_ops_controller.ts

Lines changed: 0 additions & 81 deletions
This file was deleted.

backend/app/controllers/dashboard_controller.ts

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,90 +6,118 @@ import User from '#models/user'
66
import { DateTime } from 'luxon'
77

88
export default class DashboardController {
9-
async index({ response }: HttpContext) {
10-
const stats = {
11-
totalDataSources: await DataSource.query()
9+
async index({ request, response, auth }: HttpContext) {
10+
const user = auth.user!
11+
await user.load('roles')
12+
const isGlobalView = user.isAdmin
13+
14+
const timeRange = request.input('timeRange', '7d')
15+
const daysMap: Record<string, number> = { '7d': 7, '30d': 30, '90d': 90, 'all': 90 }
16+
const days = daysMap[timeRange] || 7
17+
18+
const stats: any = {
19+
totalQueries: await QueryLog.query()
20+
.if(!isGlobalView, q => q.where('user_id', user.id))
1221
.count('* as total')
1322
.first()
1423
.then(r => Number(r?.$extras.total || 0)),
15-
totalTasks: await QueryTask.query()
24+
}
25+
26+
if (isGlobalView) {
27+
stats.totalDataSources = await DataSource.query()
1628
.count('* as total')
1729
.first()
18-
.then(r => Number(r?.$extras.total || 0)),
19-
totalQueries: await QueryLog.query()
30+
.then(r => Number(r?.$extras.total || 0))
31+
stats.totalTasks = await QueryTask.query()
2032
.count('* as total')
2133
.first()
22-
.then(r => Number(r?.$extras.total || 0)),
23-
totalUsers: await User.query()
34+
.then(r => Number(r?.$extras.total || 0))
35+
stats.totalUsers = await User.query()
2436
.count('* as total')
2537
.first()
26-
.then(r => Number(r?.$extras.total || 0)),
38+
.then(r => Number(r?.$extras.total || 0))
2739
}
2840

29-
// Get recent 7 days query activity (Success vs Failure)
30-
const trend = []
31-
for (let i = 6; i >= 0; i--) {
32-
const targetDate = DateTime.local().minus({ days: i })
33-
const startOfDay = targetDate.startOf('day').toSQL()
34-
const endOfDay = targetDate.endOf('day').toSQL()
35-
36-
const counts = await QueryLog.query()
37-
.whereBetween('created_at', [startOfDay, endOfDay])
38-
.select('status')
39-
.count('* as total')
40-
.groupBy('status')
41+
const startDate = DateTime.local().minus({ days: days - 1 }).startOf('day').toSQL()!
4142

42-
let success = 0
43-
let failed = 0
43+
// Efficient Trend JS Aggregation
44+
const trendQuery = QueryLog.query().where('created_at', '>=', startDate).select('created_at', 'status')
45+
if (!isGlobalView) {
46+
trendQuery.where('user_id', user.id)
47+
}
48+
const rawTrend = await trendQuery
4449

45-
counts.forEach((c) => {
46-
if (c.status === 'success')
47-
success = Number(c.$extras.total)
48-
else failed = Number(c.$extras.total)
49-
})
50+
const trendMap = new Map()
51+
for (let i = days - 1; i >= 0; i--) {
52+
trendMap.set(DateTime.local().minus({ days: i }).toISODate(), { success: 0, failed: 0, total: 0 })
53+
}
5054

51-
trend.push({ date: targetDate.toISODate(), success, failed, total: success + failed })
55+
// rawTrend created_at is a luxon DateTime in Lucid
56+
for (const log of rawTrend) {
57+
const d = log.createdAt.toISODate()
58+
if (d && trendMap.has(d)) {
59+
const obj = trendMap.get(d)
60+
if (log.status === 'success')
61+
obj.success++
62+
else obj.failed++
63+
obj.total++
64+
}
5265
}
5366

67+
const trend = Array.from(trendMap.entries()).map(([date, data]) => ({ date, ...data as any }))
68+
5469
// Top 5 Data Sources
55-
const topSources = await QueryLog.query()
70+
const topSourcesQuery = QueryLog.query()
71+
.where('query_logs.created_at', '>=', startDate)
5672
.leftJoin('data_sources', 'query_logs.data_source_id', 'data_sources.id')
5773
.select('data_sources.name')
5874
.count('* as total')
5975
.groupBy('data_sources.name')
6076
.orderBy('total', 'desc')
6177
.limit(5)
62-
.then(rows =>
63-
rows.map(r => ({
64-
name: r.$extras.name || 'Unknown',
65-
total: Number(r.$extras.total),
66-
})),
67-
)
78+
if (!isGlobalView)
79+
topSourcesQuery.where('query_logs.user_id', user.id)
80+
81+
const topSources = await topSourcesQuery.then(rows =>
82+
rows.map(r => ({
83+
name: r.$extras.name || 'Unknown',
84+
total: Number(r.$extras.total),
85+
})),
86+
)
6887

6988
// Top 5 Users
70-
const topUsers = await QueryLog.query()
71-
.join('users', 'query_logs.user_id', 'users.id')
72-
.select('users.full_name')
73-
.count('* as total')
74-
.groupBy('users.full_name')
75-
.orderBy('total', 'desc')
76-
.limit(5)
77-
.then(rows =>
78-
rows.map(r => ({ name: r.$extras.full_name, total: Number(r.$extras.total) })),
79-
)
89+
let topUsers: any[] = []
90+
if (isGlobalView) {
91+
topUsers = await QueryLog.query()
92+
.where('query_logs.created_at', '>=', startDate)
93+
.join('users', 'query_logs.user_id', 'users.id')
94+
.select('users.full_name')
95+
.count('* as total')
96+
.groupBy('users.full_name')
97+
.orderBy('total', 'desc')
98+
.limit(5)
99+
.then(rows =>
100+
rows.map(r => ({ name: r.$extras.full_name, total: Number(r.$extras.total) })),
101+
)
102+
}
80103

81-
const recentLogs = await QueryLog.query()
104+
const recentLogsQuery = QueryLog.query()
82105
.preload('user')
83106
.preload('task')
84107
.orderBy('createdAt', 'desc')
85108
.limit(5)
109+
if (!isGlobalView) {
110+
recentLogsQuery.where('user_id', user.id)
111+
}
112+
const recentLogs = await recentLogsQuery
86113

87114
return response.ok({
88115
stats,
89116
trend,
90117
topSources,
91118
topUsers,
92119
recentLogs,
120+
isGlobalView, // Return this so frontend knows what to render
93121
})
94122
}
95123
}

backend/app/controllers/execution_controller.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { HttpContext } from '@adonisjs/core/http'
22
import QueryTask from '#models/query_task'
33
import QueryExecutionService from '#services/query_execution_service'
4+
import { PERMISSIONS } from '@nexquery/shared'
45

56
export default class ExecutionController {
67
private executionService: QueryExecutionService
@@ -10,7 +11,18 @@ export default class ExecutionController {
1011
}
1112

1213
async execute({ params, request, response, auth }: HttpContext) {
14+
const user = auth.user!
1315
const task = await QueryTask.query().where('id', params.id).preload('dataSource').firstOrFail()
16+
17+
const hasManageTasks = await user.hasPermission(PERMISSIONS.MANAGE_TASKS)
18+
19+
// Authorization: Block execution if private and not creator
20+
if (!hasManageTasks) {
21+
if (task.visibility !== 'public' && task.createdBy !== user.id) {
22+
return response.forbidden({ message: 'Not authorized to execute this task' })
23+
}
24+
}
25+
1426
const inputParams = request.input('params', {})
1527

1628
try {

backend/app/controllers/query_tasks_controller.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
import type { HttpContext } from '@adonisjs/core/http'
22
import QueryTask from '#models/query_task'
33
import { createQueryTaskValidator, updateQueryTaskValidator } from '#validators/query_task'
4+
import { PERMISSIONS } from '@nexquery/shared'
45

56
export default class QueryTasksController {
67
/**
78
* Display a list of resource
89
*/
9-
async index({ request, response }: HttpContext) {
10+
async index({ request, response, auth }: HttpContext) {
1011
const search = request.input('search')
1112
const tag = request.input('tag')
13+
const user = auth.user!
1214

1315
const query = QueryTask.query()
1416
.preload('dataSource')
1517
.preload('creator')
1618
.orderBy('createdAt', 'desc')
1719

20+
const hasManageTasks = await user.hasPermission(PERMISSIONS.MANAGE_TASKS)
21+
22+
// Always enforce visibility rules
23+
if (!hasManageTasks) {
24+
query.where((q) => {
25+
q.where('visibility', 'public').orWhere('createdBy', user.id)
26+
})
27+
}
28+
1829
if (search && search !== '' && search !== 'undefined') {
1930
query.where((q) => {
2031
q.where('name', 'like', `%${search}%`).orWhere('description', 'like', `%${search}%`)
@@ -44,6 +55,7 @@ export default class QueryTasksController {
4455
formSchema: payload.formSchema || null,
4556
dataSourceId: payload.dataSourceId,
4657
storeResults: payload.storeResults ?? false,
58+
visibility: payload.visibility ?? 'private',
4759
tags: payload.tags || null,
4860
createdBy: user.id,
4961
})
@@ -54,20 +66,38 @@ export default class QueryTasksController {
5466
/**
5567
* Show individual record
5668
*/
57-
async show({ params, response }: HttpContext) {
69+
async show({ params, response, auth }: HttpContext) {
70+
const user = auth.user!
5871
const task = await QueryTask.query()
5972
.where('id', params.id)
6073
.preload('dataSource')
6174
.preload('creator')
6275
.firstOrFail()
76+
77+
const hasManageTasks = await user.hasPermission(PERMISSIONS.MANAGE_TASKS)
78+
if (!hasManageTasks) {
79+
if (task.visibility !== 'public' && task.createdBy !== user.id) {
80+
return response.forbidden({ message: 'Not authorized to view this task' })
81+
}
82+
}
83+
6384
return response.ok(task)
6485
}
6586

6687
/**
6788
* Handle form submission for the edit action
6889
*/
69-
async update({ params, request, response }: HttpContext) {
90+
async update({ params, request, response, auth }: HttpContext) {
91+
const user = auth.user!
7092
const task = await QueryTask.findOrFail(params.id)
93+
94+
const hasManageTasks = await user.hasPermission(PERMISSIONS.MANAGE_TASKS)
95+
96+
// Only creators or admins can edit
97+
if (!hasManageTasks && task.createdBy !== user.id) {
98+
return response.forbidden({ message: 'You can only edit your own query tasks' })
99+
}
100+
71101
const payload = await request.validateUsing(updateQueryTaskValidator)
72102

73103
if (payload.name !== undefined)
@@ -82,6 +112,8 @@ export default class QueryTasksController {
82112
task.dataSourceId = payload.dataSourceId
83113
if (payload.storeResults !== undefined)
84114
task.storeResults = payload.storeResults
115+
if (payload.visibility !== undefined)
116+
task.visibility = payload.visibility
85117
if (payload.tags !== undefined)
86118
task.tags = payload.tags
87119

@@ -93,8 +125,17 @@ export default class QueryTasksController {
93125
/**
94126
* Delete record
95127
*/
96-
async destroy({ params, response }: HttpContext) {
128+
async destroy({ params, response, auth }: HttpContext) {
129+
const user = auth.user!
97130
const task = await QueryTask.findOrFail(params.id)
131+
132+
const hasManageTasks = await user.hasPermission(PERMISSIONS.MANAGE_TASKS)
133+
134+
// Only creators or admins can delete
135+
if (!hasManageTasks && task.createdBy !== user.id) {
136+
return response.forbidden({ message: 'You can only delete your own query tasks' })
137+
}
138+
98139
await task.delete()
99140
return response.noContent()
100141
}

backend/app/models/query_task.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default class QueryTask extends BaseModel {
1717
@column()
1818
declare sqlTemplate: string
1919

20+
@column()
21+
declare visibility: 'private' | 'public'
22+
2023
@column({
2124
columnName: 'form_schema',
2225
prepare: (value: any) => (value ? JSON.stringify(value) : null),

0 commit comments

Comments
 (0)