@moneysiren/cli 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/apps/cli/src/cli.d.ts +56 -0
- package/dist/apps/cli/src/cli.js +182 -0
- package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
- package/dist/apps/cli/src/commands/dashboard.js +239 -0
- package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
- package/dist/apps/cli/src/commands/doctor.js +25 -0
- package/dist/apps/cli/src/commands/init.d.ts +3 -0
- package/dist/apps/cli/src/commands/init.js +18 -0
- package/dist/apps/cli/src/commands/install.d.ts +3 -0
- package/dist/apps/cli/src/commands/install.js +116 -0
- package/dist/apps/cli/src/commands/modes.d.ts +3 -0
- package/dist/apps/cli/src/commands/modes.js +65 -0
- package/dist/apps/cli/src/commands/notify.d.ts +3 -0
- package/dist/apps/cli/src/commands/notify.js +430 -0
- package/dist/apps/cli/src/commands/report.d.ts +3 -0
- package/dist/apps/cli/src/commands/report.js +206 -0
- package/dist/apps/cli/src/commands/runtime.d.ts +5 -0
- package/dist/apps/cli/src/commands/runtime.js +133 -0
- package/dist/apps/cli/src/commands/shared.d.ts +9 -0
- package/dist/apps/cli/src/commands/shared.js +29 -0
- package/dist/apps/cli/src/commands/summary.d.ts +3 -0
- package/dist/apps/cli/src/commands/summary.js +15 -0
- package/dist/apps/cli/src/commands/sync.d.ts +3 -0
- package/dist/apps/cli/src/commands/sync.js +393 -0
- package/dist/apps/cli/src/commands/theme.d.ts +3 -0
- package/dist/apps/cli/src/commands/theme.js +181 -0
- package/dist/apps/cli/src/home.d.ts +7 -0
- package/dist/apps/cli/src/home.js +97 -0
- package/dist/apps/cli/src/index.d.ts +3 -0
- package/dist/apps/cli/src/index.js +14 -0
- package/dist/apps/cli/src/install-profile.d.ts +35 -0
- package/dist/apps/cli/src/install-profile.js +124 -0
- package/dist/apps/cli/src/install-selector.d.ts +10 -0
- package/dist/apps/cli/src/install-selector.js +66 -0
- package/dist/apps/cli/src/interactive.d.ts +3 -0
- package/dist/apps/cli/src/interactive.js +32 -0
- package/dist/apps/cli/src/postinstall.d.ts +3 -0
- package/dist/apps/cli/src/postinstall.js +42 -0
- package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
- package/dist/apps/cli/src/runtime-adapter.js +185 -0
- package/dist/apps/cli/src/slash.d.ts +15 -0
- package/dist/apps/cli/src/slash.js +202 -0
- package/dist/apps/cli/src/summary-model.d.ts +51 -0
- package/dist/apps/cli/src/summary-model.js +136 -0
- package/dist/apps/cli/src/theme.d.ts +18 -0
- package/dist/apps/cli/src/theme.js +118 -0
- package/dist/packages/config/src/index.d.ts +3 -0
- package/dist/packages/config/src/index.js +3 -0
- package/dist/packages/config/src/load.d.ts +3 -0
- package/dist/packages/config/src/load.js +77 -0
- package/dist/packages/config/src/schema.d.ts +46 -0
- package/dist/packages/config/src/schema.js +25 -0
- package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
- package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
- package/dist/packages/connectors/aws/src/index.d.ts +35 -0
- package/dist/packages/connectors/aws/src/index.js +67 -0
- package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
- package/dist/packages/connectors/aws/src/normalize.js +141 -0
- package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
- package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
- package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
- package/dist/packages/connectors/cloudflare/src/client.js +107 -0
- package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
- package/dist/packages/connectors/cloudflare/src/index.js +81 -0
- package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
- package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
- package/dist/packages/connectors/mock/src/index.d.ts +58 -0
- package/dist/packages/connectors/mock/src/index.js +66 -0
- package/dist/packages/connectors/openai/src/index.d.ts +55 -0
- package/dist/packages/connectors/openai/src/index.js +169 -0
- package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
- package/dist/packages/connectors/openai/src/normalize.js +180 -0
- package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
- package/dist/packages/connectors/supabase/src/client.js +132 -0
- package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
- package/dist/packages/connectors/supabase/src/index.js +87 -0
- package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
- package/dist/packages/connectors/supabase/src/normalize.js +266 -0
- package/dist/packages/core/src/collector.d.ts +12 -0
- package/dist/packages/core/src/collector.js +68 -0
- package/dist/packages/core/src/index.d.ts +5 -0
- package/dist/packages/core/src/index.js +4 -0
- package/dist/packages/core/src/provider.d.ts +18 -0
- package/dist/packages/core/src/provider.js +2 -0
- package/dist/packages/core/src/risk-engine.d.ts +9 -0
- package/dist/packages/core/src/risk-engine.js +4 -0
- package/dist/packages/core/src/snapshots.d.ts +49 -0
- package/dist/packages/core/src/snapshots.js +9 -0
- package/dist/packages/db/src/client.d.ts +11 -0
- package/dist/packages/db/src/client.js +14 -0
- package/dist/packages/db/src/index.d.ts +6 -0
- package/dist/packages/db/src/index.js +6 -0
- package/dist/packages/db/src/local-store.d.ts +161 -0
- package/dist/packages/db/src/local-store.js +623 -0
- package/dist/packages/db/src/migrate.d.ts +17 -0
- package/dist/packages/db/src/migrate.js +35 -0
- package/dist/packages/db/src/schema.d.ts +5 -0
- package/dist/packages/db/src/schema.js +120 -0
- package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
- package/dist/packages/db/src/sqlite-bin.js +16 -0
- package/dist/packages/local-api/src/index.d.ts +2 -0
- package/dist/packages/local-api/src/index.js +2 -0
- package/dist/packages/local-api/src/server.d.ts +36 -0
- package/dist/packages/local-api/src/server.js +310 -0
- package/dist/packages/report/src/daily.d.ts +24 -0
- package/dist/packages/report/src/daily.js +9 -0
- package/dist/packages/report/src/index.d.ts +4 -0
- package/dist/packages/report/src/index.js +4 -0
- package/dist/packages/report/src/korean.d.ts +3 -0
- package/dist/packages/report/src/korean.js +62 -0
- package/dist/packages/report/src/slack.d.ts +34 -0
- package/dist/packages/report/src/slack.js +134 -0
- package/dist/packages/runtime/src/index.d.ts +2 -0
- package/dist/packages/runtime/src/index.js +2 -0
- package/dist/packages/runtime/src/runtime.d.ts +26 -0
- package/dist/packages/runtime/src/runtime.js +182 -0
- package/dist/packages/view-model/src/index.d.ts +3 -0
- package/dist/packages/view-model/src/index.js +3 -0
- package/dist/packages/view-model/src/notification-preferences-model.d.ts +47 -0
- package/dist/packages/view-model/src/notification-preferences-model.js +218 -0
- package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
- package/dist/packages/view-model/src/notification-preferences.js +36 -0
- package/dist/packages/view-model/src/view-model.d.ts +193 -0
- package/dist/packages/view-model/src/view-model.js +684 -0
- package/package.json +49 -0
- package/scripts/postinstall.mjs +11 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { runMigrations } from "./migrate.js";
|
|
7
|
+
import { resolveSqliteBin, SQLITE_BIN_ENV_KEY } from "./sqlite-bin.js";
|
|
8
|
+
const EMPTY_METADATA_JSON = "{}";
|
|
9
|
+
const requireNodeModule = createRequire(import.meta.url);
|
|
10
|
+
const FORBIDDEN_KEY_PATTERN = /^(raw|rawPayload|rawResponse|providerPayload|providerResponse|billingProfile)$/i;
|
|
11
|
+
const FORBIDDEN_STRING_PATTERN = /acct_|project_|invoice_|sk-|hooks\.slack|@/i;
|
|
12
|
+
export async function initializeLocalStore(options) {
|
|
13
|
+
const dbPath = normalizeDbPath(options.dbPath);
|
|
14
|
+
await mkdir(dirname(dbPath), { recursive: true });
|
|
15
|
+
return runMigrations({
|
|
16
|
+
async getAppliedMigrationIds() {
|
|
17
|
+
return getAppliedMigrationIds(dbPath);
|
|
18
|
+
},
|
|
19
|
+
async execute(sql) {
|
|
20
|
+
await executeSqlite(dbPath, sql);
|
|
21
|
+
},
|
|
22
|
+
async recordMigration(id) {
|
|
23
|
+
await executeSqlite(dbPath, `INSERT INTO schema_migrations (id) VALUES (${sqlString(id)}) ON CONFLICT(id) DO NOTHING;`);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function readLocalStore(options) {
|
|
28
|
+
const dbPath = normalizeDbPath(options.dbPath);
|
|
29
|
+
return {
|
|
30
|
+
appliedMigrationIds: getAppliedMigrationIds(dbPath),
|
|
31
|
+
providers: readProviders(dbPath),
|
|
32
|
+
usageSnapshots: readUsageSnapshots(dbPath),
|
|
33
|
+
billingSnapshots: readBillingSnapshots(dbPath),
|
|
34
|
+
serviceHealthSnapshots: readServiceHealthSnapshots(dbPath),
|
|
35
|
+
costEstimates: readCostEstimates(dbPath),
|
|
36
|
+
alerts: readAlerts(dbPath),
|
|
37
|
+
reportRuns: readReportRuns(dbPath),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function saveLocalProviderCollection(input) {
|
|
41
|
+
assertSafeForPersistence(input);
|
|
42
|
+
await initializeLocalStore({ dbPath: input.dbPath });
|
|
43
|
+
const dbPath = normalizeDbPath(input.dbPath);
|
|
44
|
+
const providerId = providerIdFor(input.provider.key);
|
|
45
|
+
const providerAccountRefs = collectProviderAccountRefs(input);
|
|
46
|
+
const statements = [
|
|
47
|
+
upsertProviderSql({
|
|
48
|
+
id: providerId,
|
|
49
|
+
key: input.provider.key,
|
|
50
|
+
displayName: input.provider.displayName,
|
|
51
|
+
connectorVersion: input.provider.connectorVersion,
|
|
52
|
+
timestamp: input.collectedAt,
|
|
53
|
+
}),
|
|
54
|
+
];
|
|
55
|
+
if (input.status === "ok") {
|
|
56
|
+
statements.push(deleteProviderSyncAlertsSql(providerId));
|
|
57
|
+
}
|
|
58
|
+
for (const providerAccountRef of providerAccountRefs) {
|
|
59
|
+
statements.push(upsertProviderAccountSql(providerId, input.provider.key, providerAccountRef, input.collectedAt));
|
|
60
|
+
}
|
|
61
|
+
for (const snapshot of input.snapshots.usage) {
|
|
62
|
+
statements.push(insertUsageSnapshotSql(providerId, input.provider.key, snapshot));
|
|
63
|
+
}
|
|
64
|
+
for (const snapshot of input.snapshots.billing) {
|
|
65
|
+
statements.push(insertBillingSnapshotSql(providerId, input.provider.key, snapshot));
|
|
66
|
+
}
|
|
67
|
+
for (const snapshot of input.snapshots.serviceHealth) {
|
|
68
|
+
statements.push(insertServiceHealthSnapshotSql(providerId, snapshot));
|
|
69
|
+
}
|
|
70
|
+
for (const snapshot of input.snapshots.costEstimates) {
|
|
71
|
+
statements.push(insertCostEstimateSql(providerId, input.provider.key, snapshot));
|
|
72
|
+
}
|
|
73
|
+
for (const alert of input.alerts) {
|
|
74
|
+
statements.push(insertAlertSql(alert));
|
|
75
|
+
}
|
|
76
|
+
executeSqliteTransaction(dbPath, statements);
|
|
77
|
+
}
|
|
78
|
+
export async function recordLocalReportRun(input) {
|
|
79
|
+
assertSafeForPersistence(input);
|
|
80
|
+
await initializeLocalStore({ dbPath: input.dbPath });
|
|
81
|
+
executeSqliteTransaction(normalizeDbPath(input.dbPath), [
|
|
82
|
+
`
|
|
83
|
+
INSERT INTO report_runs (id, created_at, report_date, language, delivery_target, status, metadata_json)
|
|
84
|
+
VALUES (
|
|
85
|
+
${sqlString(randomUUID())},
|
|
86
|
+
${sqlString(input.createdAt)},
|
|
87
|
+
${sqlString(input.reportDate)},
|
|
88
|
+
${sqlString(input.language)},
|
|
89
|
+
${sqlString(input.deliveryTarget)},
|
|
90
|
+
${sqlString(input.status)},
|
|
91
|
+
${sqlString(EMPTY_METADATA_JSON)}
|
|
92
|
+
);
|
|
93
|
+
`,
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
function getAppliedMigrationIds(dbPath) {
|
|
97
|
+
const schemaMigrationTables = querySqliteRowsSync(dbPath, "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations';");
|
|
98
|
+
if (schemaMigrationTables.length === 0) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return querySqliteRowsSync(dbPath, "SELECT id FROM schema_migrations ORDER BY id;").map((row) => row.id);
|
|
102
|
+
}
|
|
103
|
+
function readProviders(dbPath) {
|
|
104
|
+
return querySqliteRowsSync(dbPath, `
|
|
105
|
+
SELECT
|
|
106
|
+
id,
|
|
107
|
+
provider_key AS key,
|
|
108
|
+
display_name AS displayName,
|
|
109
|
+
connector_version AS connectorVersion,
|
|
110
|
+
created_at AS createdAt,
|
|
111
|
+
updated_at AS updatedAt
|
|
112
|
+
FROM providers
|
|
113
|
+
ORDER BY provider_key;
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
function readUsageSnapshots(dbPath) {
|
|
117
|
+
return querySqliteRowsSync(dbPath, `
|
|
118
|
+
SELECT
|
|
119
|
+
usage_snapshots.id AS id,
|
|
120
|
+
providers.provider_key AS providerKey,
|
|
121
|
+
provider_accounts.account_ref AS providerAccountRef,
|
|
122
|
+
usage_snapshots.collected_at AS collectedAt,
|
|
123
|
+
usage_snapshots.service AS service,
|
|
124
|
+
usage_snapshots.metric AS metric,
|
|
125
|
+
usage_snapshots.unit AS unit,
|
|
126
|
+
usage_snapshots.value AS value,
|
|
127
|
+
usage_snapshots.metadata_json AS metadataJson
|
|
128
|
+
FROM usage_snapshots
|
|
129
|
+
JOIN providers ON providers.id = usage_snapshots.provider_id
|
|
130
|
+
LEFT JOIN provider_accounts ON provider_accounts.id = usage_snapshots.provider_account_id
|
|
131
|
+
ORDER BY usage_snapshots.collected_at, usage_snapshots.id;
|
|
132
|
+
`).map((row) => ({
|
|
133
|
+
id: row.id,
|
|
134
|
+
providerKey: row.providerKey,
|
|
135
|
+
collectedAt: row.collectedAt,
|
|
136
|
+
service: row.service,
|
|
137
|
+
metric: row.metric,
|
|
138
|
+
unit: row.unit,
|
|
139
|
+
value: row.value,
|
|
140
|
+
metadataJson: emptyMetadata(row.metadataJson),
|
|
141
|
+
...(row.providerAccountRef === null ? {} : { providerAccountRef: row.providerAccountRef }),
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
function readBillingSnapshots(dbPath) {
|
|
145
|
+
return querySqliteRowsSync(dbPath, `
|
|
146
|
+
SELECT
|
|
147
|
+
billing_snapshots.id AS id,
|
|
148
|
+
providers.provider_key AS providerKey,
|
|
149
|
+
provider_accounts.account_ref AS providerAccountRef,
|
|
150
|
+
billing_snapshots.collected_at AS collectedAt,
|
|
151
|
+
billing_snapshots.period_start AS periodStart,
|
|
152
|
+
billing_snapshots.period_end AS periodEnd,
|
|
153
|
+
billing_snapshots.amount_minor AS amountMinor,
|
|
154
|
+
billing_snapshots.currency AS currency,
|
|
155
|
+
billing_snapshots.status AS status,
|
|
156
|
+
billing_snapshots.metadata_json AS metadataJson
|
|
157
|
+
FROM billing_snapshots
|
|
158
|
+
JOIN providers ON providers.id = billing_snapshots.provider_id
|
|
159
|
+
LEFT JOIN provider_accounts ON provider_accounts.id = billing_snapshots.provider_account_id
|
|
160
|
+
WHERE NOT EXISTS (
|
|
161
|
+
SELECT 1
|
|
162
|
+
FROM billing_snapshots AS newer_billing_snapshot
|
|
163
|
+
WHERE newer_billing_snapshot.provider_id = billing_snapshots.provider_id
|
|
164
|
+
AND newer_billing_snapshot.provider_account_id IS billing_snapshots.provider_account_id
|
|
165
|
+
AND newer_billing_snapshot.period_start = billing_snapshots.period_start
|
|
166
|
+
AND newer_billing_snapshot.period_end = billing_snapshots.period_end
|
|
167
|
+
AND newer_billing_snapshot.currency = billing_snapshots.currency
|
|
168
|
+
AND (
|
|
169
|
+
newer_billing_snapshot.collected_at > billing_snapshots.collected_at
|
|
170
|
+
OR (
|
|
171
|
+
newer_billing_snapshot.collected_at = billing_snapshots.collected_at
|
|
172
|
+
AND newer_billing_snapshot.id > billing_snapshots.id
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
ORDER BY billing_snapshots.collected_at, billing_snapshots.id;
|
|
177
|
+
`).map((row) => ({
|
|
178
|
+
id: row.id,
|
|
179
|
+
providerKey: row.providerKey,
|
|
180
|
+
collectedAt: row.collectedAt,
|
|
181
|
+
periodStart: row.periodStart,
|
|
182
|
+
periodEnd: row.periodEnd,
|
|
183
|
+
amountMinor: row.amountMinor,
|
|
184
|
+
currency: row.currency,
|
|
185
|
+
status: row.status,
|
|
186
|
+
metadataJson: emptyMetadata(row.metadataJson),
|
|
187
|
+
...(row.providerAccountRef === null ? {} : { providerAccountRef: row.providerAccountRef }),
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
function readServiceHealthSnapshots(dbPath) {
|
|
191
|
+
return querySqliteRowsSync(dbPath, `
|
|
192
|
+
SELECT
|
|
193
|
+
service_health_snapshots.id AS id,
|
|
194
|
+
providers.provider_key AS providerKey,
|
|
195
|
+
service_health_snapshots.collected_at AS collectedAt,
|
|
196
|
+
service_health_snapshots.service AS service,
|
|
197
|
+
service_health_snapshots.region AS region,
|
|
198
|
+
service_health_snapshots.status AS status,
|
|
199
|
+
service_health_snapshots.message AS message,
|
|
200
|
+
service_health_snapshots.metadata_json AS metadataJson
|
|
201
|
+
FROM service_health_snapshots
|
|
202
|
+
JOIN providers ON providers.id = service_health_snapshots.provider_id
|
|
203
|
+
ORDER BY service_health_snapshots.collected_at, service_health_snapshots.id;
|
|
204
|
+
`).map((row) => ({
|
|
205
|
+
id: row.id,
|
|
206
|
+
providerKey: row.providerKey,
|
|
207
|
+
collectedAt: row.collectedAt,
|
|
208
|
+
service: row.service,
|
|
209
|
+
status: row.status,
|
|
210
|
+
metadataJson: emptyMetadata(row.metadataJson),
|
|
211
|
+
...(row.region === null ? {} : { region: row.region }),
|
|
212
|
+
...(row.message === null ? {} : { message: row.message }),
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
function readCostEstimates(dbPath) {
|
|
216
|
+
return querySqliteRowsSync(dbPath, `
|
|
217
|
+
SELECT
|
|
218
|
+
cost_estimates.id AS id,
|
|
219
|
+
providers.provider_key AS providerKey,
|
|
220
|
+
provider_accounts.account_ref AS providerAccountRef,
|
|
221
|
+
cost_estimates.collected_at AS collectedAt,
|
|
222
|
+
cost_estimates.period_start AS periodStart,
|
|
223
|
+
cost_estimates.period_end AS periodEnd,
|
|
224
|
+
cost_estimates.estimated_amount_minor AS estimatedAmountMinor,
|
|
225
|
+
cost_estimates.currency AS currency,
|
|
226
|
+
cost_estimates.confidence AS confidence,
|
|
227
|
+
cost_estimates.metadata_json AS metadataJson
|
|
228
|
+
FROM cost_estimates
|
|
229
|
+
JOIN providers ON providers.id = cost_estimates.provider_id
|
|
230
|
+
LEFT JOIN provider_accounts ON provider_accounts.id = cost_estimates.provider_account_id
|
|
231
|
+
WHERE NOT EXISTS (
|
|
232
|
+
SELECT 1
|
|
233
|
+
FROM cost_estimates AS newer_cost_estimate
|
|
234
|
+
WHERE newer_cost_estimate.provider_id = cost_estimates.provider_id
|
|
235
|
+
AND newer_cost_estimate.provider_account_id IS cost_estimates.provider_account_id
|
|
236
|
+
AND newer_cost_estimate.period_start = cost_estimates.period_start
|
|
237
|
+
AND newer_cost_estimate.period_end = cost_estimates.period_end
|
|
238
|
+
AND newer_cost_estimate.currency = cost_estimates.currency
|
|
239
|
+
AND (
|
|
240
|
+
newer_cost_estimate.collected_at > cost_estimates.collected_at
|
|
241
|
+
OR (
|
|
242
|
+
newer_cost_estimate.collected_at = cost_estimates.collected_at
|
|
243
|
+
AND newer_cost_estimate.id > cost_estimates.id
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
ORDER BY cost_estimates.collected_at, cost_estimates.id;
|
|
248
|
+
`).map((row) => ({
|
|
249
|
+
id: row.id,
|
|
250
|
+
providerKey: row.providerKey,
|
|
251
|
+
collectedAt: row.collectedAt,
|
|
252
|
+
periodStart: row.periodStart,
|
|
253
|
+
periodEnd: row.periodEnd,
|
|
254
|
+
estimatedAmountMinor: row.estimatedAmountMinor,
|
|
255
|
+
currency: row.currency,
|
|
256
|
+
confidence: row.confidence,
|
|
257
|
+
metadataJson: emptyMetadata(row.metadataJson),
|
|
258
|
+
...(row.providerAccountRef === null ? {} : { providerAccountRef: row.providerAccountRef }),
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
function readAlerts(dbPath) {
|
|
262
|
+
return querySqliteRowsSync(dbPath, `
|
|
263
|
+
SELECT
|
|
264
|
+
alerts.id AS id,
|
|
265
|
+
providers.provider_key AS providerKey,
|
|
266
|
+
alerts.created_at AS createdAt,
|
|
267
|
+
alerts.severity AS severity,
|
|
268
|
+
alerts.category AS category,
|
|
269
|
+
alerts.title AS title,
|
|
270
|
+
alerts.message AS message,
|
|
271
|
+
alerts.metadata_json AS metadataJson
|
|
272
|
+
FROM alerts
|
|
273
|
+
LEFT JOIN providers ON providers.id = alerts.provider_id
|
|
274
|
+
ORDER BY alerts.created_at, alerts.id;
|
|
275
|
+
`).map((row) => ({
|
|
276
|
+
id: row.id,
|
|
277
|
+
createdAt: row.createdAt,
|
|
278
|
+
severity: row.severity,
|
|
279
|
+
category: row.category,
|
|
280
|
+
title: row.title,
|
|
281
|
+
message: row.message,
|
|
282
|
+
metadataJson: emptyMetadata(row.metadataJson),
|
|
283
|
+
...(row.providerKey === null ? {} : { providerKey: row.providerKey }),
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
function readReportRuns(dbPath) {
|
|
287
|
+
return querySqliteRowsSync(dbPath, `
|
|
288
|
+
SELECT
|
|
289
|
+
id,
|
|
290
|
+
created_at AS createdAt,
|
|
291
|
+
report_date AS reportDate,
|
|
292
|
+
language,
|
|
293
|
+
delivery_target AS deliveryTarget,
|
|
294
|
+
status,
|
|
295
|
+
metadata_json AS metadataJson
|
|
296
|
+
FROM report_runs
|
|
297
|
+
ORDER BY created_at, id;
|
|
298
|
+
`).map((row) => ({
|
|
299
|
+
id: row.id,
|
|
300
|
+
createdAt: row.createdAt,
|
|
301
|
+
reportDate: row.reportDate,
|
|
302
|
+
language: row.language,
|
|
303
|
+
deliveryTarget: row.deliveryTarget,
|
|
304
|
+
status: row.status,
|
|
305
|
+
metadataJson: emptyMetadata(row.metadataJson),
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
function upsertProviderSql(input) {
|
|
309
|
+
return `
|
|
310
|
+
INSERT INTO providers (id, provider_key, display_name, connector_version, created_at, updated_at)
|
|
311
|
+
VALUES (
|
|
312
|
+
${sqlString(input.id)},
|
|
313
|
+
${sqlString(input.key)},
|
|
314
|
+
${sqlString(input.displayName)},
|
|
315
|
+
${sqlString(input.connectorVersion)},
|
|
316
|
+
${sqlString(input.timestamp)},
|
|
317
|
+
${sqlString(input.timestamp)}
|
|
318
|
+
)
|
|
319
|
+
ON CONFLICT(provider_key) DO UPDATE SET
|
|
320
|
+
display_name = excluded.display_name,
|
|
321
|
+
connector_version = excluded.connector_version,
|
|
322
|
+
updated_at = excluded.updated_at;
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
function upsertProviderAccountSql(providerId, providerKey, providerAccountRef, timestamp) {
|
|
326
|
+
return `
|
|
327
|
+
INSERT INTO provider_accounts (id, provider_id, account_label, account_ref, created_at, updated_at)
|
|
328
|
+
VALUES (
|
|
329
|
+
${sqlString(providerAccountIdFor(providerKey, providerAccountRef))},
|
|
330
|
+
${sqlString(providerId)},
|
|
331
|
+
${sqlString("redacted-provider-account")},
|
|
332
|
+
${sqlString(providerAccountDigest(providerAccountRef))},
|
|
333
|
+
${sqlString(timestamp)},
|
|
334
|
+
${sqlString(timestamp)}
|
|
335
|
+
)
|
|
336
|
+
ON CONFLICT(provider_id, account_ref) DO UPDATE SET
|
|
337
|
+
updated_at = excluded.updated_at;
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
function insertUsageSnapshotSql(providerId, providerKey, snapshot) {
|
|
341
|
+
return `
|
|
342
|
+
INSERT INTO usage_snapshots (
|
|
343
|
+
id, provider_id, provider_account_id, collected_at, service, metric, unit, value, metadata_json
|
|
344
|
+
)
|
|
345
|
+
VALUES (
|
|
346
|
+
${sqlString(randomUUID())},
|
|
347
|
+
${sqlString(providerId)},
|
|
348
|
+
${sqlProviderAccountId(providerKey, snapshot.providerAccountRef)},
|
|
349
|
+
${sqlString(snapshot.collectedAt)},
|
|
350
|
+
${sqlString(snapshot.service ?? "unknown")},
|
|
351
|
+
${sqlString(snapshot.metric)},
|
|
352
|
+
${sqlString(snapshot.unit)},
|
|
353
|
+
${sqlNumber(snapshot.value)},
|
|
354
|
+
${sqlString(EMPTY_METADATA_JSON)}
|
|
355
|
+
);
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
function insertBillingSnapshotSql(providerId, providerKey, snapshot) {
|
|
359
|
+
return `
|
|
360
|
+
INSERT INTO billing_snapshots (
|
|
361
|
+
id, provider_id, provider_account_id, collected_at, period_start, period_end, amount_minor, currency, status,
|
|
362
|
+
metadata_json
|
|
363
|
+
)
|
|
364
|
+
VALUES (
|
|
365
|
+
${sqlString(randomUUID())},
|
|
366
|
+
${sqlString(providerId)},
|
|
367
|
+
${sqlProviderAccountId(providerKey, snapshot.providerAccountRef)},
|
|
368
|
+
${sqlString(snapshot.collectedAt)},
|
|
369
|
+
${sqlString(snapshot.periodStart)},
|
|
370
|
+
${sqlString(snapshot.periodEnd)},
|
|
371
|
+
${sqlInteger(snapshot.amountMinor)},
|
|
372
|
+
${sqlString(snapshot.currency)},
|
|
373
|
+
${sqlString(snapshot.status)},
|
|
374
|
+
${sqlString(EMPTY_METADATA_JSON)}
|
|
375
|
+
);
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
function insertServiceHealthSnapshotSql(providerId, snapshot) {
|
|
379
|
+
return `
|
|
380
|
+
INSERT INTO service_health_snapshots (
|
|
381
|
+
id, provider_id, collected_at, service, region, status, message, metadata_json
|
|
382
|
+
)
|
|
383
|
+
VALUES (
|
|
384
|
+
${sqlString(randomUUID())},
|
|
385
|
+
${sqlString(providerId)},
|
|
386
|
+
${sqlString(snapshot.collectedAt)},
|
|
387
|
+
${sqlString(snapshot.service)},
|
|
388
|
+
${sqlNullableString(snapshot.region)},
|
|
389
|
+
${sqlString(snapshot.status)},
|
|
390
|
+
${sqlNullableString(snapshot.message)},
|
|
391
|
+
${sqlString(EMPTY_METADATA_JSON)}
|
|
392
|
+
);
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
function insertCostEstimateSql(providerId, providerKey, snapshot) {
|
|
396
|
+
return `
|
|
397
|
+
INSERT INTO cost_estimates (
|
|
398
|
+
id, provider_id, provider_account_id, collected_at, period_start, period_end, estimated_amount_minor, currency,
|
|
399
|
+
confidence, metadata_json
|
|
400
|
+
)
|
|
401
|
+
VALUES (
|
|
402
|
+
${sqlString(randomUUID())},
|
|
403
|
+
${sqlString(providerId)},
|
|
404
|
+
${sqlProviderAccountId(providerKey, snapshot.providerAccountRef)},
|
|
405
|
+
${sqlString(snapshot.collectedAt)},
|
|
406
|
+
${sqlString(snapshot.periodStart)},
|
|
407
|
+
${sqlString(snapshot.periodEnd)},
|
|
408
|
+
${sqlInteger(snapshot.estimatedAmountMinor)},
|
|
409
|
+
${sqlString(snapshot.currency)},
|
|
410
|
+
${sqlString(snapshot.confidence)},
|
|
411
|
+
${sqlString(EMPTY_METADATA_JSON)}
|
|
412
|
+
);
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
415
|
+
function insertAlertSql(alert) {
|
|
416
|
+
return `
|
|
417
|
+
INSERT INTO alerts (id, provider_id, created_at, severity, category, title, message, metadata_json)
|
|
418
|
+
VALUES (
|
|
419
|
+
${sqlString(randomUUID())},
|
|
420
|
+
${alert.provider === undefined ? "NULL" : sqlString(providerIdFor(alert.provider))},
|
|
421
|
+
${sqlString(alert.createdAt)},
|
|
422
|
+
${sqlString(alert.severity)},
|
|
423
|
+
${sqlString(alert.category)},
|
|
424
|
+
${sqlString(alert.title)},
|
|
425
|
+
${sqlString(alert.message)},
|
|
426
|
+
${sqlString(EMPTY_METADATA_JSON)}
|
|
427
|
+
);
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
function deleteProviderSyncAlertsSql(providerId) {
|
|
431
|
+
return `
|
|
432
|
+
DELETE FROM alerts
|
|
433
|
+
WHERE provider_id = ${sqlString(providerId)}
|
|
434
|
+
AND category = 'provider-sync';
|
|
435
|
+
`;
|
|
436
|
+
}
|
|
437
|
+
function collectProviderAccountRefs(input) {
|
|
438
|
+
const refs = new Set();
|
|
439
|
+
const collect = (providerAccountRef) => {
|
|
440
|
+
if (providerAccountRef !== undefined) {
|
|
441
|
+
refs.add(providerAccountRef);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
for (const snapshot of input.snapshots.usage) {
|
|
445
|
+
collect(snapshot.providerAccountRef);
|
|
446
|
+
}
|
|
447
|
+
for (const snapshot of input.snapshots.billing) {
|
|
448
|
+
collect(snapshot.providerAccountRef);
|
|
449
|
+
}
|
|
450
|
+
for (const snapshot of input.snapshots.costEstimates) {
|
|
451
|
+
collect(snapshot.providerAccountRef);
|
|
452
|
+
}
|
|
453
|
+
return [...refs].sort();
|
|
454
|
+
}
|
|
455
|
+
function executeSqliteTransaction(dbPath, statements) {
|
|
456
|
+
if (statements.length === 0) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
executeSqliteSync(dbPath, ["BEGIN;", ...statements, "COMMIT;"].join("\n"));
|
|
460
|
+
}
|
|
461
|
+
async function executeSqlite(dbPath, sql) {
|
|
462
|
+
try {
|
|
463
|
+
executeSqliteWithCli(dbPath, sql);
|
|
464
|
+
}
|
|
465
|
+
catch (caught) {
|
|
466
|
+
if (!shouldFallbackToNodeSqlite(caught)) {
|
|
467
|
+
throw caught;
|
|
468
|
+
}
|
|
469
|
+
executeSqliteWithNodeSync(dbPath, sql);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function executeSqliteSync(dbPath, sql) {
|
|
473
|
+
try {
|
|
474
|
+
executeSqliteWithCli(dbPath, sql);
|
|
475
|
+
}
|
|
476
|
+
catch (caught) {
|
|
477
|
+
if (!shouldFallbackToNodeSqlite(caught)) {
|
|
478
|
+
throw caught;
|
|
479
|
+
}
|
|
480
|
+
executeSqliteWithNodeSync(dbPath, sql);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function querySqliteRowsSync(dbPath, sql) {
|
|
484
|
+
try {
|
|
485
|
+
return querySqliteRowsWithCli(dbPath, sql);
|
|
486
|
+
}
|
|
487
|
+
catch (caught) {
|
|
488
|
+
if (!shouldFallbackToNodeSqlite(caught)) {
|
|
489
|
+
throw caught;
|
|
490
|
+
}
|
|
491
|
+
return querySqliteRowsWithNodeSync(dbPath, sql);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function executeSqliteWithCli(dbPath, sql) {
|
|
495
|
+
execFileSync(resolveSqliteBin(), [dbPath], {
|
|
496
|
+
input: sqliteInput(sql),
|
|
497
|
+
encoding: "utf8",
|
|
498
|
+
maxBuffer: 1024 * 1024,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
function querySqliteRowsWithCli(dbPath, sql) {
|
|
502
|
+
const output = execFileSync(resolveSqliteBin(), ["-json", dbPath, sql], {
|
|
503
|
+
encoding: "utf8",
|
|
504
|
+
maxBuffer: 1024 * 1024,
|
|
505
|
+
}).trim();
|
|
506
|
+
return parseSqliteJsonRows(output);
|
|
507
|
+
}
|
|
508
|
+
function executeSqliteWithNodeSync(dbPath, sql) {
|
|
509
|
+
const database = createNodeSqliteDatabase(dbPath);
|
|
510
|
+
try {
|
|
511
|
+
database.exec(sqliteInput(sql));
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
database.close();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function querySqliteRowsWithNodeSync(dbPath, sql) {
|
|
518
|
+
const database = createNodeSqliteDatabase(dbPath);
|
|
519
|
+
try {
|
|
520
|
+
database.exec("PRAGMA foreign_keys = ON;");
|
|
521
|
+
return database.prepare(sql).all();
|
|
522
|
+
}
|
|
523
|
+
finally {
|
|
524
|
+
database.close();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function createNodeSqliteDatabase(dbPath) {
|
|
528
|
+
try {
|
|
529
|
+
const nodeSqlite = requireNodeModule("node:sqlite");
|
|
530
|
+
return new nodeSqlite.DatabaseSync(dbPath);
|
|
531
|
+
}
|
|
532
|
+
catch (caught) {
|
|
533
|
+
throw new Error(`SQLite is unavailable. Install sqlite3 and put it on PATH, set ${SQLITE_BIN_ENV_KEY}, or run MoneySiren with a Node.js version that includes node:sqlite. ${caught instanceof Error ? caught.message : "node:sqlite could not be loaded."}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function parseSqliteJsonRows(output) {
|
|
537
|
+
if (output.length === 0) {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
return JSON.parse(output);
|
|
541
|
+
}
|
|
542
|
+
function sqliteInput(sql) {
|
|
543
|
+
return `PRAGMA foreign_keys = ON;\n${sql.trim()}\n`;
|
|
544
|
+
}
|
|
545
|
+
function shouldFallbackToNodeSqlite(caught) {
|
|
546
|
+
if (process.env[SQLITE_BIN_ENV_KEY]?.trim()) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
const error = caught;
|
|
550
|
+
const message = typeof error.message === "string" ? error.message : "";
|
|
551
|
+
return error.code === "ENOENT" || /spawnSync .*ENOENT/i.test(message);
|
|
552
|
+
}
|
|
553
|
+
function normalizeDbPath(dbPath) {
|
|
554
|
+
const normalized = dbPath.trim();
|
|
555
|
+
if (normalized.length === 0) {
|
|
556
|
+
throw new Error("dbPath must not be blank.");
|
|
557
|
+
}
|
|
558
|
+
return normalized;
|
|
559
|
+
}
|
|
560
|
+
function providerIdFor(providerKey) {
|
|
561
|
+
return `provider:${providerKey}`;
|
|
562
|
+
}
|
|
563
|
+
function providerAccountIdFor(providerKey, providerAccountRef) {
|
|
564
|
+
return `provider-account:${providerKey}:${providerAccountDigest(providerAccountRef).slice(0, 32)}`;
|
|
565
|
+
}
|
|
566
|
+
function providerAccountDigest(providerAccountRef) {
|
|
567
|
+
return createHash("sha256").update(providerAccountRef).digest("hex");
|
|
568
|
+
}
|
|
569
|
+
function sqlProviderAccountId(providerKey, providerAccountRef) {
|
|
570
|
+
if (providerAccountRef === undefined) {
|
|
571
|
+
return "NULL";
|
|
572
|
+
}
|
|
573
|
+
return sqlString(providerAccountIdFor(providerKey, providerAccountRef));
|
|
574
|
+
}
|
|
575
|
+
function sqlString(value) {
|
|
576
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
577
|
+
}
|
|
578
|
+
function sqlNullableString(value) {
|
|
579
|
+
return value === undefined ? "NULL" : sqlString(value);
|
|
580
|
+
}
|
|
581
|
+
function sqlNumber(value) {
|
|
582
|
+
if (!Number.isFinite(value)) {
|
|
583
|
+
throw new Error("SQLite numeric value must be finite.");
|
|
584
|
+
}
|
|
585
|
+
return String(value);
|
|
586
|
+
}
|
|
587
|
+
function sqlInteger(value) {
|
|
588
|
+
if (!Number.isSafeInteger(value)) {
|
|
589
|
+
throw new Error("SQLite integer value must be a safe integer.");
|
|
590
|
+
}
|
|
591
|
+
return String(value);
|
|
592
|
+
}
|
|
593
|
+
function emptyMetadata(metadataJson) {
|
|
594
|
+
if (metadataJson !== EMPTY_METADATA_JSON) {
|
|
595
|
+
const parsed = JSON.parse(metadataJson);
|
|
596
|
+
assertSafeForPersistence(parsed);
|
|
597
|
+
}
|
|
598
|
+
return {};
|
|
599
|
+
}
|
|
600
|
+
function assertSafeForPersistence(value) {
|
|
601
|
+
if (Array.isArray(value)) {
|
|
602
|
+
for (const item of value) {
|
|
603
|
+
assertSafeForPersistence(item);
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (!isRecord(value)) {
|
|
608
|
+
if (typeof value === "string" && FORBIDDEN_STRING_PATTERN.test(value)) {
|
|
609
|
+
throw new Error("Sensitive provider value cannot be persisted.");
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
614
|
+
if (FORBIDDEN_KEY_PATTERN.test(key)) {
|
|
615
|
+
throw new Error(`Raw provider payload field cannot be persisted: ${key}`);
|
|
616
|
+
}
|
|
617
|
+
assertSafeForPersistence(nestedValue);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function isRecord(value) {
|
|
621
|
+
return typeof value === "object" && value !== null;
|
|
622
|
+
}
|
|
623
|
+
//# sourceMappingURL=local-store.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Migration {
|
|
2
|
+
id: string;
|
|
3
|
+
sql: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MigrationExecutor {
|
|
6
|
+
getAppliedMigrationIds(): Promise<readonly string[]>;
|
|
7
|
+
execute(sql: string): Promise<void>;
|
|
8
|
+
recordMigration(id: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface MigrationRunResult {
|
|
11
|
+
appliedMigrationIds: readonly string[];
|
|
12
|
+
skippedMigrationIds: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export declare const MIGRATIONS: readonly Migration[];
|
|
15
|
+
export declare function getPendingMigrations(appliedMigrationIds: readonly string[], migrations?: readonly Migration[]): readonly Migration[];
|
|
16
|
+
export declare function runMigrations(executor: MigrationExecutor, migrations?: readonly Migration[]): Promise<MigrationRunResult>;
|
|
17
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { INITIAL_SCHEMA_SQL, READ_MODEL_INDEX_SQL } from "./schema.js";
|
|
2
|
+
export const MIGRATIONS = [
|
|
3
|
+
{
|
|
4
|
+
id: "0001_init",
|
|
5
|
+
sql: INITIAL_SCHEMA_SQL,
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
id: "0002_read_model_indexes",
|
|
9
|
+
sql: READ_MODEL_INDEX_SQL,
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
export function getPendingMigrations(appliedMigrationIds, migrations = MIGRATIONS) {
|
|
13
|
+
const applied = new Set(appliedMigrationIds);
|
|
14
|
+
return migrations.filter((migration) => !applied.has(migration.id));
|
|
15
|
+
}
|
|
16
|
+
export async function runMigrations(executor, migrations = MIGRATIONS) {
|
|
17
|
+
const appliedMigrationIds = await executor.getAppliedMigrationIds();
|
|
18
|
+
const applied = new Set(appliedMigrationIds);
|
|
19
|
+
const pendingMigrations = getPendingMigrations(appliedMigrationIds, migrations);
|
|
20
|
+
const newlyAppliedMigrationIds = [];
|
|
21
|
+
for (const migration of pendingMigrations) {
|
|
22
|
+
await executor.execute(migration.sql);
|
|
23
|
+
await executor.recordMigration(migration.id);
|
|
24
|
+
newlyAppliedMigrationIds.push(migration.id);
|
|
25
|
+
applied.add(migration.id);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
appliedMigrationIds: newlyAppliedMigrationIds,
|
|
29
|
+
skippedMigrationIds: migrations
|
|
30
|
+
.filter((migration) => !newlyAppliedMigrationIds.includes(migration.id))
|
|
31
|
+
.filter((migration) => applied.has(migration.id))
|
|
32
|
+
.map((migration) => migration.id),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const REQUIRED_TABLES: readonly ["providers", "provider_accounts", "usage_snapshots", "billing_snapshots", "service_health_snapshots", "cost_estimates", "alerts", "report_runs"];
|
|
2
|
+
export type RequiredTable = (typeof REQUIRED_TABLES)[number];
|
|
3
|
+
export declare const INITIAL_SCHEMA_SQL: string;
|
|
4
|
+
export declare const READ_MODEL_INDEX_SQL: string;
|
|
5
|
+
//# sourceMappingURL=schema.d.ts.map
|