diff --git a/apps/web/__tests__/unit/developer-api-contract.test.ts b/apps/web/__tests__/unit/developer-api-contract.test.ts
index 4a2c65a0400..c016bf57cfb 100644
--- a/apps/web/__tests__/unit/developer-api-contract.test.ts
+++ b/apps/web/__tests__/unit/developer-api-contract.test.ts
@@ -115,6 +115,7 @@ import {
INFRA_RESOLUTION_TIERS,
INFRA_STORAGE_DOLLARS_PER_1000_MINUTES_PER_MONTH,
} from "@/lib/infra-pricing";
+import { getAppStorage } from "@/lib/infra-storage";
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS";
@@ -157,6 +158,11 @@ const rateLimit = (response: Response) => ({
retryAfter: response.headers.get("Retry-After"),
});
+const billingOf = async (billing: Record) => ({
+ ...billing,
+ stored: (await getAppStorage("app-1")).stored,
+});
+
beforeEach(() => {
vi.clearAllMocks();
mocks.keyRows = [{ appId: "app-1", environment: "development" }];
@@ -183,7 +189,9 @@ beforeEach(() => {
},
];
mocks.selected = [];
- mocks.billing.mockResolvedValue({ standing: { state: "active" } });
+ mocks.billing.mockImplementation(() =>
+ billingOf({ standing: { state: "active" } }),
+ );
mocks.createVideo.mockResolvedValue({
status: 200,
body: { videoId: "video-1" },
@@ -898,7 +906,9 @@ describe("usage", () => {
});
it("returns the fields the OpenAPI description requires", async () => {
- mocks.billing.mockResolvedValue({ standing: {}, upload: {}, storage: {} });
+ mocks.billing.mockImplementation(() =>
+ billingOf({ standing: {}, upload: {}, storage: {} }),
+ );
const schema = developerOpenApiSpec(ORIGIN).components.schemas.Usage;
const data = await usage();
diff --git a/apps/web/__tests__/unit/developer-cron-storage.test.ts b/apps/web/__tests__/unit/developer-cron-storage.test.ts
index ad67787a9fb..7bf720306e0 100644
--- a/apps/web/__tests__/unit/developer-cron-storage.test.ts
+++ b/apps/web/__tests__/unit/developer-cron-storage.test.ts
@@ -2,49 +2,96 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
selects: [] as Record[][],
+ page: [] as Record[],
+ accountsTable: { id: "id", appId: "appId" },
+ appsTable: { id: "id", deletedAt: "deletedAt" },
+ ledgerTable: {
+ accountId: "accountId",
+ referenceId: "referenceId",
+ referenceType: "referenceType",
+ },
+ snapshotsTable: { appId: "appId", snapshotDate: "snapshotDate" },
+ yesterdayBilled: null as Record[] | null,
+ markers: [] as unknown[],
getStorageUsageByApp: vi.fn(),
chargeStorageDay: vi.fn(),
- latestStorageCharge: vi.fn(),
+ latestStorageCharges: vi.fn(),
runDueAutoTopUps: vi.fn(),
sendInfraOutOfCreditEmail: vi.fn(),
}));
vi.mock("@cap/database", () => ({
db: () => {
+ let table: unknown;
+ const dateOf = (condition: unknown) => {
+ const match = JSON.stringify(condition).match(
+ /\{"eq":\["referenceId","(\d{4}-\d{2}-\d{2})"\]\}/,
+ );
+ return match?.[1] ?? null;
+ };
+ const read = (condition: unknown) => {
+ if (table === mocks.appsTable) {
+ return mocks.page.map((account) => ({ id: account.appId }));
+ }
+ if (table === mocks.accountsTable) {
+ mocks.page = mocks.selects[0] ?? [];
+ return mocks.page;
+ }
+ if (table === mocks.ledgerTable) {
+ if (dateOf(condition) === new Date().toISOString().slice(0, 10)) {
+ return mocks.selects[1] ?? [];
+ }
+ return (
+ mocks.yesterdayBilled ??
+ mocks.page.map((account) => ({ accountId: account.id }))
+ );
+ }
+ return [];
+ };
const chain = {
select: () => chain,
- from: () => chain,
- innerJoin: () => chain,
- where: async () => mocks.selects.shift() ?? [],
+ from: (next: unknown) => {
+ table = next;
+ return chain;
+ },
+ where: (condition: unknown) => {
+ const rows = Promise.resolve(read(condition));
+ return Object.assign(rows, {
+ orderBy: () => ({ limit: () => rows }),
+ });
+ },
};
return chain;
},
}));
vi.mock("@cap/database/schema", () => ({
- developerApps: { id: "id", deletedAt: "deletedAt" },
- developerCreditAccounts: { id: "id", appId: "appId" },
- developerCreditTransactions: {
- accountId: "accountId",
- referenceId: "referenceId",
- referenceType: "referenceType",
- },
+ developerApps: mocks.appsTable,
+ developerCreditAccounts: mocks.accountsTable,
+ developerCreditTransactions: mocks.ledgerTable,
+ developerDailyStorageSnapshots: mocks.snapshotsTable,
}));
vi.mock("drizzle-orm", () => ({
and: (...args: unknown[]) => args,
+ asc: (a: unknown) => ({ asc: a }),
+ gt: (a: unknown, b: unknown) => ({ gt: [a, b] }),
eq: (a: unknown, b: unknown) => ({ eq: [a, b] }),
inArray: (a: unknown, b: unknown) => ({ inArray: [a, b] }),
isNull: (a: unknown) => ({ isNull: a }),
+ isNotNull: (a: unknown) => ({ isNotNull: a }),
}));
vi.mock("@/lib/infra-storage", () => ({
getStorageUsageByApp: mocks.getStorageUsageByApp,
}));
vi.mock("@/lib/infra-storage-billing", () => ({
chargeStorageDay: mocks.chargeStorageDay,
+ recordStorageDayVisits: async ({ appIds }: { appIds: string[] }) => {
+ mocks.markers.push(...appIds);
+ },
}));
vi.mock("@/lib/infra-standing", () => ({
INFRA_GRACE_DAYS: 7,
STORAGE_DAY_REFERENCE_TYPE: "storage_day",
- latestStorageCharge: mocks.latestStorageCharge,
+ latestStorageCharges: mocks.latestStorageCharges,
}));
vi.mock("@/lib/infra-auto-top-up", () => ({
runDueAutoTopUps: mocks.runDueAutoTopUps,
@@ -90,17 +137,20 @@ const authorized = () => run(`Bearer ${CRON_SECRET}`);
beforeEach(() => {
process.env.CRON_SECRET = CRON_SECRET;
mocks.selects = [];
+ mocks.page = [];
+ mocks.yesterdayBilled = null;
+ mocks.markers = [];
for (const mock of [
mocks.getStorageUsageByApp,
mocks.chargeStorageDay,
- mocks.latestStorageCharge,
+ mocks.latestStorageCharges,
mocks.runDueAutoTopUps,
mocks.sendInfraOutOfCreditEmail,
]) {
mock.mockReset();
}
mocks.runDueAutoTopUps.mockResolvedValue(NO_TOP_UPS);
- mocks.latestStorageCharge.mockResolvedValue(null);
+ mocks.latestStorageCharges.mockResolvedValue(new Map());
mocks.chargeStorageDay.mockResolvedValue(billed());
});
@@ -191,6 +241,34 @@ describe("developer-storage cron job", () => {
expect(body.appsProcessed).toBe(0);
expect(mocks.chargeStorageDay).not.toHaveBeenCalled();
+ expect(mocks.markers).toEqual(["app-1"]);
+ });
+
+ it("looks up the last charge of every idle account in one read", async () => {
+ mocks.selects = [
+ [
+ { id: "account-1", appId: "app-1" },
+ { id: "account-2", appId: "app-2" },
+ { id: "account-3", appId: "app-3" },
+ ],
+ [],
+ ];
+ mocks.getStorageUsageByApp.mockResolvedValue(
+ new Map([
+ ["app-1", usage(0)],
+ ["app-2", usage(28)],
+ ["app-3", usage(0)],
+ ]),
+ );
+
+ const { body } = await authorized();
+
+ expect(body).toMatchObject({ complete: true, appsProcessed: 1 });
+ expect(mocks.latestStorageCharges).toHaveBeenCalledTimes(1);
+ expect(mocks.latestStorageCharges).toHaveBeenCalledWith(expect.anything(), [
+ "account-1",
+ "account-3",
+ ]);
});
it("still settles an app that stores nothing now but is short from before", async () => {
@@ -198,14 +276,24 @@ describe("developer-storage cron job", () => {
mocks.getStorageUsageByApp.mockResolvedValue(
new Map([["app-1", usage(0)]]),
);
- mocks.latestStorageCharge.mockResolvedValue({
- createdAt: new Date(),
- owedMicroCredits: 28,
- shortSince: new Date(),
- });
+ mocks.latestStorageCharges.mockResolvedValue(
+ new Map([
+ [
+ "account-1",
+ {
+ createdAt: new Date(),
+ owedMicroCredits: 28,
+ shortSince: new Date(),
+ },
+ ],
+ ]),
+ );
await authorized();
+ expect(mocks.latestStorageCharges).toHaveBeenCalledWith(expect.anything(), [
+ "account-1",
+ ]);
expect(mocks.chargeStorageDay).toHaveBeenCalledTimes(1);
});
diff --git a/apps/web/__tests__/unit/developer-usage-counts.test.ts b/apps/web/__tests__/unit/developer-usage-counts.test.ts
index 1b3373f3953..50ebc8eac9a 100644
--- a/apps/web/__tests__/unit/developer-usage-counts.test.ts
+++ b/apps/web/__tests__/unit/developer-usage-counts.test.ts
@@ -87,6 +87,7 @@ vi.mock("@/lib/infra-standing", async (original) => ({
import { app } from "@/app/api/developer/v1/[...route]/usage";
import { storageUsage } from "@/lib/developer-pricing";
+import { getAppStorage } from "@/lib/infra-storage";
const usage = async () => {
const response = await app.request("/", {
@@ -99,9 +100,10 @@ const usage = async () => {
beforeEach(() => {
mocks.balanceMicroCredits = 2_999_901;
mocks.videos = [];
- mocks.billing.mockResolvedValue({
+ mocks.billing.mockImplementation(async (appId: string) => ({
standing: { state: "active", pausesAt: null },
usage: storageUsage([]),
+ stored: (await getAppStorage(appId)).stored,
admission: {
allowed: true,
reason: null,
@@ -109,7 +111,7 @@ beforeEach(() => {
shortfallMicroCredits: 0,
message: null,
},
- });
+ }));
});
describe("GET /usage totals", () => {
diff --git a/apps/web/__tests__/unit/developer-usage-route.test.ts b/apps/web/__tests__/unit/developer-usage-route.test.ts
index 7054c4816ff..ca34d52280c 100644
--- a/apps/web/__tests__/unit/developer-usage-route.test.ts
+++ b/apps/web/__tests__/unit/developer-usage-route.test.ts
@@ -33,16 +33,15 @@ vi.mock("@/lib/developer-key-hash", () => ({
hashKey: async (key: string) => `hash:${key}`,
}));
vi.mock("@/lib/infra-standing", () => ({
- getInfraBilling: async () => ({}),
+ getInfraBilling: async () => ({
+ stored: { videoCount: 2, totalMinutes: 12.5 },
+ }),
infraBillingJson: () => ({
standing: { state: "active", pausesAt: null },
upload: { allowed: true },
}),
infraStorageJson: () => ({ dailyMicroCredits: 0 }),
}));
-vi.mock("@/lib/infra-storage", () => ({
- getAppStorageUsage: async () => ({ videoCount: 2, totalMinutes: 12.5 }),
-}));
vi.mock("@/lib/developer-transcript-usage", () => ({
getSpeechUsage: mocks.speech,
}));
diff --git a/apps/web/__tests__/unit/developer-video-upload.test.ts b/apps/web/__tests__/unit/developer-video-upload.test.ts
index 251fbecce06..1465c6f768c 100644
--- a/apps/web/__tests__/unit/developer-video-upload.test.ts
+++ b/apps/web/__tests__/unit/developer-video-upload.test.ts
@@ -99,7 +99,7 @@ vi.mock("@/lib/infra-storage", async () => {
const { billableVideo, storageUsage } = await import(
"@/lib/developer-pricing"
);
- return {
+ const storage = {
getAppStorageUsage: async (
appId: string,
newVideo?: Parameters[0][number]["video"],
@@ -129,6 +129,25 @@ vi.mock("@/lib/infra-storage", async () => {
return storageUsage(entries);
},
};
+ return {
+ ...storage,
+ getAppStorage: async (
+ ...[appId, newVideo, executor, options]: Parameters<
+ typeof storage.getAppStorageUsage
+ >
+ ) => ({
+ stored: await storage.getAppStorageUsage(
+ appId,
+ newVideo,
+ executor,
+ options,
+ ),
+ counted: await storage.getAppStorageUsage(appId, newVideo, executor, {
+ ...options,
+ includeProcessing: true,
+ }),
+ }),
+ };
});
vi.mock("@/lib/developer-transcript-storage", () => ({
diff --git a/apps/web/__tests__/unit/infra-auto-top-up-sweep.test.ts b/apps/web/__tests__/unit/infra-auto-top-up-sweep.test.ts
new file mode 100644
index 00000000000..c92274936df
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-auto-top-up-sweep.test.ts
@@ -0,0 +1,240 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+type Account = { id: string; lastAttemptAt: Date | null };
+
+const state = vi.hoisted(() => ({
+ accounts: [] as { id: string; lastAttemptAt: Date | null }[],
+ pages: [] as {
+ sql: string;
+ params: unknown[];
+ after: { key: string; id: string } | null;
+ limit: number;
+ }[],
+ reads: [] as string[],
+ msPerAccount: 0,
+ other: [] as { sql: string; params: unknown[] }[],
+}));
+
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const NEVER = "1970-01-01 00:00:00";
+ const keyOf = (account: { lastAttemptAt: Date | null }) =>
+ account.lastAttemptAt
+ ? account.lastAttemptAt.toISOString().slice(0, 19).replace("T", " ")
+ : NEVER;
+ const client = drizzle(async (sql, params) => {
+ if (sql.startsWith("select `developer_credit_accounts`.`id`, cast(")) {
+ const limit = Number(params.at(-1));
+ const keyset = sql.includes(" or (");
+ const after = keyset
+ ? { key: String(params.at(-4)), id: String(params.at(-2)) }
+ : null;
+ const cutoff = String(
+ params.find(
+ (param) =>
+ typeof param === "string" &&
+ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/.test(param),
+ ),
+ );
+ state.pages.push({ sql, params, after, limit });
+ const rows = state.accounts
+ .map((account) => ({ id: account.id, key: keyOf(account) }))
+ .filter((row) => row.key <= cutoff)
+ .filter(
+ (row) =>
+ !after ||
+ row.key > after.key ||
+ (row.key === after.key && row.id > after.id),
+ )
+ .sort((a, b) =>
+ a.key === b.key ? a.id.localeCompare(b.id) : a.key < b.key ? -1 : 1,
+ )
+ .slice(0, limit);
+ return { rows: rows.map((row) => [row.id, row.key]) };
+ }
+ if (sql.startsWith("select `developer_credit_accounts`.`id` from")) {
+ const limit = Number(params.at(-1));
+ const cursor = sql.includes("`id` > ?") ? String(params.at(-2)) : null;
+ state.pages.push({
+ sql,
+ params,
+ after: cursor ? { key: "", id: cursor } : null,
+ limit,
+ });
+ return {
+ rows: state.accounts
+ .filter((account) => cursor === null || account.id > cursor)
+ .slice(0, limit)
+ .map((account) => [account.id]),
+ };
+ }
+ if (sql.includes("`autoTopUpAmountCents`")) {
+ state.reads.push(String(params[0]));
+ vi.setSystemTime(Date.now() + state.msPerAccount);
+ return { rows: [] };
+ }
+ state.other.push({ sql, params });
+ return { rows: [] };
+ });
+ return { db: () => client };
+});
+vi.mock("@cap/database/helpers", () => ({
+ nanoId: () => "row",
+ nanoIdLength: 15,
+}));
+vi.mock("@cap/env", () => ({
+ buildEnv: { NEXT_PUBLIC_IS_CAP: "true" },
+ serverEnv: () => ({}),
+}));
+vi.mock("@cap/utils", () => ({ stripe: () => ({}) }));
+vi.mock("@/lib/infra-billing-emails", () => ({
+ sendInfraTopUpFailedEmail: vi.fn(),
+}));
+
+import {
+ autoTopUpRetryCutoff,
+ getAutoTopUpFailure,
+ runDueAutoTopUps,
+} from "@/lib/infra-auto-top-up";
+
+const NOW = new Date("2026-11-13T06:41:00Z");
+const HOUR_MS = 60 * 60 * 1000;
+
+const accounts = (
+ count: number,
+ from = 0,
+ lastAttemptAt: Date | null = null,
+): Account[] =>
+ Array.from({ length: count }, (_, index) => ({
+ id: `account-${String(from + index).padStart(4, "0")}`,
+ lastAttemptAt,
+ }));
+
+beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(NOW);
+ state.accounts = accounts(450);
+ state.pages = [];
+ state.reads = [];
+ state.msPerAccount = 0;
+ state.other = [];
+});
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+describe("auto top-up sweep selection", () => {
+ it("pages through due accounts by last attempt, then id", async () => {
+ await runDueAutoTopUps(NOW);
+
+ expect(state.pages.map(({ after, limit }) => ({ after, limit }))).toEqual([
+ { after: null, limit: 200 },
+ { after: { key: "1970-01-01 00:00:00", id: "account-0199" }, limit: 200 },
+ { after: { key: "1970-01-01 00:00:00", id: "account-0399" }, limit: 200 },
+ ]);
+ expect(state.reads).toHaveLength(450);
+ });
+
+ it("asks the database for enabled accounts under their threshold after held credit, not tried since the retry cutoff", async () => {
+ state.accounts = [];
+
+ await runDueAutoTopUps(NOW);
+
+ expect(state.pages).toHaveLength(1);
+ const { sql, params } = state.pages[0] ?? { sql: "", params: [] };
+ expect(sql).toContain("`autoTopUpEnabled` = ?");
+ expect(sql).toContain("`stripePaymentMethodId` is not null");
+ expect(sql).toContain(
+ "left join (select `appId`, COALESCE(SUM(`reservedMicroCredits`), 0) as `heldMicroCredits` from `developer_transcripts` where `developer_transcripts`.`status` = ? group by `developer_transcripts`.`appId`) `held` on `held`.`appId` = `developer_credit_accounts`.`appId`",
+ );
+ expect(sql).toContain(
+ "`balanceMicroCredits` < `developer_credit_accounts`.`autoTopUpThresholdMicroCredits` + coalesce(`heldMicroCredits`, 0)",
+ );
+ expect(sql).toContain(
+ "(select max(`developer_credit_transactions`.`createdAt`) from `developer_credit_transactions` where `developer_credit_transactions`.`accountId` = `developer_credit_accounts`.`id` and `developer_credit_transactions`.`type` in (?, ?, ?) and `developer_credit_transactions`.`referenceType` in (?, ?, ?))",
+ );
+ expect(sql).toMatch(
+ /order by attemptKey, `developer_credit_accounts`\.`id` asc limit \?$/,
+ );
+ expect(params).toContain(
+ autoTopUpRetryCutoff(NOW).toISOString().slice(0, 23).replace("T", " "),
+ );
+ expect(autoTopUpRetryCutoff(NOW)).toEqual(
+ new Date(NOW.getTime() - 24 * HOUR_MS),
+ );
+ });
+
+ it("tries the least recently tried account first", async () => {
+ state.accounts = [
+ {
+ id: "account-0001",
+ lastAttemptAt: new Date(NOW.getTime() - 26 * HOUR_MS),
+ },
+ { id: "account-0002", lastAttemptAt: null },
+ {
+ id: "account-0003",
+ lastAttemptAt: new Date(NOW.getTime() - 30 * HOUR_MS),
+ },
+ {
+ id: "account-0004",
+ lastAttemptAt: new Date(NOW.getTime() - 2 * HOUR_MS),
+ },
+ { id: "account-0005", lastAttemptAt: null },
+ ];
+
+ await runDueAutoTopUps(NOW);
+
+ expect(state.reads).toEqual([
+ "account-0002",
+ "account-0005",
+ "account-0003",
+ "account-0001",
+ ]);
+ });
+
+ it("reaches 50 due accounts in one run behind 300 accounts tried inside the retry window", async () => {
+ const tried = accounts(300, 0, new Date(NOW.getTime() - HOUR_MS));
+ const due = accounts(50, 300);
+ state.accounts = [...tried, ...due];
+ state.msPerAccount = 250;
+
+ await runDueAutoTopUps(NOW, { until: NOW.getTime() + 60_000 });
+
+ expect(state.reads).toEqual(due.map((account) => account.id));
+ });
+
+ it("stops between accounts when its time is up", async () => {
+ state.msPerAccount = 1000;
+
+ await runDueAutoTopUps(NOW, { until: NOW.getTime() + 10_500 });
+
+ expect(state.reads).toHaveLength(11);
+ expect(state.pages).toHaveLength(1);
+ });
+});
+
+describe("last auto top-up lookup", () => {
+ it("reads the account's top-up rows by type, so the account and type index serves it", async () => {
+ expect(await getAutoTopUpFailure("account-0001")).toBeNull();
+
+ expect(state.other).toHaveLength(1);
+ const [{ sql, params }] = state.other as [
+ { sql: string; params: unknown[] },
+ ];
+ expect(sql).toContain(
+ "where (`developer_credit_transactions`.`accountId` = ? and `developer_credit_transactions`.`type` in (?, ?) and `developer_credit_transactions`.`referenceType` in (?, ?))",
+ );
+ expect(sql).toContain(
+ "order by `developer_credit_transactions`.`createdAt` desc limit ?",
+ );
+ expect(params).toEqual([
+ "account-0001",
+ "topup",
+ "topup_failed",
+ "auto_top_up",
+ "auto_top_up_failed",
+ 1,
+ ]);
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-auto-top-up.test.ts b/apps/web/__tests__/unit/infra-auto-top-up.test.ts
index 151022dd457..c4f416818db 100644
--- a/apps/web/__tests__/unit/infra-auto-top-up.test.ts
+++ b/apps/web/__tests__/unit/infra-auto-top-up.test.ts
@@ -684,6 +684,46 @@ describe("auto top-up sweep", () => {
failed: 1,
});
});
+
+ it("does not read an account that is not due", async () => {
+ ledger.account.balanceMicroCredits =
+ ledger.account.autoTopUpThresholdMicroCredits;
+ const select = vi.spyOn(fakeDb, "select");
+
+ expect(await runDueAutoTopUps(now())).toEqual({
+ attempted: 0,
+ succeeded: 0,
+ failed: 0,
+ });
+
+ expect(
+ select.mock.calls.filter(([fields]) => "autoTopUpAmountCents" in fields),
+ ).toHaveLength(0);
+ expect(mocks.createPaymentIntent).not.toHaveBeenCalled();
+ select.mockRestore();
+ });
+
+ it("still tops up an account that only held credit puts under its threshold", async () => {
+ ledger.account.balanceMicroCredits =
+ ledger.account.autoTopUpThresholdMicroCredits + 10;
+ ledger.heldMicroCredits = 11;
+
+ expect(await runDueAutoTopUps(now())).toEqual({
+ attempted: 1,
+ succeeded: 1,
+ failed: 0,
+ });
+ });
+
+ it("starts no top-up once its time is up", async () => {
+ expect(await runDueAutoTopUps(now(), { until: Date.now() - 1 })).toEqual({
+ attempted: 0,
+ succeeded: 0,
+ failed: 0,
+ });
+
+ expect(mocks.createPaymentIntent).not.toHaveBeenCalled();
+ });
});
describe("billing emails", () => {
diff --git a/apps/web/__tests__/unit/infra-billing-fake-db.ts b/apps/web/__tests__/unit/infra-billing-fake-db.ts
index 69b9a4766f8..817fdfcaedf 100644
--- a/apps/web/__tests__/unit/infra-billing-fake-db.ts
+++ b/apps/web/__tests__/unit/infra-billing-fake-db.ts
@@ -5,7 +5,7 @@ import {
developerTranscripts,
developerVideos,
} from "@cap/database/schema";
-import type { SQL } from "drizzle-orm";
+import { type SQL, sql } from "drizzle-orm";
import { MySqlDialect } from "drizzle-orm/mysql-core";
const dialect = new MySqlDialect();
@@ -102,6 +102,25 @@ const newestFirst = (rows: LedgerRow[]) =>
ledger.rows.indexOf(b) - ledger.rows.indexOf(a),
);
+const byId = (a: LedgerRow, b: LedgerRow) =>
+ a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
+
+const latestWritten = (rows: LedgerRow[]) =>
+ [...rows].sort(
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime() || byId(a, b),
+ );
+
+const latestStorageDayRows = () => {
+ const days = storageRows().filter(
+ (row) => row.referenceType === "storage_day" && row.referenceId,
+ );
+ const latestDay = days
+ .map((row) => String(row.referenceId))
+ .sort()
+ .at(-1);
+ return days.filter((row) => row.referenceId === latestDay).sort(byId);
+};
+
function runSelect(query: Query): Record[] {
const keys = Object.keys(query.fields).sort().join(",");
const rendered = query.where
@@ -111,6 +130,41 @@ function runSelect(query: Query): Record[] {
if (query.table === developerCreditAccounts) {
const { exists, ...account } = ledger.account;
if (!exists) return [];
+ if (keys === "attemptKey,id") {
+ const cutoff = rendered.params.find(
+ (param) =>
+ typeof param === "string" &&
+ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/.test(param),
+ );
+ const attempts = ledger.rows
+ .filter((row) =>
+ ["auto_top_up", "auto_top_up_failed", "auto_top_up_pending"].includes(
+ String(row.referenceType),
+ ),
+ )
+ .map((row) => row.createdAt.getTime());
+ const lastAttempt = attempts.length > 0 ? Math.max(...attempts) : null;
+ const due =
+ account.autoTopUpEnabled &&
+ account.stripePaymentMethodId &&
+ account.balanceMicroCredits <
+ account.autoTopUpThresholdMicroCredits + ledger.heldMicroCredits &&
+ (lastAttempt === null || lastAttempt <= asTime(cutoff));
+ return due
+ ? [
+ {
+ id: account.id,
+ attemptKey:
+ lastAttempt === null
+ ? "1970-01-01 00:00:00"
+ : new Date(lastAttempt)
+ .toISOString()
+ .slice(0, 19)
+ .replace("T", " "),
+ },
+ ]
+ : [];
+ }
if (
keys === "id" &&
/`id` = \?$/.test(rendered.sql) &&
@@ -124,7 +178,8 @@ function runSelect(query: Query): Record[] {
!(
account.autoTopUpEnabled &&
account.stripePaymentMethodId &&
- account.balanceMicroCredits < account.autoTopUpThresholdMicroCredits
+ account.balanceMicroCredits <
+ account.autoTopUpThresholdMicroCredits + ledger.heldMicroCredits
)
) {
return [];
@@ -134,7 +189,14 @@ function runSelect(query: Query): Record[] {
if (query.table === developerCreditTransactions) {
if (keys === "createdAt,metadata") {
- return newestFirst(storageRows()).slice(0, 1);
+ return rendered.sql.includes(
+ "`developer_credit_transactions`.`referenceId` <= ?",
+ )
+ ? latestStorageDayRows().slice(0, 1)
+ : latestWritten(storageRows()).slice(0, 1);
+ }
+ if (keys === "accountId,createdAt,id,metadata") {
+ return latestStorageDayRows();
}
if (keys === "metadata") {
const since = asTime(rendered.params.at(-1));
@@ -228,12 +290,19 @@ function select(fields: Record) {
return chain;
},
innerJoin: () => chain,
+ leftJoin: () => chain,
where(condition: SQL | undefined) {
query.where = condition;
const rows = Promise.resolve().then(() => runSelect(query));
const result = Object.assign(rows, {
orderBy: () => result,
- groupBy: () => result,
+ groupBy: () =>
+ Object.assign(result, {
+ as: (alias: string) => ({
+ appId: sql.raw(`\`${alias}\`.\`appId\``),
+ heldMicroCredits: sql.raw("`heldMicroCredits`"),
+ }),
+ }),
limit: () => result,
for: () => result,
});
diff --git a/apps/web/__tests__/unit/infra-console-round-trips.test.ts b/apps/web/__tests__/unit/infra-console-round-trips.test.ts
new file mode 100644
index 00000000000..07c2bd9b69b
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-console-round-trips.test.ts
@@ -0,0 +1,263 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const state = vi.hoisted(() => ({
+ queries: [] as string[],
+ appId: "app-1",
+}));
+
+vi.mock("server-only", () => ({}));
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const CREATED = "2026-11-01 00:00:00";
+ const accountValues: Record = {
+ id: "account-1",
+ appId: "app-1",
+ ownerId: "user-1",
+ balanceMicroCredits: 5_000_000,
+ stripeCustomerId: null,
+ stripePaymentMethodId: null,
+ autoTopUpEnabled: 0,
+ autoTopUpThresholdMicroCredits: 0,
+ autoTopUpAmountCents: 0,
+ createdAt: CREATED,
+ updatedAt: CREATED,
+ };
+ const appValues: Record[] = [
+ { id: "app-1", name: "First app", environment: "production" },
+ { id: "app-2", name: "Second app", environment: "development" },
+ ].map((app) => ({
+ ...app,
+ ownerId: "user-1",
+ logoUrl: null,
+ deletedAt: null,
+ createdAt: CREATED,
+ updatedAt: CREATED,
+ }));
+ const selected = (sql: string) =>
+ sql
+ .slice("select ".length, sql.indexOf(" from "))
+ .split(", ")
+ .map((column) => column.replaceAll("`", "").split(".").at(-1) ?? "");
+ const client = drizzle(async (sql) => {
+ state.queries.push(sql);
+ if (sql.includes("from `developer_apps`")) {
+ return {
+ rows: appValues.map((app) => selected(sql).map((name) => app[name])),
+ };
+ }
+ if (
+ sql.includes("from `developer_credit_accounts`") &&
+ !sql.includes("select COALESCE")
+ ) {
+ return { rows: [selected(sql).map((name) => accountValues[name])] };
+ }
+ if (sql.includes("COALESCE(SUM(") && !sql.includes("group by")) {
+ return { rows: [[0]] };
+ }
+ if (sql.startsWith("select count(*)")) return { rows: [[0]] };
+ return { rows: [] };
+ });
+ return { db: () => client };
+});
+vi.mock("@cap/database/auth/session", () => ({
+ getCurrentUser: async () => ({
+ id: "user-1",
+ email: "owner@example.com",
+ activeOrganizationId: null,
+ }),
+}));
+vi.mock("@cap/env", () => ({
+ buildEnv: { NEXT_PUBLIC_IS_CAP: "true" },
+ serverEnv: () => ({}),
+ NODE_ENV: "production",
+}));
+vi.mock("next/font/google", () => {
+ const font = () => ({ className: "", variable: "", style: {} });
+ return { Geist: font, Geist_Mono: font };
+});
+vi.mock("next/headers", () => ({
+ cookies: async () => ({ get: () => undefined }),
+ headers: async () => new Headers(),
+}));
+vi.mock("next/navigation", () => ({
+ notFound: () => {
+ throw new Error("notFound");
+ },
+ redirect: (to: string) => {
+ throw new Error(`redirect ${to}`);
+ },
+ usePathname: () => "/dashboard/infra/app-1",
+ useRouter: () => ({}),
+}));
+vi.mock("@/lib/infra-access", () => ({ canUseInfra: () => true }));
+vi.mock("@/app/api/utils", async () => {
+ const { createMiddleware } = await import("hono/factory");
+ return {
+ withDeveloperSecretAuth: createMiddleware(async (c, next) => {
+ c.set("developerAppId", state.appId);
+ await next();
+ }),
+ };
+});
+
+import InfraBillingPage from "@/app/(infra)/dashboard/infra/[appId]/billing/page";
+import InfraKeysPage from "@/app/(infra)/dashboard/infra/[appId]/keys/page";
+import InfraAppLayout from "@/app/(infra)/dashboard/infra/[appId]/layout";
+import InfraOverviewPage from "@/app/(infra)/dashboard/infra/[appId]/page";
+import InfraSettingsPage from "@/app/(infra)/dashboard/infra/[appId]/settings/page";
+import InfraUsagePage from "@/app/(infra)/dashboard/infra/[appId]/usage/page";
+import { app as usageApi } from "@/app/api/developer/v1/[...route]/usage";
+import { getInfraBilling, getInfraUploadAdmission } from "@/lib/infra-standing";
+
+const params = { params: Promise.resolve({ appId: "app-1" }) };
+
+const storageAggregates = () =>
+ state.queries.filter(
+ (sql) => sql.includes("from `developer_videos`") && sql.includes("SUM("),
+ );
+const reads = (table: string) =>
+ state.queries.filter((sql) => sql.includes(`from \`${table}\``));
+
+type Element = { props: Record };
+
+beforeEach(() => {
+ state.queries = [];
+});
+
+describe("storage totals are computed once per request", () => {
+ it("on the console overview", async () => {
+ await InfraOverviewPage(params);
+
+ expect(storageAggregates()).toHaveLength(1);
+ expect(state.queries).toHaveLength(11);
+ });
+
+ it("on the console usage page", async () => {
+ await InfraUsagePage(params);
+
+ expect(storageAggregates()).toHaveLength(1);
+ });
+
+ it("on the console billing page", async () => {
+ await InfraBillingPage({ ...params, searchParams: Promise.resolve({}) });
+
+ expect(storageAggregates()).toHaveLength(1);
+ });
+
+ it("on GET /usage", async () => {
+ const response = await usageApi.request("/");
+
+ expect(response.status).toBe(200);
+ expect(storageAggregates()).toHaveLength(1);
+ });
+
+ it("and not at all when an admission is given the totals it already has", async () => {
+ const billing = await getInfraBilling("app-1");
+ state.queries = [];
+
+ const admission = await getInfraUploadAdmission(
+ "app-1",
+ undefined,
+ undefined,
+ { storage: { stored: billing.stored, counted: billing.usage } },
+ );
+
+ expect(storageAggregates()).toHaveLength(0);
+ expect(admission).toEqual(billing.admission);
+ });
+});
+
+describe("the billing page lists the ledger without counting it", () => {
+ const counts = () =>
+ state.queries.filter((sql) => sql.startsWith("select count("));
+ const ledgerReads = () =>
+ reads("developer_credit_transactions").filter((sql) =>
+ sql.includes("inner join `developer_credit_accounts`"),
+ );
+
+ it("reads one page and no total", async () => {
+ const page = (await InfraBillingPage({
+ ...params,
+ searchParams: Promise.resolve({}),
+ })) as Element;
+
+ expect(ledgerReads()).toHaveLength(1);
+ expect(ledgerReads()[0]).toContain("limit ?");
+ expect(ledgerReads()[0]).not.toContain("offset");
+ expect(counts()).toHaveLength(0);
+ expect(page.props).toMatchObject({
+ transactions: [],
+ ledgerPage: { newer: null, older: null },
+ });
+ expect(page.props).not.toHaveProperty("totalTransactions");
+ });
+
+ it("starts from the cursor in the URL", async () => {
+ await InfraBillingPage({
+ ...params,
+ searchParams: Promise.resolve({ before: "1794461929000.abc123" }),
+ });
+
+ expect(ledgerReads()).toHaveLength(1);
+ expect(ledgerReads()[0]).toContain("`createdAt` < ?");
+ expect(counts()).toHaveLength(0);
+ });
+});
+
+describe("the console loads only what a page shows", () => {
+ it("reads the app list and the open app's account for the shell, nothing else", async () => {
+ const layout = (await InfraAppLayout({
+ children: null,
+ ...params,
+ })) as Element;
+
+ expect(state.queries).toHaveLength(2);
+ expect(reads("developer_apps")).toHaveLength(1);
+ expect(reads("developer_credit_accounts")).toHaveLength(1);
+ expect(layout.props.balanceMicroCredits).toBe(5_000_000);
+ const shell = layout.props.children as Element;
+ expect(shell.props).toMatchObject({
+ appId: "app-1",
+ balanceMicroCredits: 5_000_000,
+ apps: [
+ { id: "app-1", name: "First app", environment: "production" },
+ { id: "app-2", name: "Second app", environment: "development" },
+ ],
+ });
+ });
+
+ it("never loads domains, other apps' keys or a video count for the overview", async () => {
+ await InfraOverviewPage(params);
+
+ expect(reads("developer_app_domains")).toHaveLength(0);
+ expect(reads("developer_api_keys")).toHaveLength(1);
+ expect(reads("developer_api_keys")[0]).toContain("`appId` = ?");
+ expect(
+ state.queries.filter((sql) => sql.includes("count(*)")),
+ ).toHaveLength(0);
+ });
+
+ it("reads one app's keys for the keys page", async () => {
+ const page = (await InfraKeysPage(params)) as Element;
+
+ expect(state.queries).toHaveLength(3);
+ expect(reads("developer_api_keys")[0]).toContain("`appId` = ?");
+ expect(page.props).toEqual({ appId: "app-1", secretKeyPrefix: null });
+ });
+
+ it("counts videos only on the settings page, and only for that app", async () => {
+ const page = (await InfraSettingsPage(params)) as Element;
+
+ expect(state.queries).toHaveLength(3);
+ const [count] = reads("developer_videos");
+ expect(count).toContain("count(*)");
+ expect(count).toContain("`appId` = ?");
+ expect(page.props).toMatchObject({
+ appId: "app-1",
+ name: "First app",
+ environment: "production",
+ balanceMicroCredits: 5_000_000,
+ videoCount: 0,
+ });
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-ledger-paging.test.ts b/apps/web/__tests__/unit/infra-ledger-paging.test.ts
new file mode 100644
index 00000000000..a24dac2170f
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-ledger-paging.test.ts
@@ -0,0 +1,205 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+type Row = { id: string; createdAt: Date; amountMicroCredits: number };
+
+const state = vi.hoisted(() => ({
+ rows: [] as Row[],
+ queries: [] as string[],
+}));
+
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const toDate = (value: unknown) =>
+ new Date(`${String(value).replace(" ", "T")}Z`);
+ const stamp = (date: Date) =>
+ date.toISOString().slice(0, 19).replace("T", " ");
+ const client = drizzle(async (sql, params) => {
+ state.queries.push(sql);
+ const ascending = sql.includes(
+ "order by `developer_credit_transactions`.`createdAt` asc",
+ );
+ const sorted = [...state.rows].sort(
+ (a, b) =>
+ a.createdAt.getTime() - b.createdAt.getTime() ||
+ a.id.localeCompare(b.id),
+ );
+ if (!ascending) sorted.reverse();
+ let rest = params.slice(1);
+ let matching = sorted;
+ if (sql.includes("`createdAt` < ?") || sql.includes("`createdAt` > ?")) {
+ const at = toDate(rest[0]).getTime();
+ const id = String(rest[2]);
+ rest = rest.slice(3);
+ matching = sorted.filter((row) => {
+ const time = row.createdAt.getTime();
+ return ascending
+ ? time > at || (time === at && row.id > id)
+ : time < at || (time === at && row.id < id);
+ });
+ }
+ const limit = Number(rest[0]);
+ const offset = sql.includes("offset ?") ? Number(rest[1]) : 0;
+ return {
+ rows: matching
+ .slice(offset, offset + limit)
+ .map((row) => [
+ row.id,
+ "transcription",
+ row.amountMicroCredits,
+ 0,
+ null,
+ null,
+ null,
+ stamp(row.createdAt),
+ ]),
+ };
+ });
+ return { db: () => client };
+});
+
+import { LEDGER_PAGE_SIZE } from "@/lib/infra-ledger";
+import {
+ formatLedgerCursor,
+ listAppLedger,
+ listAppLedgerPage,
+ parseLedgerCursor,
+} from "@/lib/infra-ledger-store";
+
+const START = Date.parse("2026-11-01T00:00:00Z");
+
+const seed = (count: number) => {
+ state.rows = Array.from({ length: count }, (_, index) => ({
+ id: `txn${String(index).padStart(5, "0")}`,
+ createdAt: new Date(START + Math.floor(index / 3) * 1000),
+ amountMicroCredits: -(index + 1),
+ }));
+};
+
+const newestFirst = () =>
+ [...state.rows]
+ .sort(
+ (a, b) =>
+ b.createdAt.getTime() - a.createdAt.getTime() ||
+ b.id.localeCompare(a.id),
+ )
+ .map((row) => row.id);
+
+const ids = (page: { rows: { id: string }[] }) =>
+ page.rows.map((row) => row.id).sort();
+
+const page = (cursor: { before?: string | null; after?: string | null } = {}) =>
+ listAppLedgerPage("app-1", {
+ limit: LEDGER_PAGE_SIZE,
+ before: parseLedgerCursor(cursor.before),
+ after: parseLedgerCursor(cursor.after),
+ });
+
+beforeEach(() => {
+ state.queries = [];
+ seed(120);
+});
+
+describe("ledger pages by cursor", () => {
+ it("lists the newest page in one query, with no count", async () => {
+ const first = await page();
+
+ expect(first.rows).toHaveLength(LEDGER_PAGE_SIZE);
+ expect(ids(first)).toEqual(newestFirst().slice(0, 50).sort());
+ expect(first.newer).toBeNull();
+ expect(first.older).not.toBeNull();
+ expect(state.queries).toHaveLength(1);
+ expect(state.queries.join("\n")).not.toMatch(/count\(/i);
+ expect(state.queries.join("\n")).not.toContain("offset");
+ });
+
+ it("walks every row once going Older, across rows written in the same second", async () => {
+ const seen: string[] = [];
+ let current = await page();
+ seen.push(...ids(current));
+ while (current.older) {
+ current = await page({ before: current.older });
+ seen.push(...ids(current));
+ }
+
+ expect(seen).toHaveLength(120);
+ expect(new Set(seen).size).toBe(120);
+ expect(current.rows).toHaveLength(20);
+ expect(current.older).toBeNull();
+ expect(state.queries).toHaveLength(3);
+ });
+
+ it("returns to the same page going Newer", async () => {
+ const first = await page();
+ const second = await page({ before: first.older });
+ const third = await page({ before: second.older });
+
+ const backToSecond = await page({ after: third.newer });
+ const backToFirst = await page({ after: backToSecond.newer });
+
+ expect(ids(backToSecond)).toEqual(ids(second));
+ expect(backToSecond.older).toBe(second.older);
+ expect(ids(backToFirst)).toEqual(ids(first));
+ expect(backToFirst.newer).toBeNull();
+ });
+
+ it("shows a full newest page when fewer than a page of rows is newer", async () => {
+ const first = await page();
+ const second = await page({ before: first.older });
+ state.rows.push({
+ id: "txn99999",
+ createdAt: new Date(START + 3_600_000),
+ amountMicroCredits: -1,
+ });
+
+ const anchor = state.rows.find((row) => row.id === "txn00100");
+ if (!anchor) throw new Error("seed row missing");
+ const afterOne = await page({ after: formatLedgerCursor(anchor) });
+ const backToFirst = await page({ after: second.newer });
+ const top = await page({ after: backToFirst.newer });
+
+ expect(afterOne.rows).toHaveLength(LEDGER_PAGE_SIZE);
+ expect(afterOne.newer).toBeNull();
+ expect(backToFirst.rows).toHaveLength(LEDGER_PAGE_SIZE);
+ expect(backToFirst.newer).not.toBeNull();
+ expect(top.rows).toHaveLength(LEDGER_PAGE_SIZE);
+ expect(top.newer).toBeNull();
+ expect(top.rows.some((row) => row.id === "txn99999")).toBe(true);
+ });
+
+ it("keeps a cursor that survives a URL and ignores one that is not a cursor", async () => {
+ const row = { id: "0abc9xyz", createdAt: new Date(START) };
+ const cursor = formatLedgerCursor(row);
+
+ expect(cursor).toBe(`${START}.0abc9xyz`);
+ expect(encodeURIComponent(cursor)).toBe(cursor);
+ expect(parseLedgerCursor(cursor)).toEqual(row);
+ for (const bad of [undefined, null, "", "3", "abc.def", `${START}.ABC`]) {
+ expect(parseLedgerCursor(bad)).toBeNull();
+ }
+ expect(ids(await page({ before: "not-a-cursor" }))).toEqual(
+ ids(await page()),
+ );
+ });
+
+ it("shows no page links for a ledger that fits one page", async () => {
+ seed(7);
+
+ expect(await page()).toMatchObject({ newer: null, older: null });
+ expect((await page()).rows).toHaveLength(7);
+ });
+});
+
+describe("the transactions API keeps limit and offset", () => {
+ it("lists with limit and offset in one query and never counts", async () => {
+ const result = await listAppLedger("app-1", { limit: 50, offset: 100 });
+
+ expect(result.rows).toHaveLength(20);
+ expect(result.hasMore).toBe(false);
+ expect(
+ (await listAppLedger("app-1", { limit: 50, offset: 0 })).hasMore,
+ ).toBe(true);
+ expect(state.queries).toHaveLength(2);
+ expect(state.queries[0]).toContain("limit ? offset ?");
+ expect(state.queries.join("\n")).not.toMatch(/count\(/i);
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-ledger.test.ts b/apps/web/__tests__/unit/infra-ledger.test.ts
index 601d8591d4c..ed00d305a61 100644
--- a/apps/web/__tests__/unit/infra-ledger.test.ts
+++ b/apps/web/__tests__/unit/infra-ledger.test.ts
@@ -13,7 +13,6 @@ import {
LEDGER_CSV_COLUMNS,
type LedgerRow,
ledgerCsv,
- ledgerPageWindow,
orderLedgerRows,
serializeLedgerRow,
} from "@/lib/infra-ledger";
@@ -546,19 +545,3 @@ describe("CSV export", () => {
expect(ledgerCsv([])).toBe(`${LEDGER_CSV_COLUMNS.join(",")}\r\n`);
});
});
-
-describe("paging the console ledger", () => {
- it.each([
- [undefined, 312, { page: 1, offset: 0, from: 1, to: 50, pages: 7 }],
- ["2", 312, { page: 2, offset: 50, from: 51, to: 100, pages: 7 }],
- ["7", 312, { page: 7, offset: 300, from: 301, to: 312, pages: 7 }],
- ["99", 312, { page: 7, offset: 300, from: 301, to: 312, pages: 7 }],
- ["0", 312, { page: 1, offset: 0, from: 1, to: 50, pages: 7 }],
- ["-3", 312, { page: 1, offset: 0, from: 1, to: 50, pages: 7 }],
- ["abc", 312, { page: 1, offset: 0, from: 1, to: 50, pages: 7 }],
- ["2", 0, { page: 1, offset: 0, from: 0, to: 0, pages: 1 }],
- ["1", 50, { page: 1, offset: 0, from: 1, to: 50, pages: 1 }],
- ])("reads page %j of %i rows", (raw, total, expected) => {
- expect(ledgerPageWindow(raw, total)).toEqual(expected);
- });
-});
diff --git a/apps/web/__tests__/unit/infra-storage-billing.test.ts b/apps/web/__tests__/unit/infra-storage-billing.test.ts
index f6ad01f874e..d77dc3305f8 100644
--- a/apps/web/__tests__/unit/infra-storage-billing.test.ts
+++ b/apps/web/__tests__/unit/infra-storage-billing.test.ts
@@ -29,6 +29,8 @@ import {
getInfraAppStanding,
getInfraBilling,
getInfraUploadAdmission,
+ latestStorageCharge,
+ latestStorageCharges,
} from "@/lib/infra-standing";
import { getAppStorageUsage } from "@/lib/infra-storage";
import { chargeStorageDay } from "@/lib/infra-storage-billing";
@@ -52,6 +54,20 @@ const charge = async () =>
now: new Date(ledger.now),
});
+const finish = async (day: string) =>
+ chargeStorageDay({
+ appId: "app-1",
+ accountId: "account-1",
+ usage: await getAppStorageUsage("app-1"),
+ dateKey: day,
+ now: new Date(ledger.now),
+ retryUnpaid: false,
+ });
+
+const later = (hours: number) => {
+ ledger.now = new Date(ledger.now.getTime() + hours * 60 * 60 * 1000);
+};
+
const credit = (microCredits: number) => {
ledger.account.balanceMicroCredits += microCredits;
ledger.rows.push({
@@ -231,6 +247,161 @@ describe("daily storage charge", () => {
expect(ledger.snapshots.at(-1)?.microCreditsCharged).toBe(0);
});
+ it("leaves a billed day alone when it is finished later", async () => {
+ credit(500_000);
+ expect(await charge()).toMatchObject({ state: "billed" });
+ const snapshot = { ...ledger.snapshots[0] };
+ const day = dateKey();
+ later(20);
+
+ expect(await finish(day)).toEqual({ state: "skipped" });
+
+ expect(storageRows()).toHaveLength(1);
+ expect(ledger.snapshots).toEqual([snapshot]);
+ expect(ledger.account.balanceMicroCredits).toBe(500_000 - 144);
+ });
+
+ it("leaves a short day alone when it is finished later", async () => {
+ credit(50);
+ expect(await charge()).toMatchObject({
+ state: "billed",
+ chargedMicroCredits: 50,
+ fellShort: true,
+ });
+ const snapshot = { ...ledger.snapshots[0] };
+ const day = dateKey();
+ credit(500_000);
+ later(20);
+
+ expect(await finish(day)).toEqual({ state: "skipped" });
+
+ expect(storageRows()).toHaveLength(1);
+ expect(ledger.snapshots).toEqual([snapshot]);
+ });
+
+ it("does not write an unpaid day again when it is finished later, even after a top-up", async () => {
+ await charge();
+ advanceDays(1);
+ expect(await charge()).toMatchObject({ state: "unpaid" });
+ const day = dateKey();
+ const recorded = {
+ ...ledger.snapshots.find((snapshot) => snapshot.snapshotDate === day),
+ };
+ credit(500_000);
+ later(20);
+
+ expect(await finish(day)).toEqual({ state: "skipped" });
+
+ expect(
+ ledger.snapshots.find((snapshot) => snapshot.snapshotDate === day),
+ ).toEqual(recorded);
+ expect(storageRows()).toHaveLength(1);
+ expect(ledger.account.balanceMicroCredits).toBe(500_000);
+ });
+
+ it("records an unpaid day once when it is first reached while finishing", async () => {
+ await charge();
+ advanceDays(1);
+
+ expect(await finish(dateKey())).toMatchObject({ state: "unpaid" });
+ expect(await finish(dateKey())).toEqual({ state: "skipped" });
+
+ expect(
+ ledger.snapshots.filter(
+ (snapshot) => snapshot.snapshotDate === dateKey(),
+ ),
+ ).toHaveLength(1);
+ });
+
+ it("charges a day over an idle visit marker and replaces it", async () => {
+ credit(500_000);
+ ledger.snapshots = [
+ {
+ snapshotDate: dateKey(),
+ totalDurationMinutes: 0,
+ videoCount: 0,
+ microCreditsCharged: 0,
+ processedAt: new Date(ledger.now),
+ },
+ ];
+
+ expect(await charge()).toMatchObject({
+ state: "billed",
+ chargedMicroCredits: 144,
+ });
+
+ expect(storageRows()).toHaveLength(1);
+ expect(ledger.snapshots).toEqual([
+ expect.objectContaining({
+ snapshotDate: dateKey(),
+ videoCount: 3,
+ microCreditsCharged: 144,
+ }),
+ ]);
+ expect(ledger.snapshots[0]?.totalDurationMinutes).toBeGreaterThan(0);
+ });
+
+ it("reads the latest billed day, not the last write, when two days are written in the same second", async () => {
+ const yesterday = new Date(ledger.now.getTime() - DAY_MS)
+ .toISOString()
+ .slice(0, 10);
+ const shortSince = new Date(ledger.now);
+ ledger.account.balanceMicroCredits = 0;
+ ledger.rows.push(
+ {
+ id: "row-a",
+ accountId: "account-1",
+ type: "storage_daily",
+ amountMicroCredits: -50,
+ balanceAfterMicroCredits: 0,
+ referenceId: dateKey(),
+ referenceType: "storage_day",
+ metadata: {
+ snapshotDate: dateKey(),
+ owedMicroCredits: 144,
+ chargedMicroCredits: 50,
+ shortSince: shortSince.toISOString(),
+ },
+ createdAt: new Date(ledger.now),
+ },
+ {
+ id: "row-b",
+ accountId: "account-1",
+ type: "storage_daily",
+ amountMicroCredits: -144,
+ balanceAfterMicroCredits: 50,
+ referenceId: yesterday,
+ referenceType: "storage_day",
+ metadata: {
+ snapshotDate: yesterday,
+ owedMicroCredits: 144,
+ chargedMicroCredits: 144,
+ shortSince: null,
+ },
+ createdAt: new Date(ledger.now),
+ },
+ );
+
+ expect(await latestStorageCharge(fakeDb as never, "account-1")).toEqual({
+ createdAt: new Date(ledger.now),
+ owedMicroCredits: 144,
+ shortSince,
+ });
+ expect(
+ (await latestStorageCharges(fakeDb as never, ["account-1"])).get(
+ "account-1",
+ ),
+ ).toEqual({
+ createdAt: new Date(ledger.now),
+ owedMicroCredits: 144,
+ shortSince,
+ });
+ expect(await standing()).toMatchObject({ state: "grace" });
+
+ advanceDays(1);
+ expect(await charge()).toEqual({ state: "unpaid", shortSince });
+ });
+
it("does not bill an app that stores nothing", async () => {
ledger.videos = [];
ledger.account.balanceMicroCredits = 1_000_000;
diff --git a/apps/web/__tests__/unit/infra-storage-created-before.test.ts b/apps/web/__tests__/unit/infra-storage-created-before.test.ts
new file mode 100644
index 00000000000..65a9f1eb77c
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-storage-created-before.test.ts
@@ -0,0 +1,49 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const state = vi.hoisted(() => ({
+ queries: [] as { sql: string; params: unknown[] }[],
+}));
+
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const client = drizzle(async (sql, params) => {
+ state.queries.push({ sql, params });
+ return { rows: [] };
+ });
+ return { db: () => client };
+});
+
+import { getAppStorage, getStorageUsageByApp } from "@/lib/infra-storage";
+
+beforeEach(() => {
+ state.queries = [];
+});
+
+describe("stored video bounded by creation time", () => {
+ it("adds the bound to the same aggregate query", async () => {
+ const dayEnd = new Date("2026-11-13T00:00:00.000Z");
+
+ await getStorageUsageByApp(["app-1", "app-2"], undefined, undefined, {
+ createdBefore: dayEnd,
+ });
+
+ expect(state.queries).toHaveLength(1);
+ const [{ sql, params }] = state.queries as [
+ { sql: string; params: unknown[] },
+ ];
+ expect(sql).toContain("COALESCE(SUM(`duration`), 0)");
+ expect(sql).toContain("and `developer_videos`.`createdAt` < ?)");
+ expect(sql).toContain("group by");
+ expect(params).toEqual(["app-1", "app-2", "2026-11-13 00:00:00.000"]);
+ });
+
+ it("reads every stored video when no bound is given", async () => {
+ await getStorageUsageByApp(["app-1"]);
+ await getAppStorage("app-1");
+
+ expect(state.queries).toHaveLength(2);
+ for (const { sql } of state.queries) {
+ expect(sql).not.toContain("createdAt");
+ }
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-storage-cron-resume.test.ts b/apps/web/__tests__/unit/infra-storage-cron-resume.test.ts
new file mode 100644
index 00000000000..d2bfa112f1c
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-storage-cron-resume.test.ts
@@ -0,0 +1,947 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+type ChargeCall = {
+ accountId: string;
+ dateKey: string;
+ retryUnpaid: boolean | undefined;
+ dailyMicroCredits: number;
+};
+
+const state = vi.hoisted(() => ({
+ accounts: [] as { id: string; appId: string; createdAt: Date }[],
+ appCreatedAt: new Map(),
+ videos: new Map(),
+ markers: new Map(),
+ markerInserts: [] as string[],
+ usageReads: [] as { appIds: string[]; createdBefore: Date | undefined }[],
+ deletedApps: new Set(),
+ unpaid: new Set(),
+ short: new Set(),
+ everShort: new Set(),
+ charged: new Map(),
+ snapshotWrites: new Map(),
+ chargeCalls: [] as ChargeCall[],
+ accountReads: [] as { cursor: string | null; limit: number | null }[],
+ accountSql: [] as string[],
+ longestIdList: 0,
+ secondsPerCharge: 0,
+ topUps: vi.fn(),
+ emails: vi.fn(),
+ afters: [] as Promise[],
+ continuations: [] as { url: string; authorization: string | null }[],
+ continuationStatus: 200 as number | "network",
+}));
+
+vi.mock("next/server", async (original) => ({
+ ...(await original()),
+ after: (task: Promise) => {
+ state.afters.push(task);
+ },
+}));
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const client = drizzle(async (sql, params) => {
+ if (sql.includes("from `developer_credit_accounts`")) {
+ const limit = /limit \?/.test(sql) ? Number(params.at(-1)) : null;
+ const cursor = /`id` > \?/.test(sql) ? String(params[0]) : null;
+ state.accountReads.push({ cursor, limit });
+ state.accountSql.push(sql);
+ const after = state.accounts.filter(
+ (account) => cursor === null || account.id > cursor,
+ );
+ const stamp = (date: Date) =>
+ date.toISOString().slice(0, 19).replace("T", " ");
+ return {
+ rows: (limit === null ? after : after.slice(0, limit)).map(
+ (account) => [account.id, account.appId, stamp(account.createdAt)],
+ ),
+ };
+ }
+ if (sql.includes("from `developer_apps`")) {
+ const bounded = sql.includes("`developer_apps`.`createdAt` < ?");
+ const bound = bounded
+ ? new Date(`${String(params.at(-1)).replace(" ", "T")}Z`)
+ : null;
+ return {
+ rows: (bounded ? params.slice(0, -1) : params)
+ .map(String)
+ .filter((appId) => !state.deletedApps.has(appId))
+ .filter(
+ (appId) =>
+ !bound ||
+ (state.appCreatedAt.get(appId) ?? new Date(0)).getTime() <
+ bound.getTime(),
+ )
+ .map((appId) => [appId]),
+ };
+ }
+ if (sql.includes("from `developer_credit_transactions`")) {
+ const ids = params.slice(0, -2).map(String);
+ const date = String(params.at(-2));
+ state.longestIdList = Math.max(state.longestIdList, ids.length);
+ return {
+ rows: ids
+ .filter((id) => state.charged.has(`${id}|${date}`))
+ .map((id) => [id]),
+ };
+ }
+ if (sql.includes("from `developer_daily_storage_snapshots`")) {
+ const appIds = params.slice(0, -1).map(String);
+ const date = String(params.at(-1));
+ return {
+ rows: appIds
+ .filter(
+ (appId) =>
+ state.snapshotWrites.has(`${appId}|${date}`) ||
+ state.markers.has(`${appId}|${date}`),
+ )
+ .map((appId) => [appId]),
+ };
+ }
+ throw new Error(`Unexpected query: ${sql}`);
+ });
+ return { db: () => client };
+});
+vi.mock("@/lib/infra-storage", () => ({
+ getStorageUsageByApp: async (
+ appIds: string[],
+ _extra?: unknown,
+ _executor?: unknown,
+ options: { createdBefore?: Date } = {},
+ ) => {
+ state.usageReads.push({ appIds, createdBefore: options.createdBefore });
+ return new Map(
+ appIds.map((appId) => {
+ const videos = (
+ state.videos.get(appId) ?? [new Date("2026-11-01T00:00:00Z")]
+ )
+ .map((video) =>
+ video instanceof Date
+ ? { createdAt: video, readyAt: video }
+ : video,
+ )
+ .filter(
+ ({ createdAt, readyAt }) =>
+ readyAt.getTime() <= Date.now() &&
+ (!options.createdBefore ||
+ createdAt.getTime() < options.createdBefore.getTime()),
+ ).length;
+ return [
+ appId,
+ {
+ videoCount: videos,
+ totalMinutes: videos * 10,
+ minutesByTier: { "1080p": videos * 10, "1440p": 0, "2160p": 0 },
+ monthlyMicroCredits: videos * 840,
+ dailyMicroCredits: videos * 28,
+ },
+ ];
+ }),
+ );
+ },
+}));
+vi.mock("@/lib/infra-storage-billing", () => ({
+ recordStorageDayVisits: async ({
+ appIds,
+ dateKey,
+ }: {
+ appIds: string[];
+ dateKey: string;
+ }) => {
+ for (const appId of appIds) {
+ const key = `${appId}|${dateKey}`;
+ state.markerInserts.push(key);
+ if (!state.snapshotWrites.has(key) && !state.markers.has(key)) {
+ state.markers.set(key, 1);
+ }
+ }
+ },
+ chargeStorageDay: async ({
+ appId,
+ accountId,
+ dateKey,
+ retryUnpaid,
+ usage,
+ }: {
+ appId: string;
+ accountId: string;
+ dateKey: string;
+ retryUnpaid?: boolean;
+ usage: { dailyMicroCredits: number };
+ }) => {
+ state.chargeCalls.push({
+ accountId,
+ dateKey,
+ retryUnpaid,
+ dailyMicroCredits: usage.dailyMicroCredits,
+ });
+ vi.setSystemTime(Date.now() + state.secondsPerCharge * 1000);
+ const key = `${accountId}|${dateKey}`;
+ const snapshot = `${appId}|${dateKey}`;
+ if (state.charged.has(key)) return { state: "skipped" };
+ const writeSnapshot = () => {
+ state.markers.delete(snapshot);
+ state.snapshotWrites.set(
+ snapshot,
+ (state.snapshotWrites.get(snapshot) ?? 0) + 1,
+ );
+ };
+ if (
+ retryUnpaid === false &&
+ (state.markers.has(snapshot) ||
+ (state.unpaid.has(accountId) && state.snapshotWrites.has(snapshot)))
+ ) {
+ return { state: "skipped" };
+ }
+ if (state.unpaid.has(accountId)) {
+ writeSnapshot();
+ return { state: "unpaid", shortSince: new Date("2026-11-01T00:00:00Z") };
+ }
+ if (usage.dailyMicroCredits <= 0) return { state: "skipped" };
+ state.charged.set(key, 1);
+ writeSnapshot();
+ const short = state.short.has(accountId);
+ const fellShort = short && !state.everShort.has(accountId);
+ if (short) state.everShort.add(accountId);
+ return {
+ state: "billed",
+ owedMicroCredits: 28,
+ chargedMicroCredits: short ? 0 : 28,
+ balanceMicroCredits: short ? 0 : 972,
+ shortSince: short ? new Date("2026-11-12T00:00:00Z") : null,
+ fellShort,
+ };
+ },
+}));
+vi.mock("@/lib/infra-standing", () => ({
+ INFRA_GRACE_DAYS: 7,
+ STORAGE_DAY_REFERENCE_TYPE: "storage_day",
+ latestStorageCharges: async () => new Map(),
+}));
+vi.mock("@/lib/infra-auto-top-up", () => ({ runDueAutoTopUps: state.topUps }));
+vi.mock("@/lib/infra-billing-emails", () => ({
+ sendInfraOutOfCreditEmail: state.emails,
+}));
+
+import {
+ GET,
+ MAX_STORAGE_HOPS,
+ maxDuration,
+} from "@/app/api/cron/developer-storage/route";
+
+const ACCOUNTS = 450;
+const ORIGIN = "https://cap.example";
+const PATH = "/api/cron/developer-storage";
+
+type Body = {
+ success?: boolean;
+ error?: string;
+ date: string;
+ complete: boolean;
+ continued: boolean;
+ hop: number;
+ appsProcessed: number;
+ appsShort: number;
+};
+
+const call = async (query = "") => {
+ const response = await GET(
+ new Request(`${ORIGIN}${PATH}${query}`, {
+ headers: { authorization: "Bearer cron-secret" },
+ }),
+ );
+ return { status: response.status, body: (await response.json()) as Body };
+};
+
+const run = async (query = "") => (await call(query)).body;
+
+const lastContinuation = () => {
+ const last = state.continuations.at(-1);
+ if (!last) throw new Error("no continuation was started");
+ return new URL(last.url);
+};
+
+const followChain = async (
+ first: Body,
+ between: () => void = () => {},
+ limit = MAX_STORAGE_HOPS + 2,
+) => {
+ const bodies = [first];
+ let body = first;
+ while (body.continued) {
+ if (bodies.length > limit) throw new Error("chain did not end");
+ between();
+ const next = lastContinuation();
+ body = await run(next.search);
+ bodies.push(body);
+ }
+ return bodies;
+};
+
+const chargedFor = (date: string) =>
+ [...state.charged.keys()]
+ .filter((key) => key.endsWith(`|${date}`))
+ .map((key) => key.split("|")[0])
+ .sort();
+
+const callsFor = (date: string) =>
+ state.chargeCalls.filter((call) => call.dateKey === date);
+
+const allIds = () => state.accounts.map((account) => account.id);
+
+const settle = (date: string) => {
+ for (const id of allIds()) state.charged.set(`${id}|${date}`, 1);
+};
+
+beforeEach(() => {
+ process.env.CRON_SECRET = "cron-secret";
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-11-12T06:41:00Z"));
+ state.accounts = Array.from({ length: ACCOUNTS }, (_, index) => {
+ const serial = String(index).padStart(4, "0");
+ return {
+ id: `account-${serial}`,
+ appId: `app-${serial}`,
+ createdAt: new Date("2026-10-01T00:00:00Z"),
+ };
+ });
+ state.appCreatedAt = new Map();
+ state.videos = new Map();
+ state.markers = new Map();
+ state.markerInserts = [];
+ state.usageReads = [];
+ state.deletedApps = new Set();
+ state.unpaid = new Set();
+ state.short = new Set();
+ state.everShort = new Set();
+ state.charged = new Map();
+ state.snapshotWrites = new Map();
+ settle("2026-11-11");
+ state.chargeCalls = [];
+ state.accountReads = [];
+ state.accountSql = [];
+ state.longestIdList = 0;
+ state.secondsPerCharge = 0;
+ state.afters = [];
+ state.continuations = [];
+ state.continuationStatus = 200;
+ state.emails.mockReset();
+ state.topUps.mockReset();
+ state.topUps.mockResolvedValue({ attempted: 0, succeeded: 0, failed: 0 });
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (url: URL | string, init?: RequestInit) => {
+ state.continuations.push({
+ url: String(url),
+ authorization: new Headers(init?.headers).get("authorization"),
+ });
+ if (state.continuationStatus === "network") {
+ throw new TypeError("fetch failed");
+ }
+ return new Response(null, { status: state.continuationStatus });
+ }),
+ );
+});
+
+afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
+
+describe("storage cron paging", () => {
+ it("reads accounts a page at a time, by id, for yesterday and then today, and never sends every id at once", async () => {
+ const body = await run();
+
+ expect(body).toMatchObject({
+ date: "2026-11-12",
+ complete: true,
+ continued: false,
+ appsProcessed: ACCOUNTS,
+ });
+ const pages = [
+ { cursor: null, limit: 200 },
+ { cursor: "account-0199", limit: 200 },
+ { cursor: "account-0399", limit: 200 },
+ ];
+ expect(state.accountReads).toEqual([...pages, ...pages]);
+ expect(state.longestIdList).toBe(200);
+ expect(state.continuations).toEqual([]);
+ expect(callsFor("2026-11-11")).toEqual([]);
+ });
+
+ it("pages on the account id alone and leaves out accounts of deleted apps", async () => {
+ state.deletedApps = new Set(["app-0003", "app-0250"]);
+
+ const body = await run();
+
+ expect(body).toMatchObject({ complete: true, appsProcessed: ACCOUNTS - 2 });
+ expect(chargedFor("2026-11-12")).not.toContain("account-0003");
+ expect(chargedFor("2026-11-12")).not.toContain("account-0250");
+ expect(state.accountSql.every((sql) => !sql.includes("join"))).toBe(true);
+ });
+});
+
+describe("a cut-off sweep continues itself", () => {
+ it("hands the date, the last attempted account and the next hop to a request to itself", async () => {
+ state.secondsPerCharge = 1.6;
+ const startedAt = Date.now();
+
+ const body = await run();
+
+ expect(body).toMatchObject({
+ date: "2026-11-12",
+ complete: false,
+ continued: true,
+ hop: 0,
+ appsProcessed: 130,
+ });
+ expect(Date.now() - startedAt).toBeLessThan(maxDuration * 1000);
+ expect(state.afters).toHaveLength(1);
+ await Promise.all(state.afters);
+ expect(state.continuations).toHaveLength(1);
+ const next = lastContinuation();
+ expect(next.origin).toBe(ORIGIN);
+ expect(next.pathname).toBe(PATH);
+ expect(Object.fromEntries(next.searchParams)).toEqual({
+ date: "2026-11-12",
+ after: "account-0129",
+ hop: "1",
+ });
+ expect(state.continuations[0]?.authorization).toBe("Bearer cron-secret");
+ });
+
+ it("resumes a page cut off part way at the first account it did not attempt", async () => {
+ state.secondsPerCharge = 1.6;
+ await run();
+ const attempted = state.chargeCalls.map((call) => call.accountId);
+ state.chargeCalls = [];
+ state.accountReads = [];
+ state.secondsPerCharge = 0;
+
+ const body = await run(lastContinuation().search);
+
+ expect(attempted.at(-1)).toBe("account-0129");
+ expect(state.chargeCalls[0]?.accountId).toBe("account-0130");
+ expect(state.accountReads).toEqual([
+ { cursor: "account-0129", limit: 200 },
+ { cursor: "account-0329", limit: 200 },
+ ]);
+ expect(body).toMatchObject({
+ complete: true,
+ continued: false,
+ hop: 1,
+ appsProcessed: ACCOUNTS - 130,
+ });
+ expect(chargedFor("2026-11-12")).toEqual(allIds());
+ });
+
+ it("never charges again an account an earlier hop charged", async () => {
+ state.secondsPerCharge = 1.6;
+ const bodies = await followChain(await run());
+
+ expect(bodies.length).toBeGreaterThan(2);
+ expect(bodies.at(-1)).toMatchObject({ complete: true, continued: false });
+ const attempted = callsFor("2026-11-12").map((call) => call.accountId);
+ expect(new Set(attempted).size).toBe(attempted.length);
+ expect(chargedFor("2026-11-12")).toEqual(allIds());
+ expect(bodies.reduce((total, body) => total + body.appsProcessed, 0)).toBe(
+ ACCOUNTS,
+ );
+ });
+
+ it("finishes the day a continuation started even after midnight, then sweeps the new day", async () => {
+ vi.setSystemTime(new Date("2026-11-12T23:56:00Z"));
+ state.secondsPerCharge = 1.6;
+
+ const first = await run();
+ expect(first).toMatchObject({ date: "2026-11-12", continued: true });
+ vi.setSystemTime(new Date("2026-11-13T00:02:00Z"));
+ state.secondsPerCharge = 0;
+
+ const bodies = await followChain(first);
+
+ expect(bodies.at(-1)).toMatchObject({
+ date: "2026-11-13",
+ complete: true,
+ });
+ expect(chargedFor("2026-11-12")).toEqual(allIds());
+ expect(chargedFor("2026-11-13")).toEqual(allIds());
+ expect(callsFor("2026-11-12")).toHaveLength(ACCOUNTS);
+ expect(callsFor("2026-11-13")).toHaveLength(ACCOUNTS);
+ });
+
+ it("reaches funded accounts behind a run of unpaid accounts longer than one run's budget", async () => {
+ state.unpaid = new Set(allIds().slice(0, 300));
+ state.secondsPerCharge = 1.6;
+
+ const bodies = await followChain(await run());
+
+ const funded = allIds().slice(300);
+ expect(chargedFor("2026-11-12")).toEqual(funded);
+ expect(bodies.at(-1)).toMatchObject({ complete: true });
+ const unpaidCalls = callsFor("2026-11-12").filter((call) =>
+ state.unpaid.has(call.accountId),
+ );
+ expect(unpaidCalls).toHaveLength(300);
+ expect(new Set(unpaidCalls.map((call) => call.accountId)).size).toBe(300);
+ });
+});
+
+describe("a broken chain is finished by the next day's first run", () => {
+ const breakOnLastTickOfTheDay = async () => {
+ vi.setSystemTime(new Date("2026-11-12T18:41:00Z"));
+ state.secondsPerCharge = 1.6;
+ state.continuationStatus = "network";
+ vi.spyOn(console, "error").mockImplementation(() => {});
+ const lastTick = await run();
+ await Promise.all(state.afters);
+ expect(lastTick).toMatchObject({
+ date: "2026-11-12",
+ complete: false,
+ continued: true,
+ });
+ expect(console.error).toHaveBeenCalledWith(
+ "[developer-storage] Continuation not started",
+ expect.objectContaining({ date: "2026-11-12", after: "account-0129" }),
+ );
+ const firstDayCalls = callsFor("2026-11-12").map((call) => call.accountId);
+ vi.setSystemTime(new Date("2026-11-13T00:41:00Z"));
+ state.secondsPerCharge = 0;
+ state.continuationStatus = 200;
+ state.chargeCalls = [];
+ return firstDayCalls;
+ };
+
+ it("charges the accounts after the cursor for that day, each once, then sweeps today", async () => {
+ const chargedBeforeTheBreak = await breakOnLastTickOfTheDay();
+
+ const nextDay = await run();
+
+ expect(nextDay).toMatchObject({
+ date: "2026-11-13",
+ complete: true,
+ continued: false,
+ hop: 0,
+ appsProcessed: ACCOUNTS - 130 + ACCOUNTS,
+ });
+ expect(chargedFor("2026-11-12")).toEqual(allIds());
+ expect(chargedFor("2026-11-13")).toEqual(allIds());
+ const finished = callsFor("2026-11-12").map((call) => call.accountId);
+ expect(finished).toEqual(allIds().slice(130));
+ expect(finished.some((id) => chargedBeforeTheBreak.includes(id))).toBe(
+ false,
+ );
+ expect(
+ callsFor("2026-11-12").every((call) => call.retryUnpaid === false),
+ ).toBe(true);
+ expect(
+ callsFor("2026-11-13").every((call) => call.retryUnpaid === true),
+ ).toBe(true);
+ });
+
+ it("does not charge or email again an account that fell short before the break", async () => {
+ state.short = new Set(["account-0005", "account-0006", "account-0200"]);
+ await breakOnLastTickOfTheDay();
+ expect(state.emails).toHaveBeenCalledTimes(2);
+
+ await run();
+
+ expect(state.emails).toHaveBeenCalledTimes(3);
+ const emailed = state.emails.mock.calls.map(
+ ([notice]) => (notice as { accountId: string }).accountId,
+ );
+ expect(emailed.sort()).toEqual([
+ "account-0005",
+ "account-0006",
+ "account-0200",
+ ]);
+ expect(callsFor("2026-11-12").map((call) => call.accountId)).not.toContain(
+ "account-0005",
+ );
+ });
+
+ it("writes no second snapshot for an account that was unpaid before the break", async () => {
+ state.unpaid = new Set([
+ "account-0010",
+ "account-0011",
+ "account-0300",
+ "account-0301",
+ ]);
+ await breakOnLastTickOfTheDay();
+ expect(state.snapshotWrites.get("app-0010|2026-11-12")).toBe(1);
+ expect(state.snapshotWrites.has("app-0300|2026-11-12")).toBe(false);
+
+ await run();
+
+ const finished = callsFor("2026-11-12").map((call) => call.accountId);
+ expect(finished).not.toContain("account-0010");
+ expect(finished).not.toContain("account-0011");
+ expect(finished).toContain("account-0300");
+ for (const app of ["app-0010", "app-0011", "app-0300", "app-0301"]) {
+ expect(state.snapshotWrites.get(`${app}|2026-11-12`)).toBe(1);
+ }
+ expect(state.snapshotWrites.get("app-0010|2026-11-13")).toBe(1);
+ });
+
+ it("costs a settled yesterday one read per table per page and no charge", async () => {
+ settle("2026-11-12");
+ vi.setSystemTime(new Date("2026-11-13T00:41:00Z"));
+ state.unpaid = new Set(allIds().slice(0, 50));
+ for (const id of allIds().slice(0, 50)) {
+ state.charged.delete(`${id}|2026-11-12`);
+ state.snapshotWrites.set(`${id.replace("account", "app")}|2026-11-12`, 1);
+ }
+
+ await run();
+
+ expect(callsFor("2026-11-12")).toEqual([]);
+ for (const id of allIds().slice(0, 50)) {
+ expect(
+ state.snapshotWrites.get(`${id.replace("account", "app")}|2026-11-12`),
+ ).toBe(1);
+ }
+ });
+});
+
+describe("the finishing pass counts only what existed on that day", () => {
+ const nextDayTick = () => {
+ vi.setSystemTime(new Date("2026-11-13T00:41:00Z"));
+ settle("2026-11-12");
+ for (const id of [
+ "account-0300",
+ "account-0301",
+ "account-0302",
+ "account-0303",
+ ]) {
+ state.charged.delete(`${id}|2026-11-12`);
+ }
+ };
+ const account = (id: string) => {
+ const found = state.accounts.find((row) => row.id === id);
+ if (!found) throw new Error(`no ${id}`);
+ return found;
+ };
+
+ it("does not charge yesterday for an account whose first video arrived after midnight", async () => {
+ nextDayTick();
+ state.videos.set("app-0300", [new Date("2026-11-13T00:30:00Z")]);
+
+ await run();
+
+ expect(chargedFor("2026-11-12")).not.toContain("account-0300");
+ expect(callsFor("2026-11-12").map((call) => call.accountId)).not.toContain(
+ "account-0300",
+ );
+ expect(chargedFor("2026-11-13")).toContain("account-0300");
+ });
+
+ it("does not charge yesterday for an account or app created after midnight", async () => {
+ nextDayTick();
+ account("account-0301").createdAt = new Date("2026-11-13T00:10:00Z");
+ state.appCreatedAt.set("app-0302", new Date("2026-11-13T00:10:00Z"));
+
+ await run();
+
+ const finished = callsFor("2026-11-12").map((call) => call.accountId);
+ expect(finished).not.toContain("account-0301");
+ expect(finished).not.toContain("account-0302");
+ expect(finished.sort()).toEqual(["account-0300", "account-0303"]);
+ expect(chargedFor("2026-11-13")).toEqual(
+ expect.arrayContaining(["account-0301", "account-0302"]),
+ );
+ });
+
+ it("leaves a video created after midnight out of yesterday's charge and keeps the earlier ones", async () => {
+ nextDayTick();
+ state.videos.set("app-0303", [
+ new Date("2026-11-05T00:00:00Z"),
+ new Date("2026-11-12T23:59:59Z"),
+ new Date("2026-11-13T00:20:00Z"),
+ ]);
+
+ await run();
+
+ const [yesterday] = callsFor("2026-11-12").filter(
+ (call) => call.accountId === "account-0303",
+ );
+ const [today] = callsFor("2026-11-13").filter(
+ (call) => call.accountId === "account-0303",
+ );
+ expect(yesterday?.dailyMicroCredits).toBe(56);
+ expect(today?.dailyMicroCredits).toBe(84);
+ });
+
+ it("bounds only the finishing pass, at the midnight after that day", async () => {
+ nextDayTick();
+
+ await run();
+
+ const bounds = state.usageReads.map((read) =>
+ read.createdBefore?.toISOString(),
+ );
+ expect(bounds.filter(Boolean)).toEqual(["2026-11-13T00:00:00.000Z"]);
+ expect(bounds.filter((bound) => bound === undefined)).toHaveLength(3);
+ });
+});
+
+describe("a visit on a day leaves a marker when nothing is charged", () => {
+ const tick = (at: string) => {
+ vi.setSystemTime(new Date(at));
+ return run();
+ };
+ const insertsFor = (app: string, date: string) =>
+ state.markerInserts.filter((key) => key === `${app}|${date}`).length;
+
+ it("does not charge a day for an app that was empty at every run of it, when its first video becomes ready after midnight", async () => {
+ state.videos.set("app-0300", [
+ {
+ createdAt: new Date("2026-11-12T23:00:00Z"),
+ readyAt: new Date("2026-11-13T00:20:00Z"),
+ },
+ ]);
+ for (const at of [
+ "2026-11-12T06:41:00Z",
+ "2026-11-12T12:41:00Z",
+ "2026-11-12T18:41:00Z",
+ ]) {
+ await tick(at);
+ }
+ expect(state.markers.has("app-0300|2026-11-12")).toBe(true);
+
+ await tick("2026-11-13T00:41:00Z");
+
+ expect(callsFor("2026-11-12").map((call) => call.accountId)).not.toContain(
+ "account-0300",
+ );
+ expect(chargedFor("2026-11-12")).not.toContain("account-0300");
+ expect(chargedFor("2026-11-13")).toContain("account-0300");
+ });
+
+ it("charges a video that becomes ready after the last run of its day from the next day, as main does", async () => {
+ state.videos.set("app-0300", [
+ {
+ createdAt: new Date("2026-11-12T19:00:00Z"),
+ readyAt: new Date("2026-11-12T19:05:00Z"),
+ },
+ ]);
+ for (const at of [
+ "2026-11-12T06:41:00Z",
+ "2026-11-12T12:41:00Z",
+ "2026-11-12T18:41:00Z",
+ ]) {
+ await tick(at);
+ }
+ expect(state.markers.has("app-0300|2026-11-12")).toBe(true);
+
+ await tick("2026-11-13T00:41:00Z");
+ await tick("2026-11-13T06:41:00Z");
+
+ expect(callsFor("2026-11-12").map((call) => call.accountId)).not.toContain(
+ "account-0300",
+ );
+ expect(chargedFor("2026-11-12")).not.toContain("account-0300");
+ expect(
+ callsFor("2026-11-13").filter(
+ (call) => call.accountId === "account-0300",
+ ),
+ ).toEqual([expect.objectContaining({ dailyMicroCredits: 28 })]);
+ expect(chargedFor("2026-11-13")).toContain("account-0300");
+ });
+
+ it("charges a day for an app the sweep never reached that day, for videos created before midnight", async () => {
+ state.videos.set("app-0300", [
+ new Date("2026-11-12T10:00:00Z"),
+ {
+ createdAt: new Date("2026-11-13T00:05:00Z"),
+ readyAt: new Date("2026-11-13T00:06:00Z"),
+ },
+ ]);
+ vi.setSystemTime(new Date("2026-11-12T18:41:00Z"));
+ state.secondsPerCharge = 1.6;
+ state.continuationStatus = "network";
+ vi.spyOn(console, "error").mockImplementation(() => {});
+ const lastTick = await run();
+ await Promise.all(state.afters);
+ expect(lastTick.continued).toBe(true);
+ expect(state.markers.has("app-0300|2026-11-12")).toBe(false);
+ state.secondsPerCharge = 0;
+
+ await tick("2026-11-13T00:41:00Z");
+
+ const [finished] = callsFor("2026-11-12").filter(
+ (call) => call.accountId === "account-0300",
+ );
+ expect(finished?.dailyMicroCredits).toBe(28);
+ expect(chargedFor("2026-11-12")).toContain("account-0300");
+ const [today] = callsFor("2026-11-13").filter(
+ (call) => call.accountId === "account-0300",
+ );
+ expect(today?.dailyMicroCredits).toBe(56);
+ });
+
+ it("writes one marker per idle app and day, however often the app is visited", async () => {
+ const idle = ["app-0400", "app-0401", "app-0402", "app-0403", "app-0404"];
+ for (const app of idle) state.videos.set(app, []);
+
+ for (const at of [
+ "2026-11-12T06:41:00Z",
+ "2026-11-12T12:41:00Z",
+ "2026-11-12T18:41:00Z",
+ "2026-11-13T00:41:00Z",
+ "2026-11-13T06:41:00Z",
+ ]) {
+ await tick(at);
+ }
+
+ for (const app of idle) {
+ expect(insertsFor(app, "2026-11-12")).toBe(1);
+ expect(insertsFor(app, "2026-11-13")).toBe(1);
+ expect(state.markers.get(`${app}|2026-11-12`)).toBe(1);
+ }
+ expect(state.markerInserts).toHaveLength(idle.length * 2);
+ expect(
+ state.chargeCalls.filter((call) =>
+ idle.includes(call.accountId.replace("account", "app")),
+ ),
+ ).toEqual([]);
+ });
+
+ it("still charges the same day when storage arrives after an idle marker", async () => {
+ state.videos.set("app-0410", [
+ {
+ createdAt: new Date("2026-11-12T09:00:00Z"),
+ readyAt: new Date("2026-11-12T10:00:00Z"),
+ },
+ ]);
+ await tick("2026-11-12T06:41:00Z");
+ expect(state.markers.has("app-0410|2026-11-12")).toBe(true);
+ expect(chargedFor("2026-11-12")).not.toContain("account-0410");
+
+ await tick("2026-11-12T12:41:00Z");
+
+ const charged = callsFor("2026-11-12").filter(
+ (call) => call.accountId === "account-0410",
+ );
+ expect(charged).toEqual([
+ expect.objectContaining({ dailyMicroCredits: 28, retryUnpaid: true }),
+ ]);
+ expect(chargedFor("2026-11-12")).toContain("account-0410");
+ expect(state.snapshotWrites.get("app-0410|2026-11-12")).toBe(1);
+ expect(state.markers.has("app-0410|2026-11-12")).toBe(false);
+ });
+});
+
+describe("the chain is bounded", () => {
+ it("stops at the hop limit and logs it", async () => {
+ state.secondsPerCharge = 1.6;
+ const errors = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ const body = await run(
+ `?date=2026-11-12&after=account-0099&hop=${MAX_STORAGE_HOPS}`,
+ );
+
+ expect(body).toMatchObject({
+ complete: false,
+ continued: false,
+ hop: MAX_STORAGE_HOPS,
+ });
+ expect(state.afters).toEqual([]);
+ expect(state.continuations).toEqual([]);
+ expect(errors).toHaveBeenCalledWith(
+ "[developer-storage] Sweep stopped at the hop limit",
+ expect.objectContaining({ date: "2026-11-12", hop: MAX_STORAGE_HOPS }),
+ );
+ });
+
+ it("logs a continuation the route refused", async () => {
+ state.secondsPerCharge = 1.6;
+ state.continuationStatus = 500;
+ const errors = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ await run();
+ await Promise.all(state.afters);
+
+ expect(errors).toHaveBeenCalledWith(
+ "[developer-storage] Continuation refused",
+ expect.objectContaining({ status: 500, hop: 1 }),
+ );
+ });
+
+ it("logs a handoff that is still running, with the next cursor, as information", async () => {
+ state.secondsPerCharge = 1.6;
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async () => {
+ throw new DOMException("The operation timed out.", "TimeoutError");
+ }),
+ );
+ const info = vi.spyOn(console, "info").mockImplementation(() => {});
+ const errors = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ await run();
+ await Promise.all(state.afters);
+
+ expect(info).toHaveBeenCalledWith(
+ "[developer-storage] Continuation still running at handoff",
+ { date: "2026-11-12", after: "account-0129", hop: 1 },
+ );
+ expect(errors).not.toHaveBeenCalled();
+ });
+
+ it("refuses a date other than today or yesterday and any malformed continuation", async () => {
+ for (const query of [
+ "?date=2026-11-10&hop=1",
+ "?date=2026-11-13&hop=1",
+ "?date=2026-1-12&hop=1",
+ "?date=yesterday&hop=1",
+ "?date=2026-11-12",
+ "?hop=1",
+ "?after=account-0001",
+ "?date=2026-11-12&hop=0",
+ `?date=2026-11-12&hop=${MAX_STORAGE_HOPS + 1}`,
+ "?date=2026-11-12&hop=1&after=a%20b",
+ "?date=2026-11-12&hop=1x",
+ ]) {
+ const { status, body } = await call(query);
+ expect({ query, status, error: body.error }).toEqual({
+ query,
+ status: 400,
+ error: "Invalid continuation",
+ });
+ }
+ expect(state.chargeCalls).toEqual([]);
+ expect(state.topUps).not.toHaveBeenCalled();
+ expect((await call("?date=2026-11-11&hop=1")).status).toBe(200);
+ });
+});
+
+describe("auto top-ups run once per sweep", () => {
+ it("runs before charging on the scheduled run and after charging on the last hop only", async () => {
+ state.secondsPerCharge = 1.6;
+ const startedAt = Date.now();
+
+ const first = await run();
+ expect(state.topUps).toHaveBeenCalledTimes(1);
+ const [[, before]] = state.topUps.mock.calls as [[Date, { until: number }]];
+ expect(before.until - startedAt).toBeGreaterThan(0);
+ expect(before.until - startedAt).toBeLessThan(maxDuration * 1000);
+
+ state.topUps.mockClear();
+ const second = await run(lastContinuation().search);
+ expect(second.continued).toBe(true);
+ expect(state.topUps).not.toHaveBeenCalled();
+
+ state.secondsPerCharge = 0;
+ const bodies = await followChain(second);
+ expect(bodies.at(-1)).toMatchObject({ complete: true });
+ expect(state.topUps).toHaveBeenCalledTimes(1);
+ expect(first.continued).toBe(true);
+ });
+
+ it("runs both passes on a scheduled run that finishes", async () => {
+ await run();
+
+ expect(state.topUps).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-storage-latest-day.test.ts b/apps/web/__tests__/unit/infra-storage-latest-day.test.ts
new file mode 100644
index 00000000000..43fb75e8f30
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-storage-latest-day.test.ts
@@ -0,0 +1,81 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+const state = vi.hoisted(() => ({
+ queries: [] as { sql: string; params: unknown[] }[],
+}));
+
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const client = drizzle(async (sql, params) => {
+ state.queries.push({ sql, params });
+ return { rows: [] };
+ });
+ return { db: () => client };
+});
+
+import { db } from "@cap/database";
+import {
+ latestStorageCharge,
+ latestStorageCharges,
+} from "@/lib/infra-standing";
+
+const LATEST_DAY =
+ "(select `developer_credit_transactions`.`accountId`, `developer_credit_transactions`.`referenceId` from `developer_credit_transactions` where `developer_credit_transactions`.`accountId` = ? and `developer_credit_transactions`.`referenceType` = ? and `developer_credit_transactions`.`referenceId` <= ? order by `developer_credit_transactions`.`referenceId` desc limit 1)";
+
+beforeEach(() => {
+ state.queries = [];
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-11-13T23:59:59Z"));
+});
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+describe("latest storage day", () => {
+ it("orders one account's storage day rows by the day they bill, up to tomorrow", async () => {
+ await latestStorageCharge(db() as never, "account-1");
+
+ expect(state.queries).toHaveLength(1);
+ const [{ sql, params }] = state.queries as [
+ { sql: string; params: unknown[] },
+ ];
+ expect(sql).toBe(
+ "select `createdAt`, `metadata` from `developer_credit_transactions` where (`developer_credit_transactions`.`accountId` = ? and (`developer_credit_transactions`.`type` = ? and `developer_credit_transactions`.`referenceType` = ?) and `developer_credit_transactions`.`referenceId` <= ?) order by `developer_credit_transactions`.`referenceId` desc, `developer_credit_transactions`.`id` desc limit ?",
+ );
+ expect(params).toEqual([
+ "account-1",
+ "storage_daily",
+ "storage_day",
+ "2026-11-14",
+ 1,
+ ]);
+ });
+
+ it("finds every account's latest day in one statement, one bounded lookup per account", async () => {
+ await latestStorageCharges(db() as never, ["account-1", "account-2"]);
+
+ expect(state.queries).toHaveLength(1);
+ const [{ sql, params }] = state.queries as [
+ { sql: string; params: unknown[] },
+ ];
+ expect(sql).toContain(
+ `in (select latest.accountId, latest.referenceId from (${LATEST_DAY} union all ${LATEST_DAY}) latest)`,
+ );
+ expect(params).toEqual([
+ "storage_daily",
+ "storage_day",
+ "account-1",
+ "storage_day",
+ "2026-11-14",
+ "account-2",
+ "storage_day",
+ "2026-11-14",
+ ]);
+ });
+
+ it("reads nothing for no accounts", async () => {
+ expect(await latestStorageCharges(db() as never, [])).toEqual(new Map());
+ expect(state.queries).toEqual([]);
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-storage-once.test.ts b/apps/web/__tests__/unit/infra-storage-once.test.ts
new file mode 100644
index 00000000000..625a12a4a2d
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-storage-once.test.ts
@@ -0,0 +1,130 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const holder = vi.hoisted(() => ({ db: null as unknown }));
+
+vi.mock("@cap/database", () => ({ db: () => holder.db }));
+vi.mock("@cap/database/helpers", () => ({
+ nanoId: () => "row",
+ nanoIdLength: 15,
+}));
+
+import { fakeDb, ledger, resetLedger } from "./infra-billing-fake-db";
+
+holder.db = fakeDb;
+
+import { billableVideo } from "@/lib/developer-pricing";
+import { getInfraBilling, infraBillingJson } from "@/lib/infra-standing";
+import { getAppStorage, getAppStorageUsage } from "@/lib/infra-storage";
+
+const newVideo = billableVideo({
+ duration: 900,
+ width: 3840,
+ height: 2160,
+ maxResolutionTier: "2160p",
+});
+
+const videoReads = (select: { mock: { results: { value: unknown }[] } }) =>
+ select.mock.results.length;
+
+beforeEach(() => {
+ resetLedger();
+ ledger.account.balanceMicroCredits = 40_000;
+ ledger.videos = [
+ {
+ videoId: "v1",
+ appId: "app-1",
+ width: 1280,
+ height: 720,
+ seconds: 61.5,
+ count: 3,
+ },
+ {
+ videoId: "v2",
+ appId: "app-1",
+ width: 2560,
+ height: 1440,
+ seconds: 600.25,
+ count: 2,
+ },
+ {
+ videoId: "v3",
+ appId: "app-1",
+ width: 3840,
+ height: 2160,
+ seconds: 1234.125,
+ count: 1,
+ maxResolutionTier: "1080p",
+ },
+ {
+ videoId: "v4",
+ appId: "app-1",
+ width: 1920,
+ height: 1080,
+ seconds: 300,
+ count: 4,
+ status: "processing",
+ },
+ {
+ videoId: "v5",
+ appId: "app-1",
+ width: 1920,
+ height: 1080,
+ seconds: 77,
+ count: 1,
+ status: "error",
+ },
+ ];
+});
+
+describe("one read of stored video serves both totals", () => {
+ it("gives the same stored and counted totals as two separate reads", async () => {
+ for (const [video, options] of [
+ [undefined, {}],
+ [newVideo, {}],
+ [newVideo, { excludeVideoId: "v2" }],
+ [undefined, { excludeVideoId: "v4" }],
+ ] as const) {
+ const storage = await getAppStorage("app-1", video, undefined, options);
+
+ expect(storage.stored).toEqual(
+ await getAppStorageUsage("app-1", video, undefined, options),
+ );
+ expect(storage.counted).toEqual(
+ await getAppStorageUsage("app-1", video, undefined, {
+ ...options,
+ includeProcessing: true,
+ }),
+ );
+ }
+ });
+
+ it("keeps processing video out of what is shown as stored and in what admission counts", async () => {
+ const { stored, counted } = await getAppStorage("app-1");
+
+ expect(stored.videoCount).toBe(6);
+ expect(counted.videoCount).toBe(10);
+ expect(counted.totalMinutes - stored.totalMinutes).toBeCloseTo(5, 10);
+ });
+
+ it("reads video once for billing and returns what the pages show", async () => {
+ const now = new Date(ledger.now);
+ const select = vi.spyOn(fakeDb, "select");
+ const onVideos = () =>
+ select.mock.calls.filter(([fields]) => "seconds" in fields).length;
+
+ const billing = await getInfraBilling("app-1", undefined, now);
+
+ expect(onVideos()).toBe(1);
+ expect(videoReads(select)).toBeGreaterThan(1);
+ select.mockRestore();
+ expect(billing.stored).toEqual(await getAppStorageUsage("app-1"));
+ expect(billing.usage).toEqual(
+ await getAppStorageUsage("app-1", undefined, undefined, {
+ includeProcessing: true,
+ }),
+ );
+ expect(infraBillingJson(billing).storage.minutesByTier).toEqual(
+ billing.usage.minutesByTier,
+ );
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-storage-snapshot-readers.test.ts b/apps/web/__tests__/unit/infra-storage-snapshot-readers.test.ts
new file mode 100644
index 00000000000..b52178485df
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-storage-snapshot-readers.test.ts
@@ -0,0 +1,149 @@
+import { User } from "@cap/web-domain";
+import { Effect } from "effect";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+type Snapshot = {
+ snapshotDate: string;
+ totalDurationMinutes: number;
+ videoCount: number;
+ microCreditsCharged: number;
+};
+
+const state = vi.hoisted(() => ({
+ queries: [] as string[],
+ snapshots: [] as {
+ snapshotDate: string;
+ totalDurationMinutes: number;
+ videoCount: number;
+ microCreditsCharged: number;
+ }[],
+}));
+
+vi.mock("server-only", () => ({}));
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const STAMP = "2026-11-01 00:00:00";
+ const client = drizzle(async (sql, params) => {
+ state.queries.push(sql);
+ if (sql.includes("from `developer_daily_storage_snapshots`")) {
+ const skipsZero = sql.includes(
+ "`developer_daily_storage_snapshots`.`totalDurationMinutes` > ?",
+ );
+ const since = params.find(
+ (value) =>
+ typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value),
+ );
+ const rows = state.snapshots
+ .filter((row) => !skipsZero || row.totalDurationMinutes > 0)
+ .filter((row) => !since || row.snapshotDate >= String(since))
+ .sort((a, b) => b.snapshotDate.localeCompare(a.snapshotDate));
+ if (
+ sql.startsWith(
+ "select `developer_daily_storage_snapshots`.`totalDurationMinutes`",
+ ) ||
+ sql.startsWith("select `totalDurationMinutes` from")
+ ) {
+ return {
+ rows: rows.slice(0, 1).map((row) => [row.totalDurationMinutes]),
+ };
+ }
+ return {
+ rows: rows.map((row) => [
+ row.snapshotDate,
+ row.totalDurationMinutes,
+ row.videoCount,
+ row.microCreditsCharged,
+ ]),
+ };
+ }
+ if (sql.includes("from `developer_apps`")) {
+ return {
+ rows: [["app-1", "First app", "production", null, STAMP, STAMP]],
+ };
+ }
+ if (sql.includes("from `developer_credit_accounts`")) {
+ return { rows: [[5_000_000, 0, 0, 0, 0]] };
+ }
+ if (sql.includes("COUNT(*)")) return { rows: [[3]] };
+ return { rows: [] };
+ });
+ return { db: () => client };
+});
+
+import { getInfraStorageSnapshots } from "@/app/(infra)/dashboard/infra/infra-data";
+import { summarizeStorageMonth } from "@/lib/infra-usage";
+import { AgentManagement } from "../../../../packages/web-backend/src/AgentManagement";
+
+const charged = (snapshotDate: string, minutes: number): Snapshot => ({
+ snapshotDate,
+ totalDurationMinutes: minutes,
+ videoCount: 2,
+ microCreditsCharged: 120,
+});
+const unpaid = (snapshotDate: string): Snapshot => ({
+ snapshotDate,
+ totalDurationMinutes: 30,
+ videoCount: 2,
+ microCreditsCharged: 0,
+});
+const marker = (snapshotDate: string): Snapshot => ({
+ snapshotDate,
+ totalDurationMinutes: 0,
+ videoCount: 0,
+ microCreditsCharged: 0,
+});
+
+const appContext = () =>
+ Effect.runPromise(
+ Effect.gen(function* () {
+ const agents = yield* AgentManagement;
+ return yield* agents.getDeveloperAppContext(
+ User.UserId.make("user-1"),
+ "app-1",
+ );
+ }).pipe(Effect.provide(AgentManagement.Default)),
+ );
+
+beforeEach(() => {
+ state.queries = [];
+ state.snapshots = [
+ charged("2026-11-09", 40),
+ unpaid("2026-11-10"),
+ charged("2026-11-11", 45),
+ ];
+});
+
+describe("readers of daily storage snapshots ignore visit markers", () => {
+ it("the console usage history and month summary are the same with markers on other days", async () => {
+ const now = new Date("2026-11-13T12:00:00Z");
+ const before = await getInfraStorageSnapshots("app-1", "2026-10-01");
+ state.snapshots.push(marker("2026-11-12"), marker("2026-11-13"));
+
+ const after = await getInfraStorageSnapshots("app-1", "2026-10-01");
+
+ expect(after).toEqual(before);
+ expect(after.map((row) => row.snapshotDate)).toEqual([
+ "2026-11-11",
+ "2026-11-10",
+ "2026-11-09",
+ ]);
+ expect(summarizeStorageMonth(after, now)).toEqual(
+ summarizeStorageMonth(before, now),
+ );
+ expect(summarizeStorageMonth(after, now)).toEqual({
+ chargedMicroCredits: 240,
+ billedDays: 3,
+ });
+ });
+
+ it("the agent API reports the last stored minutes, not a later marker", async () => {
+ const before = await appContext();
+ state.snapshots.push(marker("2026-11-12"));
+
+ const after = await appContext();
+
+ expect(before.storageMinutes).toBe(45);
+ expect(after.storageMinutes).toBe(45);
+ expect(after).toEqual(before);
+ });
+});
diff --git a/apps/web/__tests__/unit/infra-storage-visit-marker.test.ts b/apps/web/__tests__/unit/infra-storage-visit-marker.test.ts
new file mode 100644
index 00000000000..0b0ac5ff01f
--- /dev/null
+++ b/apps/web/__tests__/unit/infra-storage-visit-marker.test.ts
@@ -0,0 +1,68 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const state = vi.hoisted(() => ({
+ queries: [] as { sql: string; params: unknown[] }[],
+}));
+
+vi.mock("@cap/database", async () => {
+ const { drizzle } = await import("drizzle-orm/mysql-proxy");
+ const client = drizzle(async (sql, params) => {
+ state.queries.push({ sql, params });
+ return { rows: [{ affectedRows: 0, insertId: 0 }] };
+ });
+ return { db: () => client };
+});
+vi.mock("@cap/database/helpers", () => ({
+ nanoId: () => "marker-id",
+ nanoIdLength: 15,
+}));
+
+import { recordStorageDayVisits } from "@/lib/infra-storage-billing";
+
+beforeEach(() => {
+ state.queries = [];
+});
+
+describe("storage day visit markers", () => {
+ it("writes every visited app of a page in one statement and changes nothing that exists", async () => {
+ await recordStorageDayVisits({
+ appIds: ["app-1", "app-2"],
+ dateKey: "2026-11-12",
+ now: new Date("2026-11-12T06:41:00Z"),
+ });
+
+ expect(state.queries).toHaveLength(1);
+ const [{ sql, params }] = state.queries as [
+ { sql: string; params: unknown[] },
+ ];
+ expect(sql).toMatch(
+ /^insert into `developer_daily_storage_snapshots` .* values \(.*\), \(.*\) on duplicate key update `appId` = `developer_daily_storage_snapshots`\.`appId`$/,
+ );
+ expect(params).toEqual([
+ "marker-id",
+ "app-1",
+ "2026-11-12",
+ 0,
+ 0,
+ 0,
+ "2026-11-12 06:41:00.000",
+ "marker-id",
+ "app-2",
+ "2026-11-12",
+ 0,
+ 0,
+ 0,
+ "2026-11-12 06:41:00.000",
+ ]);
+ });
+
+ it("does nothing for a page with no idle visits", async () => {
+ await recordStorageDayVisits({
+ appIds: [],
+ dateKey: "2026-11-12",
+ now: new Date("2026-11-12T06:41:00Z"),
+ });
+
+ expect(state.queries).toEqual([]);
+ });
+});
diff --git a/apps/web/app/(infra)/dashboard/infra/[appId]/billing/billing-view.tsx b/apps/web/app/(infra)/dashboard/infra/[appId]/billing/billing-view.tsx
index d72659cb07b..581a37e2555 100644
--- a/apps/web/app/(infra)/dashboard/infra/[appId]/billing/billing-view.tsx
+++ b/apps/web/app/(infra)/dashboard/infra/[appId]/billing/billing-view.tsx
@@ -1,6 +1,6 @@
import clsx from "clsx";
import Link from "next/link";
-import type { LedgerRow, ledgerPageWindow } from "@/lib/infra-ledger";
+import type { LedgerRow } from "@/lib/infra-ledger";
import {
INFRA_RESOLUTION_TIERS,
INFRA_SPEECH_DOLLARS_PER_HOUR,
@@ -66,7 +66,6 @@ export function BillingView({
shortfallMicroCredits,
autoTopUpFailure,
transactions,
- totalTransactions,
ledgerPage,
}: {
app: { id: string; name: string };
@@ -76,12 +75,12 @@ export function BillingView({
shortfallMicroCredits: number;
autoTopUpFailure: { day: string; reason: string } | null;
transactions: LedgerRow[];
- totalTransactions: number;
- ledgerPage: ReturnType;
+ ledgerPage: { newer: string | null; older: string | null };
}) {
const billingPath = infraAppPath(app.id, "billing");
- const ledgerHref = (page: number) =>
- `${billingPath}${page > 1 ? `?page=${page}` : ""}#${LEDGER_ANCHOR}`;
+ const ledgerHref = (direction: "before" | "after", cursor: string) =>
+ `${billingPath}?${direction}=${encodeURIComponent(cursor)}#${LEDGER_ANCHOR}`;
+ const hasTransactions = transactions.length > 0 || ledgerPage.newer !== null;
return (
<>
@@ -207,7 +206,7 @@ export function BillingView({
title="Transactions"
description="Every credit and charge, newest first. Times are in UTC."
action={
- totalTransactions > 0 && (
+ hasTransactions && (
- {ledgerPage.pages > 1 && (
+ {(ledgerPage.newer || ledgerPage.older) && (
)}
diff --git a/apps/web/app/(infra)/dashboard/infra/[appId]/billing/page.tsx b/apps/web/app/(infra)/dashboard/infra/[appId]/billing/page.tsx
index 2e7c2fc833c..6917127d2f1 100644
--- a/apps/web/app/(infra)/dashboard/infra/[appId]/billing/page.tsx
+++ b/apps/web/app/(infra)/dashboard/infra/[appId]/billing/page.tsx
@@ -3,8 +3,12 @@ import {
autoTopUpFailureText,
getAutoTopUpFailure,
} from "@/lib/infra-auto-top-up";
-import { LEDGER_PAGE_SIZE, ledgerPageWindow } from "@/lib/infra-ledger";
-import { countAppLedger, listAppLedger } from "@/lib/infra-ledger-store";
+import { LEDGER_PAGE_SIZE } from "@/lib/infra-ledger";
+import {
+ type LedgerPage,
+ listAppLedgerPage,
+ parseLedgerCursor,
+} from "@/lib/infra-ledger-store";
import { getInfraBilling } from "@/lib/infra-standing";
import { getInfraProCredit, requireInfraApp } from "../../infra-data";
import { BillingView } from "./billing-view";
@@ -13,6 +17,11 @@ export const metadata: Metadata = {
title: "Billing | Cap Infra",
};
+const EMPTY_LEDGER: LedgerPage = { rows: [], newer: null, older: null };
+
+const first = (value: string | string[] | undefined) =>
+ Array.isArray(value) ? value[0] : value;
+
const failureDay = new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
@@ -24,30 +33,26 @@ export default async function InfraBillingPage({
searchParams,
}: {
params: Promise<{ appId: string }>;
- searchParams: Promise<{ page?: string | string[] }>;
+ searchParams: Promise<{
+ before?: string | string[];
+ after?: string | string[];
+ }>;
}) {
const [{ appId }, query] = await Promise.all([params, searchParams]);
const { app } = await requireInfraApp(appId);
const account = app.creditAccount;
- const [total, proCredit, billing, failure] = await Promise.all([
- account ? countAppLedger(app.id) : Promise.resolve(0),
+ const [ledger, proCredit, billing, failure] = await Promise.all([
+ account
+ ? listAppLedgerPage(app.id, {
+ limit: LEDGER_PAGE_SIZE,
+ before: parseLedgerCursor(first(query.before)),
+ after: parseLedgerCursor(first(query.after)),
+ })
+ : Promise.resolve(EMPTY_LEDGER),
getInfraProCredit(),
getInfraBilling(app.id),
account ? getAutoTopUpFailure(account.id) : Promise.resolve(null),
]);
- const ledgerPage = ledgerPageWindow(
- Array.isArray(query.page) ? query.page[0] : query.page,
- total,
- );
- const transactions =
- total > 0
- ? (
- await listAppLedger(app.id, {
- limit: LEDGER_PAGE_SIZE,
- offset: ledgerPage.offset,
- })
- ).rows
- : [];
return (
);
}
diff --git a/apps/web/app/(infra)/dashboard/infra/[appId]/keys/page.tsx b/apps/web/app/(infra)/dashboard/infra/[appId]/keys/page.tsx
index 29f111b4628..e9fda712863 100644
--- a/apps/web/app/(infra)/dashboard/infra/[appId]/keys/page.tsx
+++ b/apps/web/app/(infra)/dashboard/infra/[appId]/keys/page.tsx
@@ -1,4 +1,8 @@
-import { infraPageMetadata, requireInfraApp } from "../../infra-data";
+import {
+ getInfraApiKeys,
+ infraPageMetadata,
+ requireInfraApp,
+} from "../../infra-data";
import { ApiKeysClient } from "./api-keys-client";
export const generateMetadata = infraPageMetadata("API keys");
@@ -10,7 +14,8 @@ export default async function InfraKeysPage({
}) {
const { appId } = await params;
const { app } = await requireInfraApp(appId);
- const secretKey = app.apiKeys.find((k) => k.keyType === "secret");
+ const apiKeys = await getInfraApiKeys(app.id);
+ const secretKey = apiKeys.find((k) => k.keyType === "secret");
return (
-
+
{children}
diff --git a/apps/web/app/(infra)/dashboard/infra/[appId]/page.tsx b/apps/web/app/(infra)/dashboard/infra/[appId]/page.tsx
index a08cce5fc5a..a4c775b6641 100644
--- a/apps/web/app/(infra)/dashboard/infra/[appId]/page.tsx
+++ b/apps/web/app/(infra)/dashboard/infra/[appId]/page.tsx
@@ -2,7 +2,6 @@ import clsx from "clsx";
import Link from "next/link";
import { infraAppPath } from "@/lib/infra-routes";
import { getInfraBilling } from "@/lib/infra-standing";
-import { getAppStorageUsage } from "@/lib/infra-storage";
import {
formatCharge,
formatCredits,
@@ -28,6 +27,7 @@ import { UploadDrop } from "../_components/upload-drop";
import { LiveVideoStatus, WatchVideos } from "../_components/video-live";
import {
getDeveloperAppVideos,
+ getInfraApiKeys,
getInfraProCredit,
getInfraStorageChargesSince,
infraPageMetadata,
@@ -56,19 +56,20 @@ export default async function InfraOverviewPage({
const { app } = await requireInfraApp(appId);
const monthStart = utcMonthStart(new Date());
- const [usage, monthStorageCharges, recentVideos, proCredit, billing] =
+ const [monthStorageCharges, recentVideos, proCredit, billing, apiKeys] =
await Promise.all([
- getAppStorageUsage(app.id),
app.creditAccount
? getInfraStorageChargesSince(app.creditAccount.id, monthStart)
: Promise.resolve(0),
getDeveloperAppVideos(app.id, { limit: 6 }),
getInfraProCredit(),
getInfraBilling(app.id),
+ getInfraApiKeys(app.id),
]);
+ const usage = billing.stored;
const balance = app.creditAccount?.balanceMicroCredits ?? 0;
- const secretKey = app.apiKeys.find((k) => k.keyType === "secret");
+ const secretKey = apiKeys.find((k) => k.keyType === "secret");
const empty = recentVideos.length === 0;
return (
diff --git a/apps/web/app/(infra)/dashboard/infra/[appId]/settings/page.tsx b/apps/web/app/(infra)/dashboard/infra/[appId]/settings/page.tsx
index 0a5d7181ce6..940302a0409 100644
--- a/apps/web/app/(infra)/dashboard/infra/[appId]/settings/page.tsx
+++ b/apps/web/app/(infra)/dashboard/infra/[appId]/settings/page.tsx
@@ -1,4 +1,8 @@
-import { infraPageMetadata, requireInfraApp } from "../../infra-data";
+import {
+ getInfraVideoCount,
+ infraPageMetadata,
+ requireInfraApp,
+} from "../../infra-data";
import { AppSettingsClient } from "./app-settings-client";
export const generateMetadata = infraPageMetadata("Settings");
@@ -10,6 +14,7 @@ export default async function InfraSettingsPage({
}) {
const { appId } = await params;
const { app } = await requireInfraApp(appId);
+ const videoCount = await getInfraVideoCount(app.id);
return (
);
}
diff --git a/apps/web/app/(infra)/dashboard/infra/_components/shell.tsx b/apps/web/app/(infra)/dashboard/infra/_components/shell.tsx
index f9cc41a118c..2ca11a5c289 100644
--- a/apps/web/app/(infra)/dashboard/infra/_components/shell.tsx
+++ b/apps/web/app/(infra)/dashboard/infra/_components/shell.tsx
@@ -71,10 +71,12 @@ const ACCOUNT_SECTIONS = new Set(["billing", "keys", "settings"]);
export function Shell({
apps,
appId,
+ balanceMicroCredits,
children,
}: {
apps: InfraAppSummary[];
appId: string;
+ balanceMicroCredits: number;
children: React.ReactNode;
}) {
const pathname = usePathname();
@@ -218,7 +220,7 @@ export function Shell({
"m-0 mt-1.5 text-[21px] leading-[1.1] tracking-[-0.04em] tabular-nums text-white [overflow-wrap:anywhere]",
)}
>
- {formatCredits(current.balanceMicroCredits)}
+ {formatCredits(balanceMicroCredits)}
;
-export type DeveloperApp = typeof developerApps.$inferSelect & {
- domains: (typeof developerAppDomains.$inferSelect)[];
- apiKeys: DeveloperApiKey[];
- creditAccount: typeof developerCreditAccounts.$inferSelect | null;
- videoCount: number;
-};
+export type InfraApp = Pick<
+ typeof developerApps.$inferSelect,
+ "id" | "name" | "environment" | "createdAt"
+>;
-export type InfraAppSummary = {
- id: string;
- name: string;
- environment: DeveloperApp["environment"];
- balanceMicroCredits: number;
-};
+export type InfraCreditAccount = typeof developerCreditAccounts.$inferSelect;
-export async function getDeveloperApps(
+export type InfraAppSummary = Pick
;
+
+export function getDeveloperApps(
user: Pick,
-): Promise {
- const apps = await db()
- .select()
+): Promise {
+ return db()
+ .select({
+ id: developerApps.id,
+ name: developerApps.name,
+ environment: developerApps.environment,
+ createdAt: developerApps.createdAt,
+ })
.from(developerApps)
.where(
and(eq(developerApps.ownerId, user.id), isNull(developerApps.deletedAt)),
)
.orderBy(desc(developerApps.createdAt));
+}
- if (apps.length === 0) return [];
-
- const appIds = apps.map((a) => a.id);
+const getInfraCreditAccount = cache(
+ async (appId: string): Promise => {
+ const [account] = await db()
+ .select()
+ .from(developerCreditAccounts)
+ .where(eq(developerCreditAccounts.appId, appId))
+ .limit(1);
+ return account ?? null;
+ },
+);
- const [allDomains, allApiKeys, allCreditAccounts, allVideoCounts] =
- await Promise.all([
- db()
- .select()
- .from(developerAppDomains)
- .where(inArray(developerAppDomains.appId, appIds)),
- db()
- .select({
- id: developerApiKeys.id,
- appId: developerApiKeys.appId,
- keyType: developerApiKeys.keyType,
- keyPrefix: developerApiKeys.keyPrefix,
- createdAt: developerApiKeys.createdAt,
- revokedAt: developerApiKeys.revokedAt,
- })
- .from(developerApiKeys)
- .where(
- and(
- inArray(developerApiKeys.appId, appIds),
- isNull(developerApiKeys.revokedAt),
- ),
+export const getInfraApiKeys = cache(
+ (appId: string): Promise =>
+ db()
+ .select({
+ id: developerApiKeys.id,
+ keyType: developerApiKeys.keyType,
+ keyPrefix: developerApiKeys.keyPrefix,
+ createdAt: developerApiKeys.createdAt,
+ revokedAt: developerApiKeys.revokedAt,
+ })
+ .from(developerApiKeys)
+ .where(
+ and(
+ eq(developerApiKeys.appId, appId),
+ isNull(developerApiKeys.revokedAt),
),
- db()
- .select()
- .from(developerCreditAccounts)
- .where(inArray(developerCreditAccounts.appId, appIds)),
- db()
- .select({
- appId: developerVideos.appId,
- count: count(),
- })
- .from(developerVideos)
- .where(
- and(
- inArray(developerVideos.appId, appIds),
- isNull(developerVideos.deletedAt),
- ),
- )
- .groupBy(developerVideos.appId),
- ]);
-
- const domainsByApp = new Map();
- for (const d of allDomains) {
- const list = domainsByApp.get(d.appId) ?? [];
- list.push(d);
- domainsByApp.set(d.appId, list);
- }
-
- const keysByApp = new Map();
- for (const k of allApiKeys) {
- const list = keysByApp.get(k.appId) ?? [];
- list.push(k);
- keysByApp.set(k.appId, list);
- }
-
- const accountsByApp = new Map(allCreditAccounts.map((c) => [c.appId, c]));
- const countsByApp = new Map(allVideoCounts.map((v) => [v.appId, v.count]));
+ ),
+);
- return apps.map((app) => ({
- ...app,
- domains: domainsByApp.get(app.id) ?? [],
- apiKeys: (keysByApp.get(app.id) ?? []).map((k) => ({
- id: k.id,
- keyType: k.keyType,
- keyPrefix: k.keyPrefix,
- createdAt: k.createdAt,
- revokedAt: k.revokedAt,
- })),
- creditAccount: accountsByApp.get(app.id) ?? null,
- videoCount: countsByApp.get(app.id) ?? 0,
- }));
+export async function getInfraVideoCount(appId: string) {
+ const [row] = await db()
+ .select({ count: count() })
+ .from(developerVideos)
+ .where(
+ and(eq(developerVideos.appId, appId), isNull(developerVideos.deletedAt)),
+ );
+ return row?.count ?? 0;
}
export async function getDeveloperAppVideos(
@@ -308,10 +271,14 @@ export async function getSelectedInfraApp() {
}
export async function requireInfraApp(appId: string) {
- const apps = await getInfraApps(infraAppPath(appId));
+ const user = await requireInfraUser(infraAppPath(appId));
+ const [apps, creditAccount] = await Promise.all([
+ getInfraAppsForUser(user.id),
+ getInfraCreditAccount(appId),
+ ]);
const app = apps.find((a) => a.id === appId);
if (!app) notFound();
- return { app, apps };
+ return { app: { ...app, creditAccount }, apps };
}
// A page title is only sent once the app is known to exist for this viewer.
@@ -330,13 +297,8 @@ export const infraPageMetadata =
return { title: `${title} | Cap Infra` };
};
-export function toInfraAppSummary(app: DeveloperApp): InfraAppSummary {
- return {
- id: app.id,
- name: app.name,
- environment: app.environment,
- balanceMicroCredits: app.creditAccount?.balanceMicroCredits ?? 0,
- };
+export function toInfraAppSummary(app: InfraApp): InfraAppSummary {
+ return { id: app.id, name: app.name, environment: app.environment };
}
export async function getInfraStorageSnapshots(
@@ -355,6 +317,7 @@ export async function getInfraStorageSnapshots(
and(
eq(developerDailyStorageSnapshots.appId, appId),
gte(developerDailyStorageSnapshots.snapshotDate, sinceDateKey),
+ gt(developerDailyStorageSnapshots.totalDurationMinutes, 0),
),
)
.orderBy(desc(developerDailyStorageSnapshots.snapshotDate));
diff --git a/apps/web/app/api/cron/developer-storage/route.ts b/apps/web/app/api/cron/developer-storage/route.ts
index 10c38da0f33..727fb01d2ed 100644
--- a/apps/web/app/api/cron/developer-storage/route.ts
+++ b/apps/web/app/api/cron/developer-storage/route.ts
@@ -4,102 +4,265 @@ import {
developerApps,
developerCreditAccounts,
developerCreditTransactions,
+ developerDailyStorageSnapshots,
} from "@cap/database/schema";
-import { and, eq, inArray, isNull } from "drizzle-orm";
-import { NextResponse } from "next/server";
+import { and, asc, eq, gt, inArray, isNotNull, isNull, lt } from "drizzle-orm";
+import { after, NextResponse } from "next/server";
+import type { StorageUsage } from "@/lib/developer-pricing";
import { runDueAutoTopUps } from "@/lib/infra-auto-top-up";
import { sendInfraOutOfCreditEmail } from "@/lib/infra-billing-emails";
import {
INFRA_GRACE_DAYS,
- latestStorageCharge,
+ latestStorageCharges,
STORAGE_DAY_REFERENCE_TYPE,
+ type StorageCharge,
} from "@/lib/infra-standing";
import { getStorageUsageByApp } from "@/lib/infra-storage";
-import { chargeStorageDay } from "@/lib/infra-storage-billing";
+import {
+ chargeStorageDay,
+ recordStorageDayVisits,
+} from "@/lib/infra-storage-billing";
import { utcDateKey } from "@/lib/infra-usage";
const BATCH_SIZE = 10;
-const GRACE_MS = INFRA_GRACE_DAYS * 24 * 60 * 60 * 1000;
+const PAGE_SIZE = 200;
+const DAY_MS = 24 * 60 * 60 * 1000;
+const GRACE_MS = INFRA_GRACE_DAYS * DAY_MS;
-export async function GET(request: Request) {
- const cronSecret = process.env.CRON_SECRET;
- if (!cronSecret) {
- return NextResponse.json(
- { error: "Server misconfiguration" },
- { status: 500 },
- );
- }
+const TOP_UPS_BEFORE_UNTIL_MS = 60_000;
+const STORAGE_UNTIL_MS = 200_000;
+const TOP_UPS_AFTER_UNTIL_MS = 240_000;
- const authHeader = request.headers.get("authorization");
- const expected = `Bearer ${cronSecret}`;
- if (
- !authHeader ||
- authHeader.length !== expected.length ||
- !timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))
- ) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
+export const MAX_STORAGE_HOPS = 24;
+const HANDOFF_TIMEOUT_MS = 10_000;
- const now = new Date();
- const today = utcDateKey(now);
+const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
+const ACCOUNT_ID_PATTERN = /^[0-9A-Za-z_-]{1,64}$/;
+const HOP_PATTERN = /^\d{1,3}$/;
- const accounts = await db()
- .select({
- id: developerCreditAccounts.id,
- appId: developerCreditAccounts.appId,
+export const maxDuration = 300;
+
+type Sweep = { date: string; after: string | null; hop: number };
+
+function readSweep(url: URL, today: string, yesterday: string): Sweep | null {
+ const date = url.searchParams.get("date");
+ const afterId = url.searchParams.get("after");
+ const hop = url.searchParams.get("hop");
+ if (date === null && afterId === null && hop === null) {
+ return { date: yesterday, after: null, hop: 0 };
+ }
+ if (date === null || hop === null || !HOP_PATTERN.test(hop)) return null;
+ const hopNumber = Number(hop);
+ if (hopNumber < 1 || hopNumber > MAX_STORAGE_HOPS) return null;
+ if (afterId !== null && !ACCOUNT_ID_PATTERN.test(afterId)) return null;
+ if (!DATE_PATTERN.test(date) || (date !== today && date !== yesterday)) {
+ return null;
+ }
+ return { date, after: afterId, hop: hopNumber };
+}
+
+function continueSweep(request: Request, authorization: string, next: Sweep) {
+ const url = new URL(request.url);
+ url.search = "";
+ url.searchParams.set("date", next.date);
+ if (next.after) url.searchParams.set("after", next.after);
+ url.searchParams.set("hop", String(next.hop));
+ after(
+ fetch(url, {
+ headers: { authorization },
+ signal: AbortSignal.timeout(HANDOFF_TIMEOUT_MS),
})
- .from(developerCreditAccounts)
- .innerJoin(
- developerApps,
- eq(developerCreditAccounts.appId, developerApps.id),
- )
- .where(isNull(developerApps.deletedAt));
+ .then((response) => {
+ if (!response.ok) {
+ console.error("[developer-storage] Continuation refused", {
+ status: response.status,
+ ...next,
+ });
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof Error && error.name === "TimeoutError") {
+ console.info(
+ "[developer-storage] Continuation still running at handoff",
+ next,
+ );
+ return;
+ }
+ console.error("[developer-storage] Continuation not started", {
+ error,
+ ...next,
+ });
+ }),
+ );
+}
- const topUpsBefore = await runDueAutoTopUps(now);
+type DateSweep = {
+ complete: boolean;
+ cursor: string | null;
+ processed: number;
+ short: number;
+};
+async function sweepDate({
+ date,
+ from,
+ finishing,
+ now,
+ outOfTime,
+}: {
+ date: string;
+ from: string | null;
+ finishing: boolean;
+ now: Date;
+ outOfTime: () => boolean;
+}): Promise {
let processed = 0;
let short = 0;
+ let cursor = from;
+ const dayEnd = finishing
+ ? new Date(Date.parse(`${date}T00:00:00.000Z`) + DAY_MS)
+ : null;
- if (accounts.length > 0) {
- const billedToday = await db()
- .select({ accountId: developerCreditTransactions.accountId })
- .from(developerCreditTransactions)
- .where(
- and(
- inArray(
- developerCreditTransactions.accountId,
- accounts.map((account) => account.id),
- ),
- eq(developerCreditTransactions.referenceId, today),
- eq(
- developerCreditTransactions.referenceType,
- STORAGE_DAY_REFERENCE_TYPE,
- ),
- ),
- );
+ while (!outOfTime()) {
+ const page: { id: string; appId: string; createdAt: Date }[] = await db()
+ .select({
+ id: developerCreditAccounts.id,
+ appId: developerCreditAccounts.appId,
+ createdAt: developerCreditAccounts.createdAt,
+ })
+ .from(developerCreditAccounts)
+ .where(cursor ? gt(developerCreditAccounts.id, cursor) : undefined)
+ .orderBy(asc(developerCreditAccounts.id))
+ .limit(PAGE_SIZE);
+ const lastAccount = page.at(-1);
+ if (!lastAccount) return { complete: true, cursor, processed, short };
+ const existing = dayEnd
+ ? page.filter(
+ (account) => new Date(account.createdAt).getTime() < dayEnd.getTime(),
+ )
+ : page;
+ const liveApps =
+ existing.length > 0
+ ? await db()
+ .select({ id: developerApps.id })
+ .from(developerApps)
+ .where(
+ and(
+ inArray(
+ developerApps.id,
+ existing.map((account) => account.appId),
+ ),
+ isNull(developerApps.deletedAt),
+ dayEnd ? lt(developerApps.createdAt, dayEnd) : undefined,
+ ),
+ )
+ : [];
+ const liveAppIds = new Set(liveApps.map((app) => app.id));
+ const accounts = existing.filter((account) =>
+ liveAppIds.has(account.appId),
+ );
+
+ const billedToday =
+ accounts.length > 0
+ ? await db()
+ .select({ accountId: developerCreditTransactions.accountId })
+ .from(developerCreditTransactions)
+ .where(
+ and(
+ inArray(
+ developerCreditTransactions.accountId,
+ accounts.map((account) => account.id),
+ ),
+ eq(developerCreditTransactions.referenceId, date),
+ eq(
+ developerCreditTransactions.referenceType,
+ STORAGE_DAY_REFERENCE_TYPE,
+ ),
+ ),
+ )
+ : [];
const billedAccounts = new Set(billedToday.map((row) => row.accountId));
- const pending = accounts.filter(
+ const unbilled = accounts.filter(
(account) => !billedAccounts.has(account.id),
);
- const usageByApp = await getStorageUsageByApp(
- pending.map((account) => account.appId),
- );
+ const settledApps =
+ unbilled.length > 0
+ ? await db()
+ .select({ appId: developerDailyStorageSnapshots.appId })
+ .from(developerDailyStorageSnapshots)
+ .where(
+ and(
+ inArray(
+ developerDailyStorageSnapshots.appId,
+ unbilled.map((account) => account.appId),
+ ),
+ eq(developerDailyStorageSnapshots.snapshotDate, date),
+ isNotNull(developerDailyStorageSnapshots.processedAt),
+ ),
+ )
+ : [];
+ const settledAppIds = new Set(settledApps.map((row) => row.appId));
+ const pending = finishing
+ ? unbilled.filter((account) => !settledAppIds.has(account.appId))
+ : unbilled;
+ const visitedWithoutSnapshot: string[] = [];
+ const recordVisits = () =>
+ recordStorageDayVisits({
+ appIds: visitedWithoutSnapshot.filter(
+ (appId) => !settledAppIds.has(appId),
+ ),
+ dateKey: date,
+ now,
+ });
+ const appIds = pending.map((account) => account.appId);
+ const usageByApp =
+ pending.length === 0
+ ? new Map()
+ : dayEnd
+ ? await getStorageUsageByApp(appIds, undefined, undefined, {
+ createdBefore: dayEnd,
+ })
+ : await getStorageUsageByApp(appIds);
+ const idleAccountIds = pending
+ .filter(
+ (account) =>
+ (usageByApp.get(account.appId)?.dailyMicroCredits ?? 1) <= 0,
+ )
+ .map((account) => account.id);
+ const lastCharges =
+ idleAccountIds.length > 0
+ ? await latestStorageCharges(db(), idleAccountIds)
+ : new Map();
for (let i = 0; i < pending.length; i += BATCH_SIZE) {
+ if (outOfTime()) {
+ await recordVisits();
+ const firstUnattempted = pending[i];
+ const position = firstUnattempted ? page.indexOf(firstUnattempted) : -1;
+ return {
+ complete: false,
+ cursor: position > 0 ? (page[position - 1]?.id ?? cursor) : cursor,
+ processed,
+ short,
+ };
+ }
const results = await Promise.allSettled(
pending.slice(i, i + BATCH_SIZE).map(async (account) => {
const usage = usageByApp.get(account.appId);
if (!usage) return null;
- if (usage.dailyMicroCredits <= 0) {
- const lastCharge = await latestStorageCharge(db(), account.id);
- if (!lastCharge?.shortSince) return null;
+ if (
+ usage.dailyMicroCredits <= 0 &&
+ !lastCharges.get(account.id)?.shortSince
+ ) {
+ return null;
}
const result = await chargeStorageDay({
appId: account.appId,
accountId: account.id,
usage,
- dateKey: today,
+ dateKey: date,
now,
+ retryUnpaid: !finishing,
});
if (
result.state === "billed" &&
@@ -116,6 +279,16 @@ export async function GET(request: Request) {
}),
);
+ results.forEach((result, index) => {
+ const appId = pending[i + index]?.appId;
+ if (
+ appId &&
+ result.status === "fulfilled" &&
+ (!result.value || result.value.state === "skipped")
+ ) {
+ visitedWithoutSnapshot.push(appId);
+ }
+ });
for (const result of results) {
if (result.status === "rejected") {
console.error("Failed to process app in cron:", result.reason);
@@ -127,18 +300,113 @@ export async function GET(request: Request) {
}
}
}
+
+ await recordVisits();
+ cursor = lastAccount.id;
+ if (page.length < PAGE_SIZE) {
+ return { complete: true, cursor, processed, short };
+ }
+ }
+ return { complete: false, cursor, processed, short };
+}
+
+export async function GET(request: Request) {
+ const cronSecret = process.env.CRON_SECRET;
+ if (!cronSecret) {
+ return NextResponse.json(
+ { error: "Server misconfiguration" },
+ { status: 500 },
+ );
+ }
+
+ const authHeader = request.headers.get("authorization");
+ const expected = `Bearer ${cronSecret}`;
+ if (
+ !authHeader ||
+ authHeader.length !== expected.length ||
+ !timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))
+ ) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const startedAt = Date.now();
+ const now = new Date(startedAt);
+ const today = utcDateKey(now);
+ const yesterday = utcDateKey(new Date(startedAt - DAY_MS));
+ const sweep = readSweep(new URL(request.url), today, yesterday);
+ if (!sweep) {
+ return NextResponse.json(
+ { error: "Invalid continuation" },
+ { status: 400 },
+ );
+ }
+ const { hop } = sweep;
+ const outOfTime = () => Date.now() - startedAt >= STORAGE_UNTIL_MS;
+
+ const topUpsBefore =
+ hop === 0
+ ? await runDueAutoTopUps(now, {
+ until: startedAt + TOP_UPS_BEFORE_UNTIL_MS,
+ })
+ : null;
+
+ const dates = sweep.date === today ? [today] : [sweep.date, today];
+ let date = sweep.date;
+ let cursor = sweep.after;
+ let complete = false;
+ let processed = 0;
+ let short = 0;
+ for (const [index, sweepingDate] of dates.entries()) {
+ date = sweepingDate;
+ const result = await sweepDate({
+ date,
+ from: index === 0 ? sweep.after : null,
+ finishing: date !== today,
+ now,
+ outOfTime,
+ });
+ processed += result.processed;
+ short += result.short;
+ cursor = result.cursor;
+ complete = result.complete;
+ if (!complete) break;
+ }
+
+ let continued = false;
+ if (!complete) {
+ if (hop < MAX_STORAGE_HOPS) {
+ continueSweep(request, authHeader, {
+ date,
+ after: cursor,
+ hop: hop + 1,
+ });
+ continued = true;
+ } else {
+ console.error("[developer-storage] Sweep stopped at the hop limit", {
+ date,
+ after: cursor,
+ hop,
+ });
+ }
}
- const topUpsAfter = await runDueAutoTopUps(now);
+ const topUpsAfter = continued
+ ? null
+ : await runDueAutoTopUps(now, {
+ until: startedAt + TOP_UPS_AFTER_UNTIL_MS,
+ });
const autoTopUps = {
- attempted: topUpsBefore.attempted + topUpsAfter.attempted,
- succeeded: topUpsBefore.succeeded + topUpsAfter.succeeded,
- failed: topUpsBefore.failed + topUpsAfter.failed,
+ attempted: (topUpsBefore?.attempted ?? 0) + (topUpsAfter?.attempted ?? 0),
+ succeeded: (topUpsBefore?.succeeded ?? 0) + (topUpsAfter?.succeeded ?? 0),
+ failed: (topUpsBefore?.failed ?? 0) + (topUpsAfter?.failed ?? 0),
};
return NextResponse.json({
success: true,
- date: today,
+ date,
+ complete,
+ continued,
+ hop,
appsProcessed: processed,
appsShort: short,
autoTopUps,
diff --git a/apps/web/app/api/developer/v1/[...route]/usage.ts b/apps/web/app/api/developer/v1/[...route]/usage.ts
index 7d9e5001298..284b6ee112e 100644
--- a/apps/web/app/api/developer/v1/[...route]/usage.ts
+++ b/apps/web/app/api/developer/v1/[...route]/usage.ts
@@ -14,7 +14,6 @@ import {
infraBillingJson,
infraStorageJson,
} from "@/lib/infra-standing";
-import { getAppStorageUsage } from "@/lib/infra-storage";
import { creditDollars } from "@/lib/infra-usage";
import { withDeveloperSecretAuth } from "../../../utils";
@@ -28,7 +27,7 @@ export const app = new Hono<{
app.get("/", async (c) => {
const appId = c.get("developerAppId");
- const [[videoStats], billing, stored, credit] = await Promise.all([
+ const [[videoStats], billing, credit] = await Promise.all([
db()
.select({
createdVideos: count(),
@@ -42,9 +41,9 @@ app.get("/", async (c) => {
),
),
getInfraBilling(appId),
- getAppStorageUsage(appId),
getSpeechUsage(appId),
]);
+ const { stored } = billing;
const { balanceMicroCredits, ...speech } = credit;
const { standing, upload } = infraBillingJson(billing);
diff --git a/apps/web/lib/infra-auto-top-up.ts b/apps/web/lib/infra-auto-top-up.ts
index bceba7b7c11..47839d7f95c 100644
--- a/apps/web/lib/infra-auto-top-up.ts
+++ b/apps/web/lib/infra-auto-top-up.ts
@@ -7,13 +7,22 @@ import {
users,
} from "@cap/database/schema";
import { stripe } from "@cap/utils";
-import { and, desc, eq, inArray, isNotNull, isNull } from "drizzle-orm";
+import {
+ and,
+ asc,
+ desc,
+ eq,
+ inArray,
+ isNotNull,
+ isNull,
+ sql,
+} from "drizzle-orm";
import type Stripe from "stripe";
import { addCreditsToAccount } from "@/lib/developer-credits";
import { developerCreditsProductId } from "@/lib/developer-credits-product";
import { MICRO_CREDITS_PER_DOLLAR } from "@/lib/developer-pricing";
import { canUseInfra } from "@/lib/infra-access";
-import { availableBalance } from "@/lib/infra-balance";
+import { availableBalance, heldTranscriptsByApp } from "@/lib/infra-balance";
import { sendInfraTopUpFailedEmail } from "@/lib/infra-billing-emails";
import { disputedSince } from "@/lib/infra-standing";
import { utcDateKey } from "@/lib/infra-usage";
@@ -30,6 +39,7 @@ export const AUTO_TOP_UP_MAX_THRESHOLD_MICRO_CREDITS =
1_000 * MICRO_CREDITS_PER_DOLLAR;
const ATTEMPT_INTERVAL_MS = 24 * 60 * 60 * 1000;
+const DUE_PAGE_SIZE = 200;
export type AutoTopUpResult =
| {
@@ -132,6 +142,31 @@ const SETTLED_REFERENCE_TYPES = [
AUTO_TOP_UP_FAILED_REFERENCE_TYPE,
];
+const ATTEMPT_LEDGER_TYPES = {
+ [AUTO_TOP_UP_REFERENCE_TYPE]: "topup",
+ [AUTO_TOP_UP_FAILED_REFERENCE_TYPE]: "topup_failed",
+ [AUTO_TOP_UP_PENDING_REFERENCE_TYPE]: "topup_pending",
+} as const;
+
+const NEVER_ATTEMPTED = "1970-01-01 00:00:00";
+
+const sqlTime = (date: Date) =>
+ date.toISOString().slice(0, 23).replace("T", " ");
+
+export function autoTopUpRetryCutoff(now: Date) {
+ return new Date(now.getTime() - ATTEMPT_INTERVAL_MS);
+}
+
+export function autoTopUpLastAttemptAt() {
+ return sql`(select max(${developerCreditTransactions.createdAt}) from ${developerCreditTransactions} where ${developerCreditTransactions.accountId} = ${developerCreditAccounts.id} and ${inArray(
+ developerCreditTransactions.type,
+ ATTEMPT_REFERENCE_TYPES.map((type) => ATTEMPT_LEDGER_TYPES[type]),
+ )} and ${inArray(
+ developerCreditTransactions.referenceType,
+ ATTEMPT_REFERENCE_TYPES,
+ )})`;
+}
+
async function lastAttempt(
accountId: string,
referenceTypes: typeof ATTEMPT_REFERENCE_TYPES = ATTEMPT_REFERENCE_TYPES,
@@ -146,6 +181,10 @@ async function lastAttempt(
.where(
and(
eq(developerCreditTransactions.accountId, accountId),
+ inArray(
+ developerCreditTransactions.type,
+ referenceTypes.map((type) => ATTEMPT_LEDGER_TYPES[type]),
+ ),
inArray(developerCreditTransactions.referenceType, referenceTypes),
),
)
@@ -365,7 +404,7 @@ export async function runAutoTopUp(
const previous = await lastAttempt(accountId);
if (
previous &&
- now.getTime() - new Date(previous.createdAt).getTime() < ATTEMPT_INTERVAL_MS
+ new Date(previous.createdAt).getTime() > autoTopUpRetryCutoff(now).getTime()
) {
return { state: "skipped", reason: "recent_attempt" };
}
@@ -443,38 +482,65 @@ export async function runAutoTopUp(
return { state: "failed", reason: paymentIntent.status };
}
-export async function runDueAutoTopUps(now: Date = new Date()) {
- const due = await db()
- .select({ id: developerCreditAccounts.id })
+type DueCursor = { attemptKey: string; id: string };
+
+function dueAutoTopUpsAfter(now: Date, cursor: DueCursor | null) {
+ const attemptKey = sql`cast(coalesce(${autoTopUpLastAttemptAt()}, ${NEVER_ATTEMPTED}) as char)`;
+ const held = heldTranscriptsByApp(db());
+ return db()
+ .select({
+ id: developerCreditAccounts.id,
+ attemptKey: sql`${attemptKey}`.as("attemptKey"),
+ })
.from(developerCreditAccounts)
.innerJoin(
developerApps,
eq(developerCreditAccounts.appId, developerApps.id),
)
+ .leftJoin(held, eq(held.appId, developerCreditAccounts.appId))
.where(
and(
eq(developerCreditAccounts.autoTopUpEnabled, true),
isNotNull(developerCreditAccounts.stripePaymentMethodId),
isNull(developerApps.deletedAt),
+ sql`${developerCreditAccounts.balanceMicroCredits} < ${developerCreditAccounts.autoTopUpThresholdMicroCredits} + coalesce(${held.heldMicroCredits}, 0)`,
+ sql`${attemptKey} <= ${sqlTime(autoTopUpRetryCutoff(now))}`,
+ cursor
+ ? sql`(${attemptKey} > ${cursor.attemptKey} or (${attemptKey} = ${cursor.attemptKey} and ${developerCreditAccounts.id} > ${cursor.id}))`
+ : undefined,
),
- );
+ )
+ .orderBy(sql`attemptKey`, asc(developerCreditAccounts.id))
+ .limit(DUE_PAGE_SIZE);
+}
+export async function runDueAutoTopUps(
+ now: Date = new Date(),
+ { until = Number.POSITIVE_INFINITY }: { until?: number } = {},
+) {
const summary = { attempted: 0, succeeded: 0, failed: 0 };
- for (const account of due) {
- try {
- const result = await runAutoTopUp(account.id, now);
- if (result.state === "skipped") continue;
- summary.attempted += 1;
- if (result.state === "succeeded") summary.succeeded += 1;
- if (result.state === "failed") summary.failed += 1;
- } catch (error) {
- console.error("[infra-billing] Auto top-up failed", {
- accountId: account.id,
- error,
- });
+ let cursor: DueCursor | null = null;
+ for (;;) {
+ const due: DueCursor[] = await dueAutoTopUpsAfter(now, cursor);
+ for (const account of due) {
+ if (Date.now() >= until) return summary;
+ try {
+ const result = await runAutoTopUp(account.id, now);
+ if (result.state === "skipped") continue;
+ summary.attempted += 1;
+ if (result.state === "succeeded") summary.succeeded += 1;
+ if (result.state === "failed") summary.failed += 1;
+ } catch (error) {
+ console.error("[infra-billing] Auto top-up failed", {
+ accountId: account.id,
+ error,
+ });
+ }
}
+ const last = due.at(-1);
+ if (!last || due.length < DUE_PAGE_SIZE) return summary;
+ cursor = last;
}
- return summary;
}
export async function settleAutoTopUpPaymentIntent(
diff --git a/apps/web/lib/infra-balance.ts b/apps/web/lib/infra-balance.ts
index f25bbb41e6f..828c7692e1c 100644
--- a/apps/web/lib/infra-balance.ts
+++ b/apps/web/lib/infra-balance.ts
@@ -91,3 +91,19 @@ export async function availableBalance(
(await heldTranscriptMicroCredits(executor, appId))
);
}
+
+export const heldTranscriptsByApp = (
+ executor: Pick,
+) =>
+ executor
+ .select({
+ appId: developerTranscripts.appId,
+ heldMicroCredits:
+ sql`COALESCE(SUM(${developerTranscripts.reservedMicroCredits}), 0)`.as(
+ "heldMicroCredits",
+ ),
+ })
+ .from(developerTranscripts)
+ .where(eq(developerTranscripts.status, "processing"))
+ .groupBy(developerTranscripts.appId)
+ .as("held");
diff --git a/apps/web/lib/infra-ledger-store.ts b/apps/web/lib/infra-ledger-store.ts
index 0e97f319d06..7a972f93e9b 100644
--- a/apps/web/lib/infra-ledger-store.ts
+++ b/apps/web/lib/infra-ledger-store.ts
@@ -3,7 +3,7 @@ import {
developerCreditAccounts,
developerCreditTransactions,
} from "@cap/database/schema";
-import { and, count, desc, eq, lt, or } from "drizzle-orm";
+import { and, asc, desc, eq, gt, lt, or } from "drizzle-orm";
import { type LedgerRow, orderLedgerRows } from "@/lib/infra-ledger";
const EXPORT_BATCH_SIZE = 1_000;
@@ -36,6 +36,46 @@ const newestFirst = [
desc(developerCreditTransactions.id),
] as const;
+const oldestFirst = [
+ asc(developerCreditTransactions.createdAt),
+ asc(developerCreditTransactions.id),
+] as const;
+
+export type LedgerCursor = { createdAt: Date; id: string };
+
+const CURSOR_PATTERN = /^(\d{1,15})\.([0-9a-z]{1,32})$/;
+
+export function formatLedgerCursor(row: LedgerCursor) {
+ return `${new Date(row.createdAt).getTime()}.${row.id}`;
+}
+
+export function parseLedgerCursor(
+ raw: string | null | undefined,
+): LedgerCursor | null {
+ const match = raw ? CURSOR_PATTERN.exec(raw) : null;
+ if (!match?.[1] || !match[2]) return null;
+ const createdAt = new Date(Number(match[1]));
+ return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id: match[2] };
+}
+
+const olderThan = (cursor: LedgerCursor) =>
+ or(
+ lt(developerCreditTransactions.createdAt, cursor.createdAt),
+ and(
+ eq(developerCreditTransactions.createdAt, cursor.createdAt),
+ lt(developerCreditTransactions.id, cursor.id),
+ ),
+ );
+
+const newerThan = (cursor: LedgerCursor) =>
+ or(
+ gt(developerCreditTransactions.createdAt, cursor.createdAt),
+ and(
+ eq(developerCreditTransactions.createdAt, cursor.createdAt),
+ gt(developerCreditTransactions.id, cursor.id),
+ ),
+ );
+
export async function listAppLedger(
appId: string,
{ limit, offset }: { limit: number; offset: number },
@@ -51,39 +91,64 @@ export async function listAppLedger(
};
}
-export async function countAppLedger(appId: string) {
- const [row] = await db()
- .select({ total: count() })
- .from(developerCreditTransactions)
- .innerJoin(
- developerCreditAccounts,
- eq(developerCreditTransactions.accountId, developerCreditAccounts.id),
- )
- .where(ofApp(appId));
- return Number(row?.total ?? 0) || 0;
+export type LedgerPage = {
+ rows: LedgerRow[];
+ newer: string | null;
+ older: string | null;
+};
+
+export async function listAppLedgerPage(
+ appId: string,
+ {
+ limit,
+ before = null,
+ after = null,
+ }: {
+ limit: number;
+ before?: LedgerCursor | null;
+ after?: LedgerCursor | null;
+ },
+): Promise {
+ if (after) {
+ const fetched: LedgerRow[] = await appLedger()
+ .where(and(ofApp(appId), newerThan(after)))
+ .orderBy(...oldestFirst)
+ .limit(limit + 1);
+ if (fetched.length >= limit) {
+ const page = fetched.slice(0, limit).reverse();
+ const newest = page[0];
+ const oldest = page.at(-1);
+ return {
+ rows: orderLedgerRows(page),
+ newer:
+ fetched.length > limit && newest ? formatLedgerCursor(newest) : null,
+ older: oldest ? formatLedgerCursor(oldest) : null,
+ };
+ }
+ }
+
+ const cursor = after ? null : before;
+ const fetched: LedgerRow[] = await appLedger()
+ .where(and(ofApp(appId), cursor ? olderThan(cursor) : undefined))
+ .orderBy(...newestFirst)
+ .limit(limit + 1);
+ const page = fetched.slice(0, limit);
+ const oldest = page.at(-1);
+ return {
+ rows: orderLedgerRows(page),
+ newer: cursor ? formatLedgerCursor(page[0] ?? cursor) : null,
+ older: fetched.length > limit && oldest ? formatLedgerCursor(oldest) : null,
+ };
}
export async function* readAppLedger(
appId: string,
batchSize: number = EXPORT_BATCH_SIZE,
): AsyncGenerator {
- let cursor: { createdAt: Date; id: string } | null = null;
+ let cursor: LedgerCursor | null = null;
for (;;) {
const rows: LedgerRow[] = await appLedger()
- .where(
- and(
- ofApp(appId),
- cursor
- ? or(
- lt(developerCreditTransactions.createdAt, cursor.createdAt),
- and(
- eq(developerCreditTransactions.createdAt, cursor.createdAt),
- lt(developerCreditTransactions.id, cursor.id),
- ),
- )
- : undefined,
- ),
- )
+ .where(and(ofApp(appId), cursor ? olderThan(cursor) : undefined))
.orderBy(...newestFirst)
.limit(batchSize);
const last = rows.at(-1);
diff --git a/apps/web/lib/infra-ledger.ts b/apps/web/lib/infra-ledger.ts
index 004d2629fd8..37b03cdf727 100644
--- a/apps/web/lib/infra-ledger.ts
+++ b/apps/web/lib/infra-ledger.ts
@@ -233,25 +233,6 @@ export function orderLedgerRows(rows: readonly Row[]) {
return ordered;
}
-export function ledgerPageWindow(
- rawPage: string | undefined,
- total: number,
- size: number = LEDGER_PAGE_SIZE,
-) {
- const pages = Math.max(1, Math.ceil(total / size));
- const requested =
- rawPage !== undefined && /^\d+$/.test(rawPage) ? Number(rawPage) : 1;
- const page = Math.min(Math.max(1, requested), pages);
- const offset = (page - 1) * size;
- return {
- page,
- pages,
- offset,
- from: total === 0 ? 0 : offset + 1,
- to: Math.min(total, offset + size),
- };
-}
-
export const LEDGER_CSV_COLUMNS = [
"id",
"time_utc",
diff --git a/apps/web/lib/infra-standing.ts b/apps/web/lib/infra-standing.ts
index 3b8fb6b985f..77b9ee4150c 100644
--- a/apps/web/lib/infra-standing.ts
+++ b/apps/web/lib/infra-standing.ts
@@ -3,7 +3,7 @@ import {
developerCreditAccounts,
developerCreditTransactions,
} from "@cap/database/schema";
-import { and, desc, eq, gt, gte, inArray } from "drizzle-orm";
+import { and, desc, eq, gt, gte, inArray, lte, sql } from "drizzle-orm";
import { cache } from "react";
import {
type BillableVideo,
@@ -12,8 +12,8 @@ import {
uploadAdmissionMicroCredits,
} from "@/lib/developer-pricing";
import { availableBalance } from "@/lib/infra-balance";
-import { getAppStorageUsage } from "@/lib/infra-storage";
-import { formatCredits, formatNeed } from "@/lib/infra-usage";
+import { type AppStorage, getAppStorage } from "@/lib/infra-storage";
+import { formatCredits, formatNeed, utcDateKey } from "@/lib/infra-usage";
export const INFRA_GRACE_DAYS = 7;
export const STORAGE_DAY_REFERENCE_TYPE = "storage_day" as const;
@@ -111,6 +111,18 @@ export function resolveStanding({
};
}
+const storageDayCeiling = () =>
+ utcDateKey(new Date(Date.now() + 24 * 60 * 60 * 1000));
+
+const storageDayRows = () =>
+ and(
+ eq(developerCreditTransactions.type, "storage_daily"),
+ eq(developerCreditTransactions.referenceType, STORAGE_DAY_REFERENCE_TYPE),
+ );
+
+const latestStorageDay = (accountId: string, ceiling: string) =>
+ sql`(select ${developerCreditTransactions.accountId}, ${developerCreditTransactions.referenceId} from ${developerCreditTransactions} where ${developerCreditTransactions.accountId} = ${accountId} and ${developerCreditTransactions.referenceType} = ${STORAGE_DAY_REFERENCE_TYPE} and ${developerCreditTransactions.referenceId} <= ${ceiling} order by ${developerCreditTransactions.referenceId} desc limit 1)`;
+
export async function latestStorageCharge(
executor: InfraDbExecutor,
accountId: string,
@@ -124,14 +136,50 @@ export async function latestStorageCharge(
.where(
and(
eq(developerCreditTransactions.accountId, accountId),
- eq(developerCreditTransactions.type, "storage_daily"),
+ storageDayRows(),
+ lte(developerCreditTransactions.referenceId, storageDayCeiling()),
),
)
- .orderBy(desc(developerCreditTransactions.createdAt))
+ .orderBy(
+ desc(developerCreditTransactions.referenceId),
+ desc(developerCreditTransactions.id),
+ )
.limit(1);
return row ? parseStorageCharge(row) : null;
}
+export async function latestStorageCharges(
+ executor: InfraDbExecutor,
+ accountIds: readonly string[],
+) {
+ const charges = new Map();
+ if (accountIds.length === 0) return charges;
+ const ceiling = storageDayCeiling();
+ const rows = await executor
+ .select({
+ id: developerCreditTransactions.id,
+ accountId: developerCreditTransactions.accountId,
+ createdAt: developerCreditTransactions.createdAt,
+ metadata: developerCreditTransactions.metadata,
+ })
+ .from(developerCreditTransactions)
+ .where(
+ and(
+ storageDayRows(),
+ sql`(${developerCreditTransactions.accountId}, ${developerCreditTransactions.referenceId}) in (select latest.accountId, latest.referenceId from (${sql.join(
+ accountIds.map((accountId) => latestStorageDay(accountId, ceiling)),
+ sql` union all `,
+ )}) latest)`,
+ ),
+ );
+ for (const row of [...rows].sort((a, b) => (a.id < b.id ? 1 : -1))) {
+ if (!charges.has(row.accountId)) {
+ charges.set(row.accountId, parseStorageCharge(row));
+ }
+ }
+ return charges;
+}
+
export async function restoredSinceCharge(
executor: InfraDbExecutor,
accountId: string,
@@ -315,18 +363,19 @@ export async function getInfraBilling(
newVideo?: BillableVideo | null,
now: Date = new Date(),
executor?: InfraDbExecutor,
- options: { excludeVideoId?: string } = {},
+ options: { excludeVideoId?: string; storage?: AppStorage } = {},
) {
- const counted = { ...options, includeProcessing: true };
- const [{ account, lastCharge, standing }, usage] = executor
+ const [{ account, lastCharge, standing }, storage] = executor
? [
await appStanding(appId, now, executor),
- await getAppStorageUsage(appId, newVideo, executor, counted),
+ options.storage ??
+ (await getAppStorage(appId, newVideo, executor, options)),
]
: await Promise.all([
appStanding(appId, now),
- getAppStorageUsage(appId, newVideo, undefined, counted),
+ options.storage ?? getAppStorage(appId, newVideo, undefined, options),
]);
+ const usage = storage.counted;
const admission = uploadAdmission({
balanceMicroCredits: account?.balanceMicroCredits ?? 0,
standing,
@@ -336,6 +385,7 @@ export async function getInfraBilling(
return {
standing,
usage,
+ stored: storage.stored,
admission: account
? admission
: {
@@ -351,7 +401,7 @@ export async function getInfraUploadAdmission(
appId: string,
newVideo?: BillableVideo | null,
executor?: InfraDbExecutor,
- options: { excludeVideoId?: string } = {},
+ options: { excludeVideoId?: string; storage?: AppStorage } = {},
): Promise {
return (await getInfraBilling(appId, newVideo, new Date(), executor, options))
.admission;
diff --git a/apps/web/lib/infra-storage-billing.ts b/apps/web/lib/infra-storage-billing.ts
index 32c4bf1db60..113bea85439 100644
--- a/apps/web/lib/infra-storage-billing.ts
+++ b/apps/web/lib/infra-storage-billing.ts
@@ -53,18 +53,48 @@ export function settleStorageDay({
};
}
+export async function recordStorageDayVisits({
+ appIds,
+ dateKey,
+ now,
+}: {
+ appIds: readonly string[];
+ dateKey: string;
+ now: Date;
+}) {
+ if (appIds.length === 0) return;
+ await db()
+ .insert(developerDailyStorageSnapshots)
+ .values(
+ appIds.map((appId) => ({
+ id: nanoId(),
+ appId,
+ snapshotDate: dateKey,
+ totalDurationMinutes: 0,
+ videoCount: 0,
+ microCreditsCharged: 0,
+ processedAt: now,
+ })),
+ )
+ .onDuplicateKeyUpdate({
+ set: { appId: sql`${developerDailyStorageSnapshots.appId}` },
+ });
+}
+
export async function chargeStorageDay({
appId,
accountId,
usage,
dateKey,
now,
+ retryUnpaid = true,
}: {
appId: string;
accountId: string;
usage: StorageUsage;
dateKey: string;
now: Date;
+ retryUnpaid?: boolean;
}): Promise {
return db().transaction(async (tx) => {
const [account] = await tx
@@ -106,7 +136,10 @@ export async function chargeStorageDay({
),
)
.limit(1);
- if (snapshot?.processedAt && Number(snapshot.microCreditsCharged) > 0) {
+ if (
+ snapshot?.processedAt &&
+ (Number(snapshot.microCreditsCharged) > 0 || !retryUnpaid)
+ ) {
return { state: "skipped" as const };
}
diff --git a/apps/web/lib/infra-storage.ts b/apps/web/lib/infra-storage.ts
index 21f380974ef..6cd2210c247 100644
--- a/apps/web/lib/infra-storage.ts
+++ b/apps/web/lib/infra-storage.ts
@@ -1,6 +1,6 @@
import { db } from "@cap/database";
import { developerVideos } from "@cap/database/schema";
-import { and, inArray, isNotNull, isNull, ne, sql } from "drizzle-orm";
+import { and, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
import {
type BillableVideo,
billableVideo,
@@ -24,12 +24,14 @@ const APP_ID_CHUNK = 500;
export type StorageUsageOptions = {
includeProcessing?: boolean;
excludeVideoId?: string;
+ createdBefore?: Date;
};
async function storedVideoGroups(
appIds: readonly string[],
executor: InfraDbExecutor,
excludeVideoId?: string,
+ createdBefore?: Date,
) {
const groups: StoredVideoGroup[] = [];
for (let start = 0; start < appIds.length; start += APP_ID_CHUNK) {
@@ -55,6 +57,9 @@ async function storedVideoGroups(
isNull(developerVideos.deletedAt),
isNotNull(developerVideos.duration),
excludeVideoId ? ne(developerVideos.id, excludeVideoId) : undefined,
+ createdBefore
+ ? lt(developerVideos.createdAt, createdBefore)
+ : undefined,
),
)
.groupBy(
@@ -85,6 +90,7 @@ export async function getStorageUsageByApp(
appIds,
executor,
options.excludeVideoId,
+ options.createdBefore,
)) {
entriesByApp.get(group.appId)?.push({
video: billableVideo({
@@ -123,3 +129,34 @@ export async function getAppStorageUsage(
);
return usageByApp.get(appId) ?? storageUsage([]);
}
+
+export type AppStorage = { stored: StorageUsage; counted: StorageUsage };
+
+export async function getAppStorage(
+ appId: string,
+ newVideo?: BillableVideo | null,
+ executor: InfraDbExecutor = db(),
+ options: Pick = {},
+): Promise {
+ const groups = await storedVideoGroups(
+ [appId],
+ executor,
+ options.excludeVideoId,
+ );
+ const usage = (includeProcessing: boolean) =>
+ storageUsage([
+ ...groups.map((group) => ({
+ video: billableVideo({
+ duration: Number(group.seconds),
+ width: group.width,
+ height: group.height,
+ status: group.status,
+ maxResolutionTier: group.maxResolutionTier,
+ includeProcessing,
+ }),
+ count: Number(group.count) || 0,
+ })),
+ ...(newVideo ? [{ video: newVideo, count: 1 }] : []),
+ ]);
+ return { stored: usage(false), counted: usage(true) };
+}
diff --git a/packages/database/migrations/0056_developer_billing_read_indexes.sql b/packages/database/migrations/0056_developer_billing_read_indexes.sql
new file mode 100644
index 00000000000..4e35439f151
--- /dev/null
+++ b/packages/database/migrations/0056_developer_billing_read_indexes.sql
@@ -0,0 +1,4 @@
+CREATE INDEX `auto_top_up_idx` ON `developer_credit_accounts` (`autoTopUpEnabled`);--> statement-breakpoint
+CREATE INDEX `reference_type_id_idx` ON `developer_credit_transactions` (`referenceType`,`referenceId`);--> statement-breakpoint
+CREATE INDEX `dev_transcripts_app_completed_idx` ON `developer_transcripts` (`appId`,`status`,`completedAt`,`durationSeconds`,`billedMicroCredits`);--> statement-breakpoint
+CREATE INDEX `dev_transcripts_app_status_held_idx` ON `developer_transcripts` (`appId`,`status`,`createdAt`,`deletedAt`,`reservedMicroCredits`);
\ No newline at end of file
diff --git a/packages/database/migrations/meta/0056_snapshot.json b/packages/database/migrations/meta/0056_snapshot.json
new file mode 100644
index 00000000000..4c06422ec84
--- /dev/null
+++ b/packages/database/migrations/meta/0056_snapshot.json
@@ -0,0 +1,5866 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "7568082e-2748-491f-897f-16005b895884",
+ "prevId": "2773f4a4-e037-4a2a-9537-3e419186570c",
+ "tables": {
+ "accounts": {
+ "name": "accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_in": {
+ "name": "expires_in",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "refresh_token_expires_in": {
+ "name": "refresh_token_expires_in",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "tempColumn": {
+ "name": "tempColumn",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": ["userId"],
+ "isUnique": false
+ },
+ "provider_account_id_idx": {
+ "name": "provider_account_id_idx",
+ "columns": ["providerAccountId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "accounts_id": {
+ "name": "accounts_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "agent_api_authorization_codes": {
+ "name": "agent_api_authorization_codes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "codeHash": {
+ "name": "codeHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "codeChallenge": {
+ "name": "codeChallenge",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "redirectUri": {
+ "name": "redirectUri",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "consumedAt": {
+ "name": "consumedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "code_hash_idx": {
+ "name": "code_hash_idx",
+ "columns": ["codeHash"],
+ "isUnique": true
+ },
+ "expires_at_idx": {
+ "name": "expires_at_idx",
+ "columns": ["expiresAt"],
+ "isUnique": false
+ },
+ "user_created_at_idx": {
+ "name": "user_created_at_idx",
+ "columns": ["userId", "createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "agent_api_authorization_codes_id": {
+ "name": "agent_api_authorization_codes_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "agent_api_idempotency": {
+ "name": "agent_api_idempotency",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "operation": {
+ "name": "operation",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "keyHash": {
+ "name": "keyHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "requestHash": {
+ "name": "requestHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "statusCode": {
+ "name": "statusCode",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "response": {
+ "name": "response",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "user_operation_key_idx": {
+ "name": "user_operation_key_idx",
+ "columns": ["userId", "operation", "keyHash"],
+ "isUnique": true
+ },
+ "expires_at_idx": {
+ "name": "expires_at_idx",
+ "columns": ["expiresAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "agent_api_idempotency_id": {
+ "name": "agent_api_idempotency_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "agent_api_keys": {
+ "name": "agent_api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tokenHash": {
+ "name": "tokenHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Cap CLI'"
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revokedAt": {
+ "name": "revokedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "lastUsedAt": {
+ "name": "lastUsedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "token_hash_idx": {
+ "name": "token_hash_idx",
+ "columns": ["tokenHash"],
+ "isUnique": true
+ },
+ "user_created_at_idx": {
+ "name": "user_created_at_idx",
+ "columns": ["userId", "createdAt"],
+ "isUnique": false
+ },
+ "expires_at_idx": {
+ "name": "expires_at_idx",
+ "columns": ["expiresAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "agent_api_keys_id": {
+ "name": "agent_api_keys_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "agent_api_operations": {
+ "name": "agent_api_operations",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "resourceId": {
+ "name": "resourceId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "resultResourceId": {
+ "name": "resultResourceId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'queued'"
+ },
+ "payload": {
+ "name": "payload",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "result": {
+ "name": "result",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "errorCode": {
+ "name": "errorCode",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "errorMessage": {
+ "name": "errorMessage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "completedAt": {
+ "name": "completedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_created_at_idx": {
+ "name": "user_created_at_idx",
+ "columns": ["userId", "createdAt"],
+ "isUnique": false
+ },
+ "state_updated_at_idx": {
+ "name": "state_updated_at_idx",
+ "columns": ["state", "updatedAt"],
+ "isUnique": false
+ },
+ "resource_id_idx": {
+ "name": "resource_id_idx",
+ "columns": ["resourceId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "agent_api_operations_id": {
+ "name": "agent_api_operations_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "auth_api_keys": {
+ "name": "auth_api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(36)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unknown'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "user_id_created_at_idx": {
+ "name": "user_id_created_at_idx",
+ "columns": ["userId", "createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "auth_api_keys_id": {
+ "name": "auth_api_keys_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "comments": {
+ "name": "comments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(6)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "float",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "authorId": {
+ "name": "authorId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "parentCommentId": {
+ "name": "parentCommentId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mediaKey": {
+ "name": "mediaKey",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mediaDuration": {
+ "name": "mediaDuration",
+ "type": "float",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mediaMeta": {
+ "name": "mediaMeta",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "video_type_created_idx": {
+ "name": "video_type_created_idx",
+ "columns": ["videoId", "type", "createdAt", "id"],
+ "isUnique": false
+ },
+ "author_id_idx": {
+ "name": "author_id_idx",
+ "columns": ["authorId"],
+ "isUnique": false
+ },
+ "parent_comment_id_idx": {
+ "name": "parent_comment_id_idx",
+ "columns": ["parentCommentId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "comments_id": {
+ "name": "comments_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "developer_api_keys": {
+ "name": "developer_api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "appId": {
+ "name": "appId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "keyType": {
+ "name": "keyType",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "keyPrefix": {
+ "name": "keyPrefix",
+ "type": "varchar(12)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "keyHash": {
+ "name": "keyHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "encryptedKey": {
+ "name": "encryptedKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "lastUsedAt": {
+ "name": "lastUsedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "revokedAt": {
+ "name": "revokedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "key_hash_idx": {
+ "name": "key_hash_idx",
+ "columns": ["keyHash"],
+ "isUnique": true
+ },
+ "app_key_type_idx": {
+ "name": "app_key_type_idx",
+ "columns": ["appId", "keyType"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "developer_api_keys_appId_developer_apps_id_fk": {
+ "name": "developer_api_keys_appId_developer_apps_id_fk",
+ "tableFrom": "developer_api_keys",
+ "tableTo": "developer_apps",
+ "columnsFrom": ["appId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "developer_api_keys_id": {
+ "name": "developer_api_keys_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "developer_app_domains": {
+ "name": "developer_app_domains",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "appId": {
+ "name": "appId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "varchar(253)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "developer_app_domains_appId_developer_apps_id_fk": {
+ "name": "developer_app_domains_appId_developer_apps_id_fk",
+ "tableFrom": "developer_app_domains",
+ "tableTo": "developer_apps",
+ "columnsFrom": ["appId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "developer_app_domains_id": {
+ "name": "developer_app_domains_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {
+ "app_domain_unique": {
+ "name": "app_domain_unique",
+ "columns": ["appId", "domain"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "developer_apps": {
+ "name": "developer_apps",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ownerId": {
+ "name": "ownerId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "environment": {
+ "name": "environment",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "logoUrl": {
+ "name": "logoUrl",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deletedAt": {
+ "name": "deletedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "owner_deleted_idx": {
+ "name": "owner_deleted_idx",
+ "columns": ["ownerId", "deletedAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "developer_apps_id": {
+ "name": "developer_apps_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "developer_credit_accounts": {
+ "name": "developer_credit_accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "appId": {
+ "name": "appId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ownerId": {
+ "name": "ownerId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "balanceMicroCredits": {
+ "name": "balanceMicroCredits",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "stripeCustomerId": {
+ "name": "stripeCustomerId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripePaymentMethodId": {
+ "name": "stripePaymentMethodId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "autoTopUpEnabled": {
+ "name": "autoTopUpEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "autoTopUpThresholdMicroCredits": {
+ "name": "autoTopUpThresholdMicroCredits",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "autoTopUpAmountCents": {
+ "name": "autoTopUpAmountCents",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "app_id_unique": {
+ "name": "app_id_unique",
+ "columns": ["appId"],
+ "isUnique": true
+ },
+ "owner_idx": {
+ "name": "owner_idx",
+ "columns": ["ownerId"],
+ "isUnique": false
+ },
+ "auto_top_up_idx": {
+ "name": "auto_top_up_idx",
+ "columns": ["autoTopUpEnabled"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "developer_credit_accounts_appId_developer_apps_id_fk": {
+ "name": "developer_credit_accounts_appId_developer_apps_id_fk",
+ "tableFrom": "developer_credit_accounts",
+ "tableTo": "developer_apps",
+ "columnsFrom": ["appId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "developer_credit_accounts_id": {
+ "name": "developer_credit_accounts_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "developer_credit_transactions": {
+ "name": "developer_credit_transactions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "accountId": {
+ "name": "accountId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "amountMicroCredits": {
+ "name": "amountMicroCredits",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "balanceAfterMicroCredits": {
+ "name": "balanceAfterMicroCredits",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "referenceId": {
+ "name": "referenceId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "referenceType": {
+ "name": "referenceType",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "account_type_created_idx": {
+ "name": "account_type_created_idx",
+ "columns": ["accountId", "type", "createdAt"],
+ "isUnique": false
+ },
+ "account_created_idx": {
+ "name": "account_created_idx",
+ "columns": ["accountId", "createdAt"],
+ "isUnique": false
+ },
+ "account_ref_dedup_idx": {
+ "name": "account_ref_dedup_idx",
+ "columns": ["accountId", "referenceId", "referenceType"],
+ "isUnique": false
+ },
+ "reference_type_id_idx": {
+ "name": "reference_type_id_idx",
+ "columns": ["referenceType", "referenceId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "dev_credit_txn_account_fk": {
+ "name": "dev_credit_txn_account_fk",
+ "tableFrom": "developer_credit_transactions",
+ "tableTo": "developer_credit_accounts",
+ "columnsFrom": ["accountId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "developer_credit_transactions_id": {
+ "name": "developer_credit_transactions_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "developer_daily_storage_snapshots": {
+ "name": "developer_daily_storage_snapshots",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "appId": {
+ "name": "appId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "snapshotDate": {
+ "name": "snapshotDate",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "totalDurationMinutes": {
+ "name": "totalDurationMinutes",
+ "type": "float",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "videoCount": {
+ "name": "videoCount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "microCreditsCharged": {
+ "name": "microCreditsCharged",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "processedAt": {
+ "name": "processedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "developer_daily_storage_snapshots_appId_developer_apps_id_fk": {
+ "name": "developer_daily_storage_snapshots_appId_developer_apps_id_fk",
+ "tableFrom": "developer_daily_storage_snapshots",
+ "tableTo": "developer_apps",
+ "columnsFrom": ["appId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "developer_daily_storage_snapshots_id": {
+ "name": "developer_daily_storage_snapshots_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {
+ "app_date_unique": {
+ "name": "app_date_unique",
+ "columns": ["appId", "snapshotDate"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "developer_transcripts": {
+ "name": "developer_transcripts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "appId": {
+ "name": "appId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "externalUserId": {
+ "name": "externalUserId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Untitled'"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sourceType": {
+ "name": "sourceType",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sourceUrl": {
+ "name": "sourceUrl",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "s3Key": {
+ "name": "s3Key",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resultKey": {
+ "name": "resultKey",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sourceBytes": {
+ "name": "sourceBytes",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "durationSeconds": {
+ "name": "durationSeconds",
+ "type": "float",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "requestedLanguage": {
+ "name": "requestedLanguage",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "language": {
+ "name": "language",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "text": {
+ "name": "text",
+ "type": "mediumtext",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "wordCount": {
+ "name": "wordCount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reservedMicroCredits": {
+ "name": "reservedMicroCredits",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "billedMicroCredits": {
+ "name": "billedMicroCredits",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "errorCode": {
+ "name": "errorCode",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "errorMessage": {
+ "name": "errorMessage",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "runId": {
+ "name": "runId",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "queuedAt": {
+ "name": "queuedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "startedAt": {
+ "name": "startedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "completedAt": {
+ "name": "completedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deletedAt": {
+ "name": "deletedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "dev_transcripts_app_list_idx": {
+ "name": "dev_transcripts_app_list_idx",
+ "columns": ["appId", "deletedAt", "createdAt", "id"],
+ "isUnique": false
+ },
+ "dev_transcripts_app_user_idx": {
+ "name": "dev_transcripts_app_user_idx",
+ "columns": ["appId", "externalUserId", "createdAt"],
+ "isUnique": false
+ },
+ "dev_transcripts_app_status_idx": {
+ "name": "dev_transcripts_app_status_idx",
+ "columns": ["appId", "status", "createdAt"],
+ "isUnique": false
+ },
+ "dev_transcripts_status_updated_idx": {
+ "name": "dev_transcripts_status_updated_idx",
+ "columns": ["status", "updatedAt"],
+ "isUnique": false
+ },
+ "dev_transcripts_app_completed_idx": {
+ "name": "dev_transcripts_app_completed_idx",
+ "columns": [
+ "appId",
+ "status",
+ "completedAt",
+ "durationSeconds",
+ "billedMicroCredits"
+ ],
+ "isUnique": false
+ },
+ "dev_transcripts_app_status_held_idx": {
+ "name": "dev_transcripts_app_status_held_idx",
+ "columns": [
+ "appId",
+ "status",
+ "createdAt",
+ "deletedAt",
+ "reservedMicroCredits"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "developer_transcripts_appId_developer_apps_id_fk": {
+ "name": "developer_transcripts_appId_developer_apps_id_fk",
+ "tableFrom": "developer_transcripts",
+ "tableTo": "developer_apps",
+ "columnsFrom": ["appId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "developer_transcripts_id": {
+ "name": "developer_transcripts_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "developer_videos": {
+ "name": "developer_videos",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "appId": {
+ "name": "appId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "externalUserId": {
+ "name": "externalUserId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Untitled'"
+ },
+ "duration": {
+ "name": "duration",
+ "type": "float",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "width": {
+ "name": "width",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "height": {
+ "name": "height",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "fps": {
+ "name": "fps",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "s3Key": {
+ "name": "s3Key",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "transcriptionStatus": {
+ "name": "transcriptionStatus",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'uploading'"
+ },
+ "playbackId": {
+ "name": "playbackId",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "maxResolutionTier": {
+ "name": "maxResolutionTier",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'1080p'"
+ },
+ "renditions": {
+ "name": "renditions",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "storedBytes": {
+ "name": "storedBytes",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "errorReason": {
+ "name": "errorReason",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "processing": {
+ "name": "processing",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deletedAt": {
+ "name": "deletedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "filesDeletedAt": {
+ "name": "filesDeletedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "app_created_idx": {
+ "name": "app_created_idx",
+ "columns": ["appId", "createdAt"],
+ "isUnique": false
+ },
+ "app_user_idx": {
+ "name": "app_user_idx",
+ "columns": ["appId", "externalUserId"],
+ "isUnique": false
+ },
+ "app_deleted_idx": {
+ "name": "app_deleted_idx",
+ "columns": ["appId", "deletedAt"],
+ "isUnique": false
+ },
+ "playback_id_idx": {
+ "name": "playback_id_idx",
+ "columns": ["playbackId"],
+ "isUnique": true
+ },
+ "files_pending_idx": {
+ "name": "files_pending_idx",
+ "columns": ["filesDeletedAt", "deletedAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "developer_videos_appId_developer_apps_id_fk": {
+ "name": "developer_videos_appId_developer_apps_id_fk",
+ "tableFrom": "developer_videos",
+ "tableTo": "developer_apps",
+ "columnsFrom": ["appId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "developer_videos_id": {
+ "name": "developer_videos_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "directory_users": {
+ "name": "directory_users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "directoryId": {
+ "name": "directoryId",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "directory_users_organization_id_idx": {
+ "name": "directory_users_organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": false
+ },
+ "directory_users_user_org_idx": {
+ "name": "directory_users_user_org_idx",
+ "columns": ["userId", "organizationId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "directory_users_id": {
+ "name": "directory_users_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "folders": {
+ "name": "folders",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'normal'"
+ },
+ "public": {
+ "name": "public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdById": {
+ "name": "createdById",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parentId": {
+ "name": "parentId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "spaceId": {
+ "name": "spaceId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "organization_id_idx": {
+ "name": "organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": false
+ },
+ "created_by_id_idx": {
+ "name": "created_by_id_idx",
+ "columns": ["createdById"],
+ "isUnique": false
+ },
+ "parent_id_idx": {
+ "name": "parent_id_idx",
+ "columns": ["parentId"],
+ "isUnique": false
+ },
+ "space_id_idx": {
+ "name": "space_id_idx",
+ "columns": ["spaceId"],
+ "isUnique": false
+ },
+ "public_parent_id_idx": {
+ "name": "public_parent_id_idx",
+ "columns": ["public", "parentId"],
+ "isUnique": false
+ },
+ "public_space_parent_id_idx": {
+ "name": "public_space_parent_id_idx",
+ "columns": ["public", "spaceId", "parentId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "folders_id": {
+ "name": "folders_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "imported_videos": {
+ "name": "imported_videos",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "orgId": {
+ "name": "orgId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "id_idx": {
+ "name": "id_idx",
+ "columns": ["id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "imported_videos_orgId_source_source_id_pk": {
+ "name": "imported_videos_orgId_source_source_id_pk",
+ "columns": ["orgId", "source", "source_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "integration_installations": {
+ "name": "integration_installations",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "externalId": {
+ "name": "externalId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "displayName": {
+ "name": "displayName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "installedByUserId": {
+ "name": "installedByUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "encryptedCredentials": {
+ "name": "encryptedCredentials",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "provider_external_id_idx": {
+ "name": "provider_external_id_idx",
+ "columns": ["provider", "externalId"],
+ "isUnique": true
+ },
+ "organization_provider_display_name_idx": {
+ "name": "organization_provider_display_name_idx",
+ "columns": ["organizationId", "provider", "displayName"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "integration_installations_id": {
+ "name": "integration_installations_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "loom_migration_imports": {
+ "name": "loom_migration_imports",
+ "columns": {
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "requestId": {
+ "name": "requestId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "loom_migration_imports_request_idx": {
+ "name": "loom_migration_imports_request_idx",
+ "columns": ["requestId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "loom_migration_imports_videoId": {
+ "name": "loom_migration_imports_videoId",
+ "columns": ["videoId"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "loom_migration_requests": {
+ "name": "loom_migration_requests",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "activeOrganizationId": {
+ "name": "activeOrganizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "requestedByUserId": {
+ "name": "requestedByUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspaceName": {
+ "name": "workspaceName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "customerNote": {
+ "name": "customerNote",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "customerReply": {
+ "name": "customerReply",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "invitedAt": {
+ "name": "invitedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "capMessage": {
+ "name": "capMessage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expectedVideoCount": {
+ "name": "expectedVideoCount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "importedVideoCount": {
+ "name": "importedVideoCount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "queuedVideoCount": {
+ "name": "queuedVideoCount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "activeImportCount": {
+ "name": "activeImportCount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "activeImportLeaseToken": {
+ "name": "activeImportLeaseToken",
+ "type": "varchar(36)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "activeImportLeaseUntil": {
+ "name": "activeImportLeaseUntil",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lastOperatorUserId": {
+ "name": "lastOperatorUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lastOperatorAt": {
+ "name": "lastOperatorAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "completedAt": {
+ "name": "completedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "loom_migration_active_org_idx": {
+ "name": "loom_migration_active_org_idx",
+ "columns": ["activeOrganizationId"],
+ "isUnique": true
+ },
+ "loom_migration_org_created_idx": {
+ "name": "loom_migration_org_created_idx",
+ "columns": ["organizationId", "createdAt"],
+ "isUnique": false
+ },
+ "loom_migration_status_created_idx": {
+ "name": "loom_migration_status_created_idx",
+ "columns": ["status", "createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "loom_migration_requests_id": {
+ "name": "loom_migration_requests_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "loops_sync_jobs": {
+ "name": "loops_sync_jobs",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "nextAttemptAt": {
+ "name": "nextAttemptAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "leaseToken": {
+ "name": "leaseToken",
+ "type": "varchar(36)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "leaseUntil": {
+ "name": "leaseUntil",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "failures": {
+ "name": "failures",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "lastError": {
+ "name": "lastError",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "profileHash": {
+ "name": "profileHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "teammateJoinedAt": {
+ "name": "teammateJoinedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "syncedEmail": {
+ "name": "syncedEmail",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lastSyncedAt": {
+ "name": "lastSyncedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "loops_sync_due_idx": {
+ "name": "loops_sync_due_idx",
+ "columns": ["nextAttemptAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "loops_sync_jobs_userId": {
+ "name": "loops_sync_jobs_userId",
+ "columns": ["userId"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "mcp_oauth_clients": {
+ "name": "mcp_oauth_clients",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "clientId": {
+ "name": "clientId",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "clientName": {
+ "name": "clientName",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "redirectUris": {
+ "name": "redirectUris",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "activatedAt": {
+ "name": "activatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "client_id_idx": {
+ "name": "client_id_idx",
+ "columns": ["clientId"],
+ "isUnique": true
+ },
+ "inactive_created_at_idx": {
+ "name": "inactive_created_at_idx",
+ "columns": ["activatedAt", "createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_oauth_clients_id": {
+ "name": "mcp_oauth_clients_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "mcp_oauth_codes": {
+ "name": "mcp_oauth_codes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "clientId": {
+ "name": "clientId",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "codeHash": {
+ "name": "codeHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "codeChallenge": {
+ "name": "codeChallenge",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "redirectUri": {
+ "name": "redirectUri",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "resource": {
+ "name": "resource",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "consumedAt": {
+ "name": "consumedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "code_hash_idx": {
+ "name": "code_hash_idx",
+ "columns": ["codeHash"],
+ "isUnique": true
+ },
+ "expires_at_idx": {
+ "name": "expires_at_idx",
+ "columns": ["expiresAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_oauth_codes_id": {
+ "name": "mcp_oauth_codes_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "mcp_oauth_registration_quotas": {
+ "name": "mcp_oauth_registration_quotas",
+ "columns": {
+ "windowId": {
+ "name": "windowId",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "registrations": {
+ "name": "registrations",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "expires_at_idx": {
+ "name": "expires_at_idx",
+ "columns": ["expiresAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_oauth_registration_quotas_windowId": {
+ "name": "mcp_oauth_registration_quotas_windowId",
+ "columns": ["windowId"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "mcp_oauth_tokens": {
+ "name": "mcp_oauth_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "clientId": {
+ "name": "clientId",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "familyId": {
+ "name": "familyId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "resource": {
+ "name": "resource",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "accessHash": {
+ "name": "accessHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refreshHash": {
+ "name": "refreshHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "accessExpiresAt": {
+ "name": "accessExpiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refreshExpiresAt": {
+ "name": "refreshExpiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revokedAt": {
+ "name": "revokedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "access_hash_idx": {
+ "name": "access_hash_idx",
+ "columns": ["accessHash"],
+ "isUnique": true
+ },
+ "refresh_hash_idx": {
+ "name": "refresh_hash_idx",
+ "columns": ["refreshHash"],
+ "isUnique": true
+ },
+ "family_id_idx": {
+ "name": "family_id_idx",
+ "columns": ["familyId"],
+ "isUnique": false
+ },
+ "client_active_idx": {
+ "name": "client_active_idx",
+ "columns": ["clientId", "revokedAt", "refreshExpiresAt"],
+ "isUnique": false
+ },
+ "refresh_expires_at_idx": {
+ "name": "refresh_expires_at_idx",
+ "columns": ["refreshExpiresAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mcp_oauth_tokens_id": {
+ "name": "mcp_oauth_tokens_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "media_processing_budgets": {
+ "name": "media_processing_budgets",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "reserved_bytes": {
+ "name": "reserved_bytes",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "limit_bytes": {
+ "name": "limit_bytes",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "datetime(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "media_budget_expiry_idx": {
+ "name": "media_budget_expiry_idx",
+ "columns": ["expires_at"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "media_processing_budgets_id": {
+ "name": "media_processing_budgets_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "messenger_conversations": {
+ "name": "messenger_conversations",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "agent": {
+ "name": "agent",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mode": {
+ "name": "mode",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'agent'"
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "anonymousId": {
+ "name": "anonymousId",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "takeoverByUserId": {
+ "name": "takeoverByUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "takeoverAt": {
+ "name": "takeoverAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "lastMessageAt": {
+ "name": "lastMessageAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "user_last_message_idx": {
+ "name": "user_last_message_idx",
+ "columns": ["userId", "lastMessageAt"],
+ "isUnique": false
+ },
+ "anonymous_last_message_idx": {
+ "name": "anonymous_last_message_idx",
+ "columns": ["anonymousId", "lastMessageAt"],
+ "isUnique": false
+ },
+ "mode_last_message_idx": {
+ "name": "mode_last_message_idx",
+ "columns": ["mode", "lastMessageAt"],
+ "isUnique": false
+ },
+ "updated_at_idx": {
+ "name": "updated_at_idx",
+ "columns": ["updatedAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "messenger_conversations_id": {
+ "name": "messenger_conversations_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "messenger_messages": {
+ "name": "messenger_messages",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "conversationId": {
+ "name": "conversationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "anonymousId": {
+ "name": "anonymousId",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "conversation_created_at_idx": {
+ "name": "conversation_created_at_idx",
+ "columns": ["conversationId", "createdAt"],
+ "isUnique": false
+ },
+ "role_created_at_idx": {
+ "name": "role_created_at_idx",
+ "columns": ["role", "createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "messenger_messages_conversationId_messenger_conversations_id_fk": {
+ "name": "messenger_messages_conversationId_messenger_conversations_id_fk",
+ "tableFrom": "messenger_messages",
+ "tableTo": "messenger_conversations",
+ "columnsFrom": ["conversationId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "messenger_messages_id": {
+ "name": "messenger_messages_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "messenger_support_emails": {
+ "name": "messenger_support_emails",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "conversationId": {
+ "name": "conversationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userEmail": {
+ "name": "userEmail",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "support_email_user_created_at_idx": {
+ "name": "support_email_user_created_at_idx",
+ "columns": ["userId", "createdAt"],
+ "isUnique": false
+ },
+ "support_email_conversation_created_at_idx": {
+ "name": "support_email_conversation_created_at_idx",
+ "columns": ["conversationId", "createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "support_email_conversation_fk": {
+ "name": "support_email_conversation_fk",
+ "tableFrom": "messenger_support_emails",
+ "tableTo": "messenger_conversations",
+ "columnsFrom": ["conversationId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "messenger_support_emails_id": {
+ "name": "messenger_support_emails_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "mobile_push_claims": {
+ "name": "mobile_push_claims",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": ["createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mobile_push_claims_id": {
+ "name": "mobile_push_claims_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "mobile_push_tokens": {
+ "name": "mobile_push_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sessionHash": {
+ "name": "sessionHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "environment": {
+ "name": "environment",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "token_idx": {
+ "name": "token_idx",
+ "columns": ["token"],
+ "isUnique": true
+ },
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": ["userId"],
+ "isUnique": false
+ },
+ "session_hash_idx": {
+ "name": "session_hash_idx",
+ "columns": ["sessionHash"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "mobile_push_tokens_id": {
+ "name": "mobile_push_tokens_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "notifications": {
+ "name": "notifications",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "orgId": {
+ "name": "orgId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "recipientId": {
+ "name": "recipientId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "data": {
+ "name": "data",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "dedupKey": {
+ "name": "dedupKey",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "readAt": {
+ "name": "readAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "org_id_idx": {
+ "name": "org_id_idx",
+ "columns": ["orgId"],
+ "isUnique": false
+ },
+ "type_idx": {
+ "name": "type_idx",
+ "columns": ["type"],
+ "isUnique": false
+ },
+ "read_at_idx": {
+ "name": "read_at_idx",
+ "columns": ["readAt"],
+ "isUnique": false
+ },
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": ["createdAt"],
+ "isUnique": false
+ },
+ "recipient_read_idx": {
+ "name": "recipient_read_idx",
+ "columns": ["recipientId", "readAt"],
+ "isUnique": false
+ },
+ "recipient_created_idx": {
+ "name": "recipient_created_idx",
+ "columns": ["recipientId", "createdAt"],
+ "isUnique": false
+ },
+ "dedup_key_idx": {
+ "name": "dedup_key_idx",
+ "columns": ["dedupKey"],
+ "isUnique": true
+ },
+ "type_recipient_created_idx": {
+ "name": "type_recipient_created_idx",
+ "columns": ["type", "recipientId", "createdAt"],
+ "isUnique": false
+ },
+ "type_recipient_video_created_idx": {
+ "name": "type_recipient_video_created_idx",
+ "columns": ["type", "recipientId", "videoId", "createdAt"],
+ "isUnique": false
+ },
+ "video_id_idx": {
+ "name": "video_id_idx",
+ "columns": ["videoId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "notifications_id": {
+ "name": "notifications_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "organization_directory_sync": {
+ "name": "organization_directory_sync",
+ "columns": {
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "requestedAt": {
+ "name": "requestedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "syncedAt": {
+ "name": "syncedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "leaseExpiresAt": {
+ "name": "leaseExpiresAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lastError": {
+ "name": "lastError",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "heldRemovals": {
+ "name": "heldRemovals",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "removalsApprovedAt": {
+ "name": "removalsApprovedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "organization_directory_sync_organizationId": {
+ "name": "organization_directory_sync_organizationId",
+ "columns": ["organizationId"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "organization_invites": {
+ "name": "organization_invites",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "invitedEmail": {
+ "name": "invitedEmail",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "invitedByUserId": {
+ "name": "invitedByUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "organization_id_idx": {
+ "name": "organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": false
+ },
+ "invited_email_idx": {
+ "name": "invited_email_idx",
+ "columns": ["invitedEmail"],
+ "isUnique": false
+ },
+ "invited_by_user_id_idx": {
+ "name": "invited_by_user_id_idx",
+ "columns": ["invitedByUserId"],
+ "isUnique": false
+ },
+ "status_idx": {
+ "name": "status_idx",
+ "columns": ["status"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "organization_invites_id": {
+ "name": "organization_invites_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "organization_members": {
+ "name": "organization_members",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "hasProSeat": {
+ "name": "hasProSeat",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "organization_id_idx": {
+ "name": "organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": false
+ },
+ "user_id_organization_id_idx": {
+ "name": "user_id_organization_id_idx",
+ "columns": ["userId", "organizationId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "organization_members_id": {
+ "name": "organization_members_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "organization_sso": {
+ "name": "organization_sso",
+ "columns": {
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "purchasedByUserId": {
+ "name": "purchasedByUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "stripeCustomerId": {
+ "name": "stripeCustomerId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripeSubscriptionId": {
+ "name": "stripeSubscriptionId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripePriceId": {
+ "name": "stripePriceId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unpaid'"
+ },
+ "paidThrough": {
+ "name": "paidThrough",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "currentPeriodEnd": {
+ "name": "currentPeriodEnd",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cancelAtPeriodEnd": {
+ "name": "cancelAtPeriodEnd",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "checkoutAttemptId": {
+ "name": "checkoutAttemptId",
+ "type": "varchar(36)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checkoutCurrency": {
+ "name": "checkoutCurrency",
+ "type": "varchar(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checkoutPriceId": {
+ "name": "checkoutPriceId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checkoutSessionId": {
+ "name": "checkoutSessionId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checkoutStartedAt": {
+ "name": "checkoutStartedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "sso_stripe_subscription_id_idx": {
+ "name": "sso_stripe_subscription_id_idx",
+ "columns": ["stripeSubscriptionId"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "organization_sso_organizationId": {
+ "name": "organization_sso_organizationId",
+ "columns": ["organizationId"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "organizations": {
+ "name": "organizations",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ownerId": {
+ "name": "ownerId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tombstoneAt": {
+ "name": "tombstoneAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "allowedEmailDomain": {
+ "name": "allowedEmailDomain",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "defaultVideoVisibility": {
+ "name": "defaultVideoVisibility",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "customDomain": {
+ "name": "customDomain",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "domainVerified": {
+ "name": "domainVerified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "iconUrl": {
+ "name": "iconUrl",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "shareableLinkIconUrl": {
+ "name": "shareableLinkIconUrl",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "workosOrganizationId": {
+ "name": "workosOrganizationId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "workosConnectionId": {
+ "name": "workosConnectionId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "owner_id_tombstone_idx": {
+ "name": "owner_id_tombstone_idx",
+ "columns": ["ownerId", "tombstoneAt"],
+ "isUnique": false
+ },
+ "custom_domain_idx": {
+ "name": "custom_domain_idx",
+ "columns": ["customDomain"],
+ "isUnique": false
+ },
+ "workos_organization_id_idx": {
+ "name": "workos_organization_id_idx",
+ "columns": ["workosOrganizationId"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "organizations_id": {
+ "name": "organizations_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "s3_buckets": {
+ "name": "s3_buckets",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ownerId": {
+ "name": "ownerId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "region": {
+ "name": "region",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "bucketName": {
+ "name": "bucketName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "accessKeyId": {
+ "name": "accessKeyId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "secretAccessKey": {
+ "name": "secretAccessKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('aws')"
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "owner_organization_idx": {
+ "name": "owner_organization_idx",
+ "columns": ["ownerId", "organizationId"],
+ "isUnique": false
+ },
+ "organization_id_idx": {
+ "name": "organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": false
+ },
+ "organization_active_idx": {
+ "name": "organization_active_idx",
+ "columns": ["organizationId", "active", "updatedAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "s3_buckets_id": {
+ "name": "s3_buckets_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "sessions": {
+ "name": "sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sessionToken": {
+ "name": "sessionToken",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "session_token_idx": {
+ "name": "session_token_idx",
+ "columns": ["sessionToken"],
+ "isUnique": true
+ },
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": ["userId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "sessions_id": {
+ "name": "sessions_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "shared_videos": {
+ "name": "shared_videos",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "folderId": {
+ "name": "folderId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sharedByUserId": {
+ "name": "sharedByUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sharedAt": {
+ "name": "sharedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "folder_id_idx": {
+ "name": "folder_id_idx",
+ "columns": ["folderId"],
+ "isUnique": false
+ },
+ "organization_id_idx": {
+ "name": "organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": false
+ },
+ "shared_by_user_id_idx": {
+ "name": "shared_by_user_id_idx",
+ "columns": ["sharedByUserId"],
+ "isUnique": false
+ },
+ "video_id_organization_id_idx": {
+ "name": "video_id_organization_id_idx",
+ "columns": ["videoId", "organizationId"],
+ "isUnique": false
+ },
+ "video_id_folder_id_idx": {
+ "name": "video_id_folder_id_idx",
+ "columns": ["videoId", "folderId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "shared_videos_id": {
+ "name": "shared_videos_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "signed_baas": {
+ "name": "signed_baas",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "stripeSubscriptionId": {
+ "name": "stripeSubscriptionId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "entityName": {
+ "name": "entityName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entityType": {
+ "name": "entityType",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entityAddress": {
+ "name": "entityAddress",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "signerName": {
+ "name": "signerName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "signerTitle": {
+ "name": "signerTitle",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "noticesEmail": {
+ "name": "noticesEmail",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "signatureData": {
+ "name": "signatureData",
+ "type": "longtext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "signedAt": {
+ "name": "signedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "emailSentAt": {
+ "name": "emailSentAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "signed_baa_organization_id_idx": {
+ "name": "signed_baa_organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": true
+ },
+ "signed_baa_stripe_subscription_idx": {
+ "name": "signed_baa_stripe_subscription_idx",
+ "columns": ["stripeSubscriptionId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "signed_baas_id": {
+ "name": "signed_baas_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "space_members": {
+ "name": "space_members",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "spaceId": {
+ "name": "spaceId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'member'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": ["userId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "space_members_id": {
+ "name": "space_members_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {
+ "space_id_user_id_unique": {
+ "name": "space_id_user_id_unique",
+ "columns": ["spaceId", "userId"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "space_videos": {
+ "name": "space_videos",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "spaceId": {
+ "name": "spaceId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "folderId": {
+ "name": "folderId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "addedById": {
+ "name": "addedById",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "addedAt": {
+ "name": "addedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "folder_id_idx": {
+ "name": "folder_id_idx",
+ "columns": ["folderId"],
+ "isUnique": false
+ },
+ "video_id_idx": {
+ "name": "video_id_idx",
+ "columns": ["videoId"],
+ "isUnique": false
+ },
+ "added_by_id_idx": {
+ "name": "added_by_id_idx",
+ "columns": ["addedById"],
+ "isUnique": false
+ },
+ "space_id_video_id_idx": {
+ "name": "space_id_video_id_idx",
+ "columns": ["spaceId", "videoId"],
+ "isUnique": false
+ },
+ "space_id_folder_id_idx": {
+ "name": "space_id_folder_id_idx",
+ "columns": ["spaceId", "folderId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "space_videos_id": {
+ "name": "space_videos_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "spaces": {
+ "name": "spaces",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "primary": {
+ "name": "primary",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdById": {
+ "name": "createdById",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "iconUrl": {
+ "name": "iconUrl",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "privacy": {
+ "name": "privacy",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Private'"
+ },
+ "public": {
+ "name": "public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {
+ "organization_id_idx": {
+ "name": "organization_id_idx",
+ "columns": ["organizationId"],
+ "isUnique": false
+ },
+ "created_by_id_idx": {
+ "name": "created_by_id_idx",
+ "columns": ["createdById"],
+ "isUnique": false
+ },
+ "public_organization_id_idx": {
+ "name": "public_organization_id_idx",
+ "columns": ["public", "organizationId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "spaces_id": {
+ "name": "spaces_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "storage_integrations": {
+ "name": "storage_integrations",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ownerId": {
+ "name": "ownerId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "displayName": {
+ "name": "displayName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "encryptedConfig": {
+ "name": "encryptedConfig",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "googleDriveAccessToken": {
+ "name": "googleDriveAccessToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "googleDriveAccessTokenExpiresAt": {
+ "name": "googleDriveAccessTokenExpiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "googleDriveTokenRefreshLeaseId": {
+ "name": "googleDriveTokenRefreshLeaseId",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "googleDriveTokenRefreshLeaseExpiresAt": {
+ "name": "googleDriveTokenRefreshLeaseExpiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "googleDriveStorageQuotaCache": {
+ "name": "googleDriveStorageQuotaCache",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "owner_provider_idx": {
+ "name": "owner_provider_idx",
+ "columns": ["ownerId", "provider"],
+ "isUnique": false
+ },
+ "owner_active_idx": {
+ "name": "owner_active_idx",
+ "columns": ["ownerId", "active"],
+ "isUnique": false
+ },
+ "organization_provider_idx": {
+ "name": "organization_provider_idx",
+ "columns": ["organizationId", "provider"],
+ "isUnique": false
+ },
+ "organization_active_idx": {
+ "name": "organization_active_idx",
+ "columns": ["organizationId", "active", "status"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "storage_integrations_id": {
+ "name": "storage_integrations_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "storage_objects": {
+ "name": "storage_objects",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "integrationId": {
+ "name": "integrationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ownerId": {
+ "name": "ownerId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "objectKey": {
+ "name": "objectKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "objectKeyHash": {
+ "name": "objectKeyHash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerObjectId": {
+ "name": "providerObjectId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "uploadSessionUrl": {
+ "name": "uploadSessionUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "uploadStatus": {
+ "name": "uploadStatus",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "contentType": {
+ "name": "contentType",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "contentLength": {
+ "name": "contentLength",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "integration_key_hash_idx": {
+ "name": "integration_key_hash_idx",
+ "columns": ["integrationId", "objectKeyHash"],
+ "isUnique": true
+ },
+ "integration_status_idx": {
+ "name": "integration_status_idx",
+ "columns": ["integrationId", "uploadStatus"],
+ "isUnique": false
+ },
+ "integration_object_key_prefix_idx": {
+ "name": "integration_object_key_prefix_idx",
+ "columns": ["integrationId", "`objectKey`(191)"],
+ "isUnique": false
+ },
+ "video_id_idx": {
+ "name": "video_id_idx",
+ "columns": ["videoId"],
+ "isUnique": false
+ },
+ "owner_id_idx": {
+ "name": "owner_id_idx",
+ "columns": ["ownerId"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "storage_objects_integrationId_storage_integrations_id_fk": {
+ "name": "storage_objects_integrationId_storage_integrations_id_fk",
+ "tableFrom": "storage_objects",
+ "tableTo": "storage_integrations",
+ "columnsFrom": ["integrationId"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "storage_objects_id": {
+ "name": "storage_objects_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lastName": {
+ "name": "lastName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripeCustomerId": {
+ "name": "stripeCustomerId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripeSubscriptionId": {
+ "name": "stripeSubscriptionId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "thirdPartyStripeSubscriptionId": {
+ "name": "thirdPartyStripeSubscriptionId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripeSubscriptionStatus": {
+ "name": "stripeSubscriptionStatus",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "stripeSubscriptionPriceId": {
+ "name": "stripeSubscriptionPriceId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "preferences": {
+ "name": "preferences",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "('null')"
+ },
+ "activeOrganizationId": {
+ "name": "activeOrganizationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "onboardingSteps": {
+ "name": "onboardingSteps",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "onboarding_completed_at": {
+ "name": "onboarding_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "customBucket": {
+ "name": "customBucket",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "inviteQuota": {
+ "name": "inviteQuota",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "defaultOrgId": {
+ "name": "defaultOrgId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "authSessionVersion": {
+ "name": "authSessionVersion",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "marketingOrigin": {
+ "name": "marketingOrigin",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unknown'"
+ }
+ },
+ "indexes": {
+ "email_idx": {
+ "name": "email_idx",
+ "columns": ["email"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "users_id": {
+ "name": "users_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "verification_tokens": {
+ "name": "verification_tokens",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verification_tokens_identifier": {
+ "name": "verification_tokens_identifier",
+ "columns": ["identifier"]
+ }
+ },
+ "uniqueConstraints": {
+ "verification_tokens_token_unique": {
+ "name": "verification_tokens_token_unique",
+ "columns": ["token"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "video_edits": {
+ "name": "video_edits",
+ "columns": {
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sourceKey": {
+ "name": "sourceKey",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "editSpec": {
+ "name": "editSpec",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "video_edits_videoId_videos_id_fk": {
+ "name": "video_edits_videoId_videos_id_fk",
+ "tableFrom": "video_edits",
+ "tableTo": "videos",
+ "columnsFrom": ["videoId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "video_edits_videoId": {
+ "name": "video_edits_videoId",
+ "columns": ["videoId"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "video_processing_jobs": {
+ "name": "video_processing_jobs",
+ "columns": {
+ "video_id": {
+ "name": "video_id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "generation": {
+ "name": "generation",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "manifest_sha256": {
+ "name": "manifest_sha256",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'committing'"
+ },
+ "attempt_id": {
+ "name": "attempt_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "datetime(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "next_retry_at": {
+ "name": "next_retry_at",
+ "type": "datetime(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workflow_run_id": {
+ "name": "workflow_run_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "remote_job_id": {
+ "name": "remote_job_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "verification": {
+ "name": "verification",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "output": {
+ "name": "output",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "datetime(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "datetime(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "processing_state_retry_video_idx": {
+ "name": "processing_state_retry_video_idx",
+ "columns": ["state", "next_retry_at", "video_id"],
+ "isUnique": false
+ },
+ "processing_state_lease_video_idx": {
+ "name": "processing_state_lease_video_idx",
+ "columns": ["state", "lease_expires_at", "video_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "video_processing_jobs_video_id": {
+ "name": "video_processing_jobs_video_id",
+ "columns": ["video_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "video_uploads": {
+ "name": "video_uploads",
+ "columns": {
+ "video_id": {
+ "name": "video_id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "uploaded": {
+ "name": "uploaded",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "total": {
+ "name": "total",
+ "type": "bigint unsigned",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "mode": {
+ "name": "mode",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "phase": {
+ "name": "phase",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'uploading'"
+ },
+ "processing_progress": {
+ "name": "processing_progress",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "processing_message": {
+ "name": "processing_message",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "processing_error": {
+ "name": "processing_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "raw_file_key": {
+ "name": "raw_file_key",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "phase_updated_at_video_id_idx": {
+ "name": "phase_updated_at_video_id_idx",
+ "columns": ["phase", "updated_at", "video_id"],
+ "isUnique": false
+ },
+ "started_at_idx": {
+ "name": "started_at_idx",
+ "columns": ["started_at"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "video_uploads_video_id": {
+ "name": "video_uploads_video_id",
+ "columns": ["video_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "video_viewer_grants": {
+ "name": "video_viewer_grants",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "videoId": {
+ "name": "videoId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "invitedByUserId": {
+ "name": "invitedByUserId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "revokedAt": {
+ "name": "revokedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "video_email_idx": {
+ "name": "video_email_idx",
+ "columns": ["videoId", "email"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "video_viewer_grants_videoId_videos_id_fk": {
+ "name": "video_viewer_grants_videoId_videos_id_fk",
+ "tableFrom": "video_viewer_grants",
+ "tableTo": "videos",
+ "columnsFrom": ["videoId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "video_viewer_grants_id": {
+ "name": "video_viewer_grants_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "videos": {
+ "name": "videos",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ownerId": {
+ "name": "ownerId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "orgId": {
+ "name": "orgId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'My Video'"
+ },
+ "bucket": {
+ "name": "bucket",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "storageIntegrationId": {
+ "name": "storageIntegrationId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "duration": {
+ "name": "duration",
+ "type": "float",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "width": {
+ "name": "width",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "height": {
+ "name": "height",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "fps": {
+ "name": "fps",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "public": {
+ "name": "public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "transcriptionStatus": {
+ "name": "transcriptionStatus",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('{\"type\":\"MediaConvert\"}')"
+ },
+ "folderId": {
+ "name": "folderId",
+ "type": "varchar(15)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "effectiveCreatedAt": {
+ "name": "effectiveCreatedAt",
+ "type": "datetime",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "generated": {
+ "as": "COALESCE(\n STR_TO_DATE(JSON_UNQUOTE(JSON_EXTRACT(`metadata`, '$.customCreatedAt')), '%Y-%m-%dT%H:%i:%s.%fZ'),\n STR_TO_DATE(JSON_UNQUOTE(JSON_EXTRACT(`metadata`, '$.customCreatedAt')), '%Y-%m-%dT%H:%i:%sZ'),\n `createdAt`\n )",
+ "type": "stored"
+ }
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "xStreamInfo": {
+ "name": "xStreamInfo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "firstViewEmailSentAt": {
+ "name": "firstViewEmailSentAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "isScreenshot": {
+ "name": "isScreenshot",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "awsRegion": {
+ "name": "awsRegion",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "awsBucket": {
+ "name": "awsBucket",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "videoStartTime": {
+ "name": "videoStartTime",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "audioStartTime": {
+ "name": "audioStartTime",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "jobId": {
+ "name": "jobId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "jobStatus": {
+ "name": "jobStatus",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "skipProcessing": {
+ "name": "skipProcessing",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {
+ "owner_id_idx": {
+ "name": "owner_id_idx",
+ "columns": ["ownerId"],
+ "isUnique": false
+ },
+ "owner_updated_id_idx": {
+ "name": "owner_updated_id_idx",
+ "columns": ["ownerId", "updatedAt", "id"],
+ "isUnique": false
+ },
+ "is_public_idx": {
+ "name": "is_public_idx",
+ "columns": ["public"],
+ "isUnique": false
+ },
+ "folder_id_idx": {
+ "name": "folder_id_idx",
+ "columns": ["folderId"],
+ "isUnique": false
+ },
+ "storage_integration_id_idx": {
+ "name": "storage_integration_id_idx",
+ "columns": ["storageIntegrationId"],
+ "isUnique": false
+ },
+ "org_owner_folder_idx": {
+ "name": "org_owner_folder_idx",
+ "columns": ["orgId", "ownerId", "folderId"],
+ "isUnique": false
+ },
+ "org_owner_folder_effective_idx": {
+ "name": "org_owner_folder_effective_idx",
+ "columns": ["orgId", "ownerId", "folderId", "effectiveCreatedAt"],
+ "isUnique": false
+ },
+ "org_effective_created_idx": {
+ "name": "org_effective_created_idx",
+ "columns": ["orgId", "effectiveCreatedAt"],
+ "isUnique": false
+ },
+ "screenshot_transcription_created_idx": {
+ "name": "screenshot_transcription_created_idx",
+ "columns": ["isScreenshot", "transcriptionStatus", "createdAt"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "videos_storageIntegrationId_storage_integrations_id_fk": {
+ "name": "videos_storageIntegrationId_storage_integrations_id_fk",
+ "tableFrom": "videos",
+ "tableTo": "storage_integrations",
+ "columnsFrom": ["storageIntegrationId"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "videos_id": {
+ "name": "videos_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {
+ "integration_object_key_prefix_idx": {
+ "columns": {
+ "`objectKey`(191)": {
+ "isExpression": true
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json
index 9f14cd20c63..d5f56c42e87 100644
--- a/packages/database/migrations/meta/_journal.json
+++ b/packages/database/migrations/meta/_journal.json
@@ -393,6 +393,13 @@
"when": 1794461929204,
"tag": "0055_developer_transcripts",
"breakpoints": true
+ },
+ {
+ "idx": 56,
+ "version": "5",
+ "when": 1794532188306,
+ "tag": "0056_developer_billing_read_indexes",
+ "breakpoints": true
}
]
}
diff --git a/packages/database/schema.ts b/packages/database/schema.ts
index b5570f7cae7..68a82dacc0c 100644
--- a/packages/database/schema.ts
+++ b/packages/database/schema.ts
@@ -2002,6 +2002,20 @@ export const developerTranscripts = mysqlTable(
table.status,
table.updatedAt,
),
+ index("dev_transcripts_app_completed_idx").on(
+ table.appId,
+ table.status,
+ table.completedAt,
+ table.durationSeconds,
+ table.billedMicroCredits,
+ ),
+ index("dev_transcripts_app_status_held_idx").on(
+ table.appId,
+ table.status,
+ table.createdAt,
+ table.deletedAt,
+ table.reservedMicroCredits,
+ ),
],
);
@@ -2035,6 +2049,7 @@ export const developerCreditAccounts = mysqlTable(
(table) => [
uniqueIndex("app_id_unique").on(table.appId),
index("owner_idx").on(table.ownerId),
+ index("auto_top_up_idx").on(table.autoTopUpEnabled),
],
);
@@ -2129,6 +2144,7 @@ export const developerCreditTransactions = mysqlTable(
table.referenceId,
table.referenceType,
),
+ index("reference_type_id_idx").on(table.referenceType, table.referenceId),
],
);
diff --git a/packages/web-backend/src/AgentManagement.ts b/packages/web-backend/src/AgentManagement.ts
index e2f0d8273b3..8298434efad 100644
--- a/packages/web-backend/src/AgentManagement.ts
+++ b/packages/web-backend/src/AgentManagement.ts
@@ -7,7 +7,7 @@ import {
type User,
type Video,
} from "@cap/web-domain";
-import { and, desc, eq, isNull, lt, or, sql } from "drizzle-orm";
+import { and, desc, eq, gt, isNull, lt, or, sql } from "drizzle-orm";
import { Effect } from "effect";
import { Database } from "./Database.ts";
@@ -649,7 +649,15 @@ export class AgentManagement extends Effect.Service()(
Db.developerDailyStorageSnapshots.totalDurationMinutes,
})
.from(Db.developerDailyStorageSnapshots)
- .where(eq(Db.developerDailyStorageSnapshots.appId, appId))
+ .where(
+ and(
+ eq(Db.developerDailyStorageSnapshots.appId, appId),
+ gt(
+ Db.developerDailyStorageSnapshots.totalDurationMinutes,
+ 0,
+ ),
+ ),
+ )
.orderBy(desc(Db.developerDailyStorageSnapshots.snapshotDate))
.limit(1),
]),