/** * TASK-0133 — org OVERVIEW tab (counts + 30-day storage/audit trend). * * All counts are ORG-scoped (the org's workspaces). Storage = `File.size` * sum (the current-version rows — version history is not billed). The * 30-day trend is two queries (file bytes created, audit rows) bucketed by * UTC day — 30 rows each, no rollup table (the spec allows a rollup as an * optimization; kept simple in v1). */ import { Inject, Injectable } from "@nestjs/common"; import type { Request } from "express"; import { PrismaService } from "../../common/prisma.service.js"; import { requireUser } from "../../common/types.js"; import { assertOrgAdmin } from "./guards.js"; import { parseStoredPolicy } from "./policy.schema.js"; const DAY_MS = 86_400_000; @Injectable() export class OrgOverviewService { constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {} async get(orgId: string, req: Request) { const userId = requireUser(req); await assertOrgAdmin(this.prisma, orgId, userId); // Sessions are user-scoped (no workspace field) → active = an org member // holding an ACTIVE session (the members tab's `lastActiveAt` semantics). const [workspaces, members, pageCount, databaseCount, fileAgg, org] = await Promise.all([ this.prisma.workspace.findMany({ where: { organizationId: orgId }, select: { id: true } }), this.prisma.organizationMember.findMany({ where: { organizationId: orgId }, select: { userId: true } }), this.prisma.page.count({ where: { workspace: { organizationId: orgId }, trashedAt: null } }), this.prisma.database.count({ where: { workspace: { organizationId: orgId } } }), this.prisma.file.aggregate({ where: { workspace: { organizationId: orgId }, deletedAt: null }, _sum: { size: true }, _count: { _all: true } }), this.prisma.organization.findUnique({ where: { id: orgId }, select: { policy: true } }) ]); const workspaceIds = workspaces.map((w) => w.id); const memberIds = [...new Set(members.map((m) => m.userId))]; const activeSessions = await this.prisma.session.groupBy({ by: ["userId"], where: { userId: { in: memberIds }, status: "ACTIVE" }, _count: { _all: true } }); const storageBytes = fileAgg._sum.size ?? 0; // 30-day trends (bucketed by UTC day; sparse → zero-filled). const since = new Date(Date.now() - 29 * DAY_MS); const [storageTrend, auditTrend] = await Promise.all([ this.prisma.file.findMany({ where: { workspaceId: { in: workspaceIds }, createdAt: { gte: since } }, select: { size: true, createdAt: true } }), this.prisma.auditLog.groupBy({ by: ["createdAt"], where: { OR: [{ workspaceId: { in: workspaceIds } }, { workspaceId: null }], createdAt: { gte: since } }, _count: { _all: true } }) ]); const storageByDay = new Map(); const auditByDay = new Map(); for (let i = 29; i >= 0; i--) { const d = new Date(Date.now() - i * DAY_MS).toISOString().slice(0, 10); storageByDay.set(d, 0); auditByDay.set(d, 0); } for (const f of storageTrend) { const d = f.createdAt.toISOString().slice(0, 10); if (storageByDay.has(d)) storageByDay.set(d, (storageByDay.get(d) ?? 0) + f.size); } for (const a of auditTrend) { const d = a.createdAt.toISOString().slice(0, 10); if (auditByDay.has(d)) auditByDay.set(d, (auditByDay.get(d) ?? 0) + a._count._all); } return { memberCount: members.length, activeMemberCount: activeSessions.length, workspaceCount: workspaces.length, pageCount, databaseCount, fileCount: fileAgg._count._all, storageBytes, storageTrend: [...storageByDay.entries()].map(([day, bytes]) => ({ day, bytes })), auditTrend: [...auditByDay.entries()].map(([day, count]) => ({ day, count })), policy: parseStoredPolicy(org?.policy) }; } /** * `GET /orgs/:orgId/admin/workspaces` — the org's workspace list (id + * name + slug). The org console's API-keys / webhooks tabs are * WORKSPACE-scoped in the API (`/workspaces/:id/api-keys`, …/webhooks), so * the UI needs the org's workspaces to pick one. Admin-only. */ async workspaces(orgId: string, req: Request) { const userId = requireUser(req); await assertOrgAdmin(this.prisma, orgId, userId); const rows = await this.prisma.workspace.findMany({ where: { organizationId: orgId, isPersonal: false }, select: { id: true, name: true, slug: true }, orderBy: { createdAt: "asc" } }); return { data: rows }; } }