1
10import { Inject, Injectable } from "@nestjs/common";
11import type { Request } from "express";
12import { PrismaService } from "../../common/prisma.service.js";
13import { requireUser } from "../../common/types.js";
14import { assertOrgAdmin } from "./guards.js";
15import { parseStoredPolicy } from "./policy.schema.js";
16
17const DAY_MS = 86_400_000;
18
19@Injectable()
20export class OrgOverviewService {
21 constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
22
23 async get(orgId: string, req: Request) {
24 const userId = requireUser(req);
25 await assertOrgAdmin(this.prisma, orgId, userId);
26
27
28
29 const [workspaces, members, pageCount, databaseCount, fileAgg, org] = await Promise.all([
30 this.prisma.workspace.findMany({ where: { organizationId: orgId }, select: { id: true } }),
31 this.prisma.organizationMember.findMany({
32 where: { organizationId: orgId },
33 select: { userId: true }
34 }),
35 this.prisma.page.count({ where: { workspace: { organizationId: orgId }, trashedAt: null } }),
36 this.prisma.database.count({ where: { workspace: { organizationId: orgId } } }),
37 this.prisma.file.aggregate({
38 where: { workspace: { organizationId: orgId }, deletedAt: null },
39 _sum: { size: true },
40 _count: { _all: true }
41 }),
42 this.prisma.organization.findUnique({ where: { id: orgId }, select: { policy: true } })
43 ]);
44 const workspaceIds = workspaces.map((w) => w.id);
45 const memberIds = [...new Set(members.map((m) => m.userId))];
46 const activeSessions = await this.prisma.session.groupBy({
47 by: ["userId"],
48 where: { userId: { in: memberIds }, status: "ACTIVE" },
49 _count: { _all: true }
50 });
51 const storageBytes = fileAgg._sum.size ?? 0;
52
53
54 const since = new Date(Date.now() - 29 * DAY_MS);
55 const [storageTrend, auditTrend] = await Promise.all([
56 this.prisma.file.findMany({
57 where: { workspaceId: { in: workspaceIds }, createdAt: { gte: since } },
58 select: { size: true, createdAt: true }
59 }),
60 this.prisma.auditLog.groupBy({
61 by: ["createdAt"],
62 where: { OR: [{ workspaceId: { in: workspaceIds } }, { workspaceId: null }], createdAt: { gte: since } },
63 _count: { _all: true }
64 })
65 ]);
66 const storageByDay = new Map<string, number>();
67 const auditByDay = new Map<string, number>();
68 for (let i = 29; i >= 0; i--) {
69 const d = new Date(Date.now() - i * DAY_MS).toISOString().slice(0, 10);
70 storageByDay.set(d, 0);
71 auditByDay.set(d, 0);
72 }
73 for (const f of storageTrend) {
74 const d = f.createdAt.toISOString().slice(0, 10);
75 if (storageByDay.has(d)) storageByDay.set(d, (storageByDay.get(d) ?? 0) + f.size);
76 }
77 for (const a of auditTrend) {
78 const d = a.createdAt.toISOString().slice(0, 10);
79 if (auditByDay.has(d)) auditByDay.set(d, (auditByDay.get(d) ?? 0) + a._count._all);
80 }
81
82 return {
83 memberCount: members.length,
84 activeMemberCount: activeSessions.length,
85 workspaceCount: workspaces.length,
86 pageCount,
87 databaseCount,
88 fileCount: fileAgg._count._all,
89 storageBytes,
90 storageTrend: [...storageByDay.entries()].map(([day, bytes]) => ({ day, bytes })),
91 auditTrend: [...auditByDay.entries()].map(([day, count]) => ({ day, count })),
92 policy: parseStoredPolicy(org?.policy)
93 };
94 }
95
96
102 async workspaces(orgId: string, req: Request) {
103 const userId = requireUser(req);
104 await assertOrgAdmin(this.prisma, orgId, userId);
105 const rows = await this.prisma.workspace.findMany({
106 where: { organizationId: orgId, isPersonal: false },
107 select: { id: true, name: true, slug: true },
108 orderBy: { createdAt: "asc" }
109 });
110 return { data: rows };
111 }
112}
113