-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathmetering.ts
More file actions
135 lines (120 loc) · 4.08 KB
/
metering.ts
File metadata and controls
135 lines (120 loc) · 4.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import type { Request, Response } from 'express';
import { Context } from '@heyputer/backend/src/core';
import { HttpError } from '@heyputer/backend/src/core/http';
import {
controllersContainers,
driversContainers,
} from '@heyputer/backend/src/exports';
import { extension } from '@heyputer/backend/src/extensions';
const services = extension.import('service');
const clients = extension.import('client');
// Cached on first request — the underlying cost catalogues are baked into
// driver/controller source so they only change on deploy.
let cachedAllCosts: Record<string, unknown>[] | null = null;
function collectAllCosts(): Record<string, unknown>[] {
const all: Record<string, unknown>[] = [];
const collect = (
source: Record<string, unknown>,
kind: 'driver' | 'controller',
) => {
for (const [name, instance] of Object.entries(source)) {
const fn = (
instance as {
getReportedCosts?: () => Record<string, unknown>[];
}
)?.getReportedCosts;
if (typeof fn !== 'function') continue;
try {
const entries = fn.call(instance);
if (!Array.isArray(entries)) continue;
for (const entry of entries) {
all.push({ ...entry, registry: kind, registryKey: name });
}
} catch (e) {
console.warn(
`[metering] getReportedCosts failed for ${kind}:${name}:`,
(e as Error).message,
);
}
}
};
collect(driversContainers as Record<string, unknown>, 'driver');
collect(controllersContainers as Record<string, unknown>, 'controller');
return all;
}
export const handleMeteringUsage = async (
_req: Request,
res: Response,
): Promise<void> => {
const actor = Context.get('actor');
if (!actor?.user) throw new HttpError(401, 'Authentication required');
const [actorUsage, allowanceInfo] = await Promise.all([
services.metering.getActorCurrentMonthUsageDetails(actor),
services.metering.getAllowedUsage(actor),
]);
res.json({ ...actorUsage, allowanceInfo });
};
export const handleMeteringUsageForApp = async (
req: Request,
res: Response,
): Promise<void> => {
const actor = Context.get('actor');
if (!actor?.user) throw new HttpError(401, 'Authentication required');
let appId = String(req.params.appIdOrName ?? '');
if (!appId) throw new HttpError(400, 'appId parameter is required');
// If not a UUID-shaped app UID, look up by name
if (!appId.startsWith('app-')) {
const appRows = (await clients.db.read(
'SELECT `uid` FROM `apps` WHERE `name` = ? LIMIT 1',
[appId],
)) as Array<{ uid: string }>;
if (appRows.length > 0) {
appId = appRows[0].uid;
} else {
throw new HttpError(404, 'App not found');
}
}
const appUsage =
await services.metering.getActorCurrentMonthAppUsageDetails(
actor,
appId,
);
res.json(appUsage);
};
export const handleMeteringGlobalUsage = async (
_req: Request,
res: Response,
): Promise<void> => {
const globalUsage = await services.metering.getGlobalUsage();
res.json(globalUsage);
};
// First hit walks the registries; subsequent hits serve the in-memory cache.
export const handleMeteringAllCosts = async (
_req: Request,
res: Response,
): Promise<void> => {
if (!cachedAllCosts) {
cachedAllCosts = collectAllCosts();
}
res.json({ costs: cachedAllCosts });
};
extension.get(
'/metering/usage',
{ subdomain: 'api', requireAuth: true },
handleMeteringUsage,
);
extension.get(
'/metering/usage/:appIdOrName',
{ subdomain: 'api', requireAuth: true },
handleMeteringUsageForApp,
);
extension.get(
'/metering/globalUsage',
{ subdomain: 'api', adminOnly: true },
handleMeteringGlobalUsage,
);
extension.get(
'/metering/allCosts',
{ subdomain: 'api', requireAuth: true },
handleMeteringAllCosts,
);