From 1f15dc31a9a12f229a335405d8a88ae6d0a813a2 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 21 Sep 2026 11:46:11 +0100
Subject: [PATCH 1/7] feat: add Cap Pro Loom concierge migration
---
apps/web/__tests__/unit/loom-csv.test.ts | 25 +
.../unit/loom-migration-state.test.ts | 39 +
apps/web/actions/loom-concierge.tsx | 331 ++
apps/web/actions/loom.ts | 429 +-
.../dashboard/_components/Navbar/Items.tsx | 25 +
.../loom-migrations/LoomMigrationQueue.tsx | 527 ++
.../dashboard/admin/loom-migrations/page.tsx | 17 +
.../app/(org)/dashboard/import/ImportPage.tsx | 38 +-
.../dashboard/import/loom/ImportLoomPage.tsx | 61 +-
.../migrations/loom/LoomMigrationPage.tsx | 398 ++
.../(org)/dashboard/migrations/loom/page.tsx | 10 +
apps/web/content/docs/migrating-to-cap.mdx | 16 +-
apps/web/lib/loom-concierge.ts | 134 +
apps/web/lib/loom-csv.ts | 72 +
apps/web/lib/loom-migration-state.ts | 80 +
packages/database/emails/loom-migration.tsx | 70 +
.../migrations/0046_loom_concierge.sql | 25 +
.../migrations/meta/0046_snapshot.json | 4735 +++++++++++++++++
.../database/migrations/meta/_journal.json | 7 +
packages/database/schema.ts | 51 +
20 files changed, 7031 insertions(+), 59 deletions(-)
create mode 100644 apps/web/__tests__/unit/loom-csv.test.ts
create mode 100644 apps/web/__tests__/unit/loom-migration-state.test.ts
create mode 100644 apps/web/actions/loom-concierge.tsx
create mode 100644 apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
create mode 100644 apps/web/app/(org)/dashboard/admin/loom-migrations/page.tsx
create mode 100644 apps/web/app/(org)/dashboard/migrations/loom/LoomMigrationPage.tsx
create mode 100644 apps/web/app/(org)/dashboard/migrations/loom/page.tsx
create mode 100644 apps/web/lib/loom-concierge.ts
create mode 100644 apps/web/lib/loom-csv.ts
create mode 100644 apps/web/lib/loom-migration-state.ts
create mode 100644 packages/database/emails/loom-migration.tsx
create mode 100644 packages/database/migrations/0046_loom_concierge.sql
create mode 100644 packages/database/migrations/meta/0046_snapshot.json
diff --git a/apps/web/__tests__/unit/loom-csv.test.ts b/apps/web/__tests__/unit/loom-csv.test.ts
new file mode 100644
index 00000000000..5338285db28
--- /dev/null
+++ b/apps/web/__tests__/unit/loom-csv.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from "vitest";
+import { parseConciergeLoomCsv } from "@/lib/loom-csv";
+
+describe("concierge Loom CSV", () => {
+ it("keeps quoted workspace names and source row numbers", () => {
+ const rows = parseConciergeLoomCsv(
+ "\uFEFFuser_email,space_name,loom_video_url\r\n" +
+ 'owner@example.com,"Sales, Europe",https://www.loom.com/share/0123456789abcdef\r\n',
+ );
+ expect(rows).toEqual([
+ {
+ rowNumber: 2,
+ loomUrl: "https://www.loom.com/share/0123456789abcdef",
+ userEmail: "owner@example.com",
+ spaceName: "Sales, Europe",
+ },
+ ]);
+ });
+
+ it("rejects a file without the mapping columns", () => {
+ expect(() => parseConciergeLoomCsv("url,email\nlink,person@x.com")).toThrow(
+ "loom_video_url and user_email",
+ );
+ });
+});
diff --git a/apps/web/__tests__/unit/loom-migration-state.test.ts b/apps/web/__tests__/unit/loom-migration-state.test.ts
new file mode 100644
index 00000000000..87b1f90a544
--- /dev/null
+++ b/apps/web/__tests__/unit/loom-migration-state.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+import { validateOperatorMigrationUpdate } from "@/lib/loom-migration-state";
+
+const base = {
+ currentStatus: "in_progress" as const,
+ nextStatus: "completed" as const,
+ message: "Migration finished",
+ verified: true,
+ expectedVideoCount: 12,
+ importedVideoCount: 12,
+};
+
+describe("Loom migration completion", () => {
+ it("requires a verified library and reconciled counts", () => {
+ expect(() =>
+ validateOperatorMigrationUpdate({ ...base, verified: false }),
+ ).toThrow("Verify the migrated library");
+ expect(() =>
+ validateOperatorMigrationUpdate({ ...base, importedVideoCount: 11 }),
+ ).toThrow("Reconcile the expected video count");
+ expect(() =>
+ validateOperatorMigrationUpdate({ ...base, expectedVideoCount: null }),
+ ).toThrow("Enter the agreed source video count");
+ });
+
+ it("requires a useful message when Cap asks for information", () => {
+ expect(() =>
+ validateOperatorMigrationUpdate({
+ ...base,
+ nextStatus: "needs_information",
+ message: " ",
+ }),
+ ).toThrow("Tell the customer");
+ });
+
+ it("accepts completion after verification", () => {
+ expect(validateOperatorMigrationUpdate(base)).toBe("Migration finished");
+ });
+});
diff --git a/apps/web/actions/loom-concierge.tsx b/apps/web/actions/loom-concierge.tsx
new file mode 100644
index 00000000000..b1a91d30f14
--- /dev/null
+++ b/apps/web/actions/loom-concierge.tsx
@@ -0,0 +1,331 @@
+"use server";
+
+import { db } from "@cap/database";
+import { sendEmail } from "@cap/database/emails/config";
+import {
+ LoomMigrationRequestEmail,
+ LoomMigrationStatusEmail,
+} from "@cap/database/emails/loom-migration";
+import { nanoId } from "@cap/database/helpers";
+import {
+ importedVideos,
+ loomMigrationRequests,
+ organizations,
+ users,
+ videoUploads,
+} from "@cap/database/schema";
+import { serverEnv } from "@cap/env";
+import type { Organisation } from "@cap/web-domain";
+import { and, eq, inArray, isNotNull } from "drizzle-orm";
+import { revalidatePath } from "next/cache";
+import {
+ getCustomerMigrationRequests,
+ getOperatorMigrationQueue,
+ requireCustomerMigrationAccess,
+ requireMigrationOperator,
+ requireProCustomerMigrationAccess,
+} from "@/lib/loom-concierge";
+import {
+ LOOM_MIGRATION_STATUS_LABELS,
+ type LoomMigrationStatus,
+ normalizeMigrationText,
+ validateOperatorMigrationUpdate,
+} from "@/lib/loom-migration-state";
+import { isOrganizationOwnerPro } from "@/lib/org-pro";
+
+const SUPPORT_EMAIL = "hello@cap.so";
+
+function affectedRows(result: unknown) {
+ if (Array.isArray(result)) {
+ return (
+ (result[0] as { affectedRows?: number } | undefined)?.affectedRows ?? 0
+ );
+ }
+ return (result as { affectedRows?: number } | undefined)?.affectedRows ?? 0;
+}
+
+function refreshMigrationPages() {
+ revalidatePath("/dashboard/migrations/loom");
+ revalidatePath("/dashboard/admin/loom-migrations");
+}
+
+async function findMigration(requestId: string) {
+ if (typeof requestId !== "string" || !/^[A-Za-z0-9_-]{15}$/.test(requestId)) {
+ throw new Error("Invalid migration request.");
+ }
+ const [request] = await db()
+ .select()
+ .from(loomMigrationRequests)
+ .where(eq(loomMigrationRequests.id, requestId))
+ .limit(1);
+ if (!request) throw new Error("Migration request not found.");
+ return request;
+}
+
+export async function getLoomMigrationDashboard(
+ organizationId: Organisation.OrganisationId,
+) {
+ await requireCustomerMigrationAccess(organizationId);
+ const [isPro, requests] = await Promise.all([
+ isOrganizationOwnerPro(organizationId),
+ getCustomerMigrationRequests(organizationId),
+ ]);
+ return { isPro, requests };
+}
+
+export async function requestLoomMigration({
+ organizationId,
+ workspaceName,
+ note,
+}: {
+ organizationId: Organisation.OrganisationId;
+ workspaceName: string;
+ note: string;
+}) {
+ const user = await requireProCustomerMigrationAccess(organizationId);
+ const normalizedWorkspaceName = normalizeMigrationText(
+ workspaceName,
+ "Workspace name",
+ 255,
+ );
+ const normalizedNote = normalizeMigrationText(note, "Note", 2000);
+ const [existing] = await db()
+ .select({ id: loomMigrationRequests.id })
+ .from(loomMigrationRequests)
+ .where(eq(loomMigrationRequests.activeOrganizationId, organizationId))
+ .limit(1);
+ if (existing) return { id: existing.id, alreadyRequested: true };
+
+ const id = nanoId();
+ try {
+ await db()
+ .insert(loomMigrationRequests)
+ .values({
+ id,
+ organizationId,
+ activeOrganizationId: organizationId,
+ requestedByUserId: user.id,
+ workspaceName: normalizedWorkspaceName || null,
+ customerNote: normalizedNote || null,
+ });
+ } catch (error) {
+ const [racedRequest] = await db()
+ .select({ id: loomMigrationRequests.id })
+ .from(loomMigrationRequests)
+ .where(eq(loomMigrationRequests.activeOrganizationId, organizationId))
+ .limit(1);
+ if (racedRequest) {
+ return { id: racedRequest.id, alreadyRequested: true };
+ }
+ throw error;
+ }
+
+ const [organization] = await db()
+ .select({ name: organizations.name })
+ .from(organizations)
+ .where(eq(organizations.id, organizationId))
+ .limit(1);
+ try {
+ const delivery = await sendEmail({
+ email: SUPPORT_EMAIL,
+ subject: `New Loom migration request: ${organization?.name ?? "Cap workspace"}`,
+ react: (
+
+ ),
+ idempotencyKey: `loom-migration-request-${id}`,
+ });
+ if (delivery?.error) {
+ console.error(
+ "Failed to send Loom migration request notice",
+ delivery.error,
+ );
+ }
+ } catch (error) {
+ console.error("Failed to send Loom migration request notice", error);
+ }
+
+ refreshMigrationPages();
+ return { id, alreadyRequested: false };
+}
+
+export async function confirmLoomMigrationInvite(requestId: string) {
+ const request = await findMigration(requestId);
+ await requireCustomerMigrationAccess(request.organizationId);
+ if (request.status === "completed") {
+ throw new Error("This migration is already complete.");
+ }
+ if (request.invitedAt) return;
+ const result = await db()
+ .update(loomMigrationRequests)
+ .set({ invitedAt: new Date(), updatedAt: new Date() })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ if (affectedRows(result) === 0) {
+ throw new Error("The migration request changed. Refresh and try again.");
+ }
+ refreshMigrationPages();
+}
+
+export async function answerLoomMigrationQuestion({
+ requestId,
+ answer,
+}: {
+ requestId: string;
+ answer: string;
+}) {
+ const request = await findMigration(requestId);
+ await requireCustomerMigrationAccess(request.organizationId);
+ const normalizedAnswer = normalizeMigrationText(answer, "Answer", 2000);
+ if (!normalizedAnswer)
+ throw new Error("Enter the information Cap asked for.");
+ const result = await db()
+ .update(loomMigrationRequests)
+ .set({
+ customerReply: normalizedAnswer,
+ status: "pending",
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ eq(loomMigrationRequests.status, "needs_information"),
+ ),
+ );
+ if (affectedRows(result) === 0) {
+ throw new Error("The migration request changed. Refresh and try again.");
+ }
+ refreshMigrationPages();
+}
+
+export async function getLoomMigrationOperatorQueue() {
+ return getOperatorMigrationQueue();
+}
+
+export async function updateLoomMigrationStatus({
+ requestId,
+ nextStatus,
+ message,
+ verified,
+ expectedVideoCount,
+ importedVideoCount,
+}: {
+ requestId: string;
+ nextStatus: LoomMigrationStatus;
+ message: string;
+ verified: boolean;
+ expectedVideoCount: number | null;
+ importedVideoCount: number;
+}) {
+ const operator = await requireMigrationOperator();
+ const request = await findMigration(requestId);
+ const normalizedMessage = validateOperatorMigrationUpdate({
+ currentStatus: request.status,
+ nextStatus,
+ message,
+ verified,
+ expectedVideoCount,
+ importedVideoCount,
+ });
+ if (
+ nextStatus === request.status &&
+ (normalizedMessage || null) === request.capMessage &&
+ expectedVideoCount === request.expectedVideoCount &&
+ importedVideoCount === request.importedVideoCount
+ ) {
+ return;
+ }
+ if (nextStatus === "completed") {
+ const [unfinishedImport] = await db()
+ .select({ videoId: videoUploads.videoId })
+ .from(importedVideos)
+ .innerJoin(videoUploads, eq(videoUploads.videoId, importedVideos.id))
+ .where(
+ and(
+ eq(importedVideos.orgId, request.organizationId),
+ eq(importedVideos.source, "loom"),
+ inArray(videoUploads.phase, [
+ "uploading",
+ "processing",
+ "generating_thumbnail",
+ "error",
+ ]),
+ ),
+ )
+ .limit(1);
+ if (unfinishedImport) {
+ throw new Error(
+ "Loom imports are still processing or have errors. Resolve them before completion.",
+ );
+ }
+ }
+ const [organization] = await db()
+ .select({ name: organizations.name })
+ .from(organizations)
+ .where(eq(organizations.id, request.organizationId))
+ .limit(1);
+ const result = await db()
+ .update(loomMigrationRequests)
+ .set({
+ status: nextStatus,
+ capMessage: normalizedMessage || null,
+ expectedVideoCount,
+ importedVideoCount,
+ activeOrganizationId:
+ nextStatus === "completed" ? null : request.organizationId,
+ completedAt: nextStatus === "completed" ? new Date() : null,
+ lastOperatorUserId: operator.id,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ eq(loomMigrationRequests.status, request.status),
+ ),
+ );
+ if (affectedRows(result) === 0) {
+ throw new Error("The migration request changed. Refresh and try again.");
+ }
+
+ const [requester] = await db()
+ .select({ email: users.email })
+ .from(users)
+ .where(eq(users.id, request.requestedByUserId))
+ .limit(1);
+ if (requester) {
+ try {
+ const delivery = await sendEmail({
+ email: requester.email,
+ subject: `Loom migration: ${LOOM_MIGRATION_STATUS_LABELS[nextStatus]}`,
+ react: (
+
+ ),
+ replyTo: SUPPORT_EMAIL,
+ idempotencyKey: `loom-migration-status-${requestId}-${Date.now()}`,
+ });
+ if (delivery?.error) {
+ console.error(
+ "Failed to send Loom migration status notice",
+ delivery.error,
+ );
+ }
+ } catch (error) {
+ console.error("Failed to send Loom migration status notice", error);
+ }
+ }
+ refreshMigrationPages();
+}
diff --git a/apps/web/actions/loom.ts b/apps/web/actions/loom.ts
index b9e03dcecf9..7cb49e13fac 100644
--- a/apps/web/actions/loom.ts
+++ b/apps/web/actions/loom.ts
@@ -7,7 +7,9 @@ import { nanoId } from "@cap/database/helpers";
import {
folders,
importedVideos,
+ loomMigrationRequests,
organizationMembers,
+ organizations,
sharedVideos,
spaceMembers,
spaces,
@@ -23,11 +25,12 @@ import {
type Organisation,
Space,
SpaceMemberId,
+ type Storage as StorageDomain,
type User,
Video,
} from "@cap/web-domain";
-import { and, asc, eq, isNull } from "drizzle-orm";
-import { Option } from "effect";
+import { and, asc, eq, isNotNull, isNull, sql } from "drizzle-orm";
+import { Effect, Option, Schedule } from "effect";
import { revalidatePath } from "next/cache";
import { start } from "workflow/api";
import {
@@ -36,10 +39,14 @@ import {
requireOrganizationSettingsManager,
} from "@/actions/organization/authorization";
import { requireSpaceManager } from "@/actions/organization/space-authorization";
+import { requireMigrationOperator } from "@/lib/loom-concierge";
import type { LoomImportDestination } from "@/lib/loom-import-destination";
+import { isOrganizationOwnerPro } from "@/lib/org-pro";
import { provisionOrganizationInvitee } from "@/lib/organization-provisioning";
import { canManageOrganizationSettings } from "@/lib/permissions/roles";
import { runPromise } from "@/lib/server";
+import { startVideoProcessingWorkflow } from "@/lib/video-processing";
+import { decodeStorageVideo } from "@/lib/video-storage";
import { importLoomVideoWorkflow } from "@/workflows/import-loom-video";
interface LoomUrlResponse {
@@ -97,10 +104,22 @@ const LOOM_CSV_LIMIT_ERROR = `CSV imports are limited to ${MAX_LOOM_CSV_ROWS} ro
const LOOM_CSV_PERMISSION_ERROR =
"Only organization admins and owners can import Loom videos from a CSV.";
+function affectedRows(result: unknown) {
+ if (Array.isArray(result)) {
+ return (
+ (result[0] as { affectedRows?: number } | undefined)?.affectedRows ?? 0
+ );
+ }
+ return (result as { affectedRows?: number } | undefined)?.affectedRows ?? 0;
+}
+
function extractLoomVideoId(url: string): string | null {
try {
const parsed = new URL(url);
- if (!parsed.hostname.includes("loom.com")) {
+ if (
+ parsed.protocol !== "https:" ||
+ (parsed.hostname !== "loom.com" && !parsed.hostname.endsWith(".loom.com"))
+ ) {
return null;
}
@@ -770,6 +789,18 @@ export async function importFromLoomCsv({
};
}
+ return processLoomCsvRows({ rows, orgId, actorId: user.id });
+}
+
+async function processLoomCsvRows({
+ rows,
+ orgId,
+ actorId,
+}: {
+ rows: LoomCsvImportRow[];
+ orgId: Organisation.OrganisationId;
+ actorId: User.UserId;
+}): Promise {
const inputRows = Array.isArray(rows) ? rows : [];
const normalizedRows = inputRows
.map((row, index) => ({
@@ -854,7 +885,7 @@ export async function importFromLoomCsv({
const provisionedMember = await provisionOrganizationInvitee({
organizationId: orgId,
email: row.userEmail,
- invitedByUserId: user.id,
+ invitedByUserId: actorId,
role: "member",
});
member = {
@@ -886,14 +917,14 @@ export async function importFromLoomCsv({
try {
const space = await getOrCreateImportSpace({
orgId,
- createdById: user.id,
+ createdById: actorId,
name: row.spaceName,
spaceCache,
});
await addImportedVideoToSpace({
videoId: result.videoId,
spaceId: space.id,
- addedById: user.id,
+ addedById: actorId,
});
await addImportOwnerToSpace({
spaceId: space.id,
@@ -944,3 +975,389 @@ export async function importFromLoomCsv({
error: importedCount > 0 ? undefined : "No Loom videos were imported.",
};
}
+
+export async function importFromLoomCsvForConcierge({
+ requestId,
+ rows,
+}: {
+ requestId: string;
+ rows: LoomCsvImportRow[];
+}): Promise {
+ const operator = await requireMigrationOperator();
+ if (typeof requestId !== "string" || !/^[A-Za-z0-9_-]{15}$/.test(requestId)) {
+ throw new Error("Invalid migration request.");
+ }
+ const [request] = await db()
+ .select({
+ organizationId: loomMigrationRequests.organizationId,
+ status: loomMigrationRequests.status,
+ ownerId: organizations.ownerId,
+ })
+ .from(loomMigrationRequests)
+ .innerJoin(
+ organizations,
+ eq(organizations.id, loomMigrationRequests.organizationId),
+ )
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ isNull(organizations.tombstoneAt),
+ ),
+ )
+ .limit(1);
+ if (!request || request.status === "completed") {
+ throw new Error("The migration request is no longer active.");
+ }
+ if (!(await isOrganizationOwnerPro(request.organizationId))) {
+ throw new Error("The destination workspace needs Cap Pro.");
+ }
+ const claimed = await db()
+ .update(loomMigrationRequests)
+ .set({
+ status: "in_progress",
+ lastOperatorUserId: operator.id,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ if (affectedRows(claimed) === 0) {
+ throw new Error("The migration request changed. Refresh and try again.");
+ }
+ const result = await processLoomCsvRows({
+ rows,
+ orgId: request.organizationId,
+ actorId: request.ownerId,
+ });
+ if (result.importedCount > 0) {
+ const updated = await db()
+ .update(loomMigrationRequests)
+ .set({
+ queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + ${result.importedCount}`,
+ lastOperatorUserId: operator.id,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ if (affectedRows(updated) === 0) {
+ throw new Error(
+ "The migration request changed after jobs started. Review them before retrying.",
+ );
+ }
+ }
+ revalidatePath("/dashboard/migrations/loom");
+ revalidatePath("/dashboard/admin/loom-migrations");
+ return result;
+}
+
+export async function createConciergeLoomFileUpload({
+ requestId,
+ loomUrl,
+ userEmail,
+ spaceName,
+ videoTitle,
+}: {
+ requestId: string;
+ loomUrl: string;
+ userEmail: string;
+ spaceName: string;
+ videoTitle: string;
+}): Promise<{
+ videoId: Video.VideoId;
+ uploadTarget: StorageDomain.UploadTarget;
+}> {
+ const operator = await requireMigrationOperator();
+ if (typeof requestId !== "string" || !/^[A-Za-z0-9_-]{15}$/.test(requestId)) {
+ throw new Error("Invalid migration request.");
+ }
+ const loomVideoId = extractLoomVideoId(loomUrl.trim());
+ if (!loomVideoId) throw new Error("Enter a valid Loom video URL.");
+ const normalizedEmail = normalizeImportEmail(userEmail);
+ if (!isValidImportEmail(normalizedEmail)) {
+ throw new Error("Enter a valid destination owner email.");
+ }
+ const normalizedSpaceName = normalizeImportSpaceName(spaceName);
+ if (!isValidImportSpaceName(normalizedSpaceName)) {
+ throw new Error("Space name is too long.");
+ }
+ const normalizedTitle = videoTitle.trim();
+ if (!normalizedTitle || normalizedTitle.length > 255) {
+ throw new Error("Enter a video title of 255 characters or fewer.");
+ }
+ const [request] = await db()
+ .select({
+ organizationId: loomMigrationRequests.organizationId,
+ ownerId: organizations.ownerId,
+ })
+ .from(loomMigrationRequests)
+ .innerJoin(
+ organizations,
+ eq(organizations.id, loomMigrationRequests.organizationId),
+ )
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ isNull(organizations.tombstoneAt),
+ ),
+ )
+ .limit(1);
+ if (!request) throw new Error("The migration request is no longer active.");
+ if (!(await isOrganizationOwnerPro(request.organizationId))) {
+ throw new Error("The destination workspace needs Cap Pro.");
+ }
+ const [existing] = await db()
+ .select({
+ id: importedVideos.id,
+ ownerId: videos.ownerId,
+ ownerEmail: users.email,
+ bucket: videos.bucket,
+ storageIntegrationId: videos.storageIntegrationId,
+ rawFileKey: videoUploads.rawFileKey,
+ phase: videoUploads.phase,
+ processingMessage: videoUploads.processingMessage,
+ })
+ .from(importedVideos)
+ .leftJoin(
+ videos,
+ and(
+ eq(videos.id, importedVideos.id),
+ eq(videos.orgId, importedVideos.orgId),
+ ),
+ )
+ .leftJoin(users, eq(users.id, videos.ownerId))
+ .leftJoin(videoUploads, eq(videoUploads.videoId, importedVideos.id))
+ .where(
+ and(
+ eq(importedVideos.orgId, request.organizationId),
+ eq(importedVideos.source, "loom"),
+ eq(importedVideos.sourceId, loomVideoId),
+ ),
+ )
+ .limit(1);
+ if (existing) {
+ if (
+ !existing.ownerId ||
+ !existing.ownerEmail ||
+ normalizeImportEmail(existing.ownerEmail) !== normalizedEmail ||
+ !existing.rawFileKey ||
+ (existing.phase !== "error" &&
+ (existing.phase !== "uploading" ||
+ existing.processingMessage !== "Uploading Loom video..."))
+ ) {
+ throw new Error(
+ "This Loom video is already in the Cap import inventory.",
+ );
+ }
+ const retryUpload = await Storage.createUploadTargetForUser(
+ existing.ownerId,
+ existing.rawFileKey,
+ {
+ contentType: "video/mp4",
+ videoTitle: normalizedTitle,
+ method: "put",
+ fields: { "x-amz-meta-userid": existing.ownerId },
+ },
+ request.organizationId,
+ ).pipe(runPromise);
+ if (
+ Option.getOrNull(retryUpload.bucketId) !== existing.bucket ||
+ Option.getOrNull(retryUpload.storageIntegrationId) !==
+ existing.storageIntegrationId
+ ) {
+ throw new Error(
+ "The Cap storage destination changed. Review this upload before retrying.",
+ );
+ }
+ return {
+ videoId: Video.VideoId.make(existing.id),
+ uploadTarget: retryUpload.upload,
+ };
+ }
+ let member = await getOrganizationMemberByEmail(
+ request.organizationId,
+ normalizedEmail,
+ );
+ if (!member) {
+ const provisioned = await provisionOrganizationInvitee({
+ organizationId: request.organizationId,
+ email: normalizedEmail,
+ invitedByUserId: request.ownerId,
+ role: "member",
+ });
+ member = { userId: provisioned.userId, email: normalizedEmail };
+ }
+ const space = normalizedSpaceName
+ ? await getOrCreateImportSpace({
+ orgId: request.organizationId,
+ createdById: request.ownerId,
+ name: normalizedSpaceName,
+ spaceCache: new Map(),
+ })
+ : null;
+ if (space) {
+ await addImportOwnerToSpace({ spaceId: space.id, userId: member.userId });
+ }
+ const videoId = Video.VideoId.make(nanoId());
+ const rawFileKey = `${member.userId}/${videoId}/raw-upload.mp4`;
+ const upload = await Storage.createUploadTargetForUser(
+ member.userId,
+ rawFileKey,
+ {
+ contentType: "video/mp4",
+ videoTitle: normalizedTitle,
+ method: "put",
+ fields: { "x-amz-meta-userid": member.userId },
+ },
+ request.organizationId,
+ ).pipe(runPromise);
+ await db().transaction(async (tx) => {
+ await tx.insert(videos).values({
+ id: videoId,
+ name: normalizedTitle,
+ ownerId: member.userId,
+ orgId: request.organizationId,
+ source: { type: "webMP4" as const },
+ bucket: Option.getOrNull(upload.bucketId),
+ storageIntegrationId: Option.getOrNull(upload.storageIntegrationId),
+ public: serverEnv().CAP_VIDEOS_DEFAULT_PUBLIC,
+ });
+ await tx.insert(videoUploads).values({
+ videoId,
+ mode: "singlepart",
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: "Uploading Loom video...",
+ rawFileKey,
+ });
+ await tx.insert(importedVideos).values({
+ id: videoId,
+ orgId: request.organizationId,
+ source: "loom",
+ sourceId: loomVideoId,
+ });
+ if (space) {
+ await tx.insert(spaceVideos).values({
+ id: nanoId(),
+ videoId,
+ spaceId: space.id,
+ addedById: request.ownerId,
+ });
+ }
+ });
+ await db()
+ .update(loomMigrationRequests)
+ .set({
+ status: "in_progress",
+ lastOperatorUserId: operator.id,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ revalidatePath("/dashboard/admin/loom-migrations");
+ return { videoId, uploadTarget: upload.upload };
+}
+
+export async function finishConciergeLoomFileUpload({
+ requestId,
+ videoId,
+}: {
+ requestId: string;
+ videoId: Video.VideoId;
+}) {
+ const operator = await requireMigrationOperator();
+ if (typeof requestId !== "string" || !/^[A-Za-z0-9_-]{15}$/.test(requestId)) {
+ throw new Error("Invalid migration request.");
+ }
+ const [record] = await db()
+ .select({
+ video: videos,
+ rawFileKey: videoUploads.rawFileKey,
+ phase: videoUploads.phase,
+ processingMessage: videoUploads.processingMessage,
+ })
+ .from(videos)
+ .innerJoin(
+ importedVideos,
+ and(
+ eq(importedVideos.id, videos.id),
+ eq(importedVideos.orgId, videos.orgId),
+ ),
+ )
+ .innerJoin(videoUploads, eq(videoUploads.videoId, videos.id))
+ .innerJoin(
+ loomMigrationRequests,
+ eq(loomMigrationRequests.organizationId, importedVideos.orgId),
+ )
+ .where(
+ and(
+ eq(videos.id, videoId),
+ eq(loomMigrationRequests.id, requestId),
+ eq(importedVideos.source, "loom"),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ )
+ .limit(1);
+ if (
+ !record ||
+ !record.rawFileKey ||
+ (record.phase !== "error" &&
+ (record.phase !== "uploading" ||
+ record.processingMessage !== "Uploading Loom video..."))
+ ) {
+ throw new Error("This Loom upload is no longer available.");
+ }
+ const [bucket] = await Storage.getAccessForVideo(
+ decodeStorageVideo(record.video),
+ ).pipe(runPromise);
+ const head = await bucket
+ .headObject(record.rawFileKey)
+ .pipe(
+ Effect.retry({ times: 3, schedule: Schedule.exponential("100 millis") }),
+ runPromise,
+ );
+ if ((head.ContentLength ?? 0) <= 0)
+ throw new Error("The uploaded file is empty.");
+ const status = await startVideoProcessingWorkflow({
+ videoId,
+ userId: record.video.ownerId,
+ rawFileKey: record.rawFileKey,
+ bucketId: record.video.bucket,
+ processingMessage: "Processing Loom video...",
+ startFailureMessage: "Loom file processing could not start.",
+ });
+ if (status === "started") {
+ await db()
+ .update(loomMigrationRequests)
+ .set({
+ queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + 1`,
+ lastOperatorUserId: operator.id,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ }
+ revalidatePath("/dashboard/migrations/loom");
+ revalidatePath("/dashboard/admin/loom-migrations");
+ return { status };
+}
diff --git a/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx b/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
index e91138d70e5..d5d0ebfe639 100644
--- a/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
+++ b/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
@@ -46,6 +46,7 @@ import {
loomImportDestinationFromPathname,
loomImportPageHref,
} from "@/lib/loom-import-destination";
+import { MESSENGER_ADMIN_EMAIL } from "@/lib/messenger/constants";
import {
canViewOrganizationSettings,
getEffectiveOrganizationRole,
@@ -78,6 +79,9 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => {
const showDeveloperDashboard =
buildEnv.NEXT_PUBLIC_IS_CAP &&
DEVELOPER_DASHBOARD_ALLOWED_EMAILS.includes(user.email);
+ const showMigrationQueue =
+ buildEnv.NEXT_PUBLIC_IS_CAP &&
+ user.email.toLowerCase() === MESSENGER_ADMIN_EMAIL;
const manageNavigation = [
{
@@ -107,6 +111,17 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => {
icon: ,
subNav: [],
},
+ ...(buildEnv.NEXT_PUBLIC_IS_CAP
+ ? [
+ {
+ name: "Loom Migration",
+ href: "/dashboard/migrations/loom",
+ adminOnly: true,
+ icon: ,
+ subNav: [],
+ },
+ ]
+ : []),
{
name: "Organization Settings",
href: `/dashboard/settings/organization`,
@@ -127,6 +142,16 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => {
},
]
: []),
+ ...(showMigrationQueue
+ ? [
+ {
+ name: "Migration Queue",
+ href: "/dashboard/admin/loom-migrations",
+ icon: ,
+ subNav: [],
+ },
+ ]
+ : []),
];
const [dialogOpen, setDialogOpen] = useState(false);
diff --git a/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx b/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
new file mode 100644
index 00000000000..d256a9aded3
--- /dev/null
+++ b/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
@@ -0,0 +1,527 @@
+"use client";
+
+import { Button } from "@cap/ui";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { LoaderCircle } from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import {
+ createConciergeLoomFileUpload,
+ finishConciergeLoomFileUpload,
+ importFromLoomCsvForConcierge,
+ type LoomCsvImportRow,
+ type LoomCsvImportRowResult,
+} from "@/actions/loom";
+import {
+ getLoomMigrationOperatorQueue,
+ updateLoomMigrationStatus,
+} from "@/actions/loom-concierge";
+import type { OperatorLoomMigrationView } from "@/lib/loom-concierge";
+import { parseConciergeLoomCsv } from "@/lib/loom-csv";
+import {
+ LOOM_MIGRATION_STATUS_LABELS,
+ type LoomMigrationStatus,
+} from "@/lib/loom-migration-state";
+import { uploadWithTarget } from "@/utils/upload-target";
+
+const BATCH_SIZE = 10;
+const BATCH_DELAY_MS = 1500;
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function QueueItem({ request }: { request: OperatorLoomMigrationView }) {
+ const queryClient = useQueryClient();
+ const [nextStatus, setNextStatus] = useState(
+ request.status,
+ );
+ const [message, setMessage] = useState(request.capMessage ?? "");
+ const [expectedVideoCount, setExpectedVideoCount] = useState(
+ request.expectedVideoCount?.toString() ?? "",
+ );
+ const [importedVideoCount, setImportedVideoCount] = useState(
+ request.importedVideoCount.toString(),
+ );
+ const [verified, setVerified] = useState(false);
+ const [csvRows, setCsvRows] = useState([]);
+ const [processedRows, setProcessedRows] = useState(0);
+ const [startedRows, setStartedRows] = useState(0);
+ const [failedRows, setFailedRows] = useState([]);
+ const [fileLoomUrl, setFileLoomUrl] = useState("");
+ const [fileOwnerEmail, setFileOwnerEmail] = useState("");
+ const [fileSpaceName, setFileSpaceName] = useState("");
+ const [fileTitle, setFileTitle] = useState("");
+ const [file, setFile] = useState(null);
+ const [uploadPercent, setUploadPercent] = useState(0);
+
+ useEffect(() => {
+ setNextStatus(request.status);
+ setMessage(request.capMessage ?? "");
+ setExpectedVideoCount(request.expectedVideoCount?.toString() ?? "");
+ setImportedVideoCount(request.importedVideoCount.toString());
+ setVerified(false);
+ }, [
+ request.status,
+ request.capMessage,
+ request.expectedVideoCount,
+ request.importedVideoCount,
+ ]);
+
+ const refresh = () =>
+ queryClient.invalidateQueries({ queryKey: ["loom-migration-queue"] });
+ const statusMutation = useMutation({
+ mutationFn: () =>
+ updateLoomMigrationStatus({
+ requestId: request.id,
+ nextStatus,
+ message,
+ verified,
+ expectedVideoCount:
+ expectedVideoCount.trim() === "" ? null : Number(expectedVideoCount),
+ importedVideoCount: Number(importedVideoCount),
+ }),
+ onSuccess: async () => {
+ await refresh();
+ toast.success("Migration status updated.");
+ },
+ onError: (error) => toast.error(error.message),
+ });
+ const importMutation = useMutation({
+ mutationFn: async () => {
+ if (csvRows.length === 0) throw new Error("Choose a Loom CSV first.");
+ setProcessedRows(0);
+ setStartedRows(0);
+ setFailedRows([]);
+ let started = 0;
+ const failures: LoomCsvImportRowResult[] = [];
+ for (let index = 0; index < csvRows.length; index += BATCH_SIZE) {
+ const batch = csvRows.slice(index, index + BATCH_SIZE);
+ const result = await importFromLoomCsvForConcierge({
+ requestId: request.id,
+ rows: batch,
+ });
+ started += result.importedCount;
+ failures.push(...result.results.filter((row) => !row.success));
+ setProcessedRows(index + batch.length);
+ setStartedRows(started);
+ setFailedRows([...failures]);
+ if (index + BATCH_SIZE < csvRows.length) await delay(BATCH_DELAY_MS);
+ }
+ return { started, failures };
+ },
+ onSuccess: async (result) => {
+ await refresh();
+ toast.success(
+ `${result.started} imports started. ${result.failures.length} rows need review. Wait for processing and reconcile the library before completion.`,
+ );
+ },
+ onError: async (error) => {
+ await refresh();
+ toast.error(
+ `${error.message} Check the rows already started before retrying.`,
+ );
+ },
+ });
+ const fileMutation = useMutation({
+ mutationFn: async () => {
+ if (!file || !fileLoomUrl.trim() || !fileOwnerEmail.trim()) {
+ throw new Error("Choose an MP4 file, Loom URL, and destination owner.");
+ }
+ if (!file.name.toLowerCase().endsWith(".mp4")) {
+ throw new Error("Choose an MP4 downloaded from Loom.");
+ }
+ if (file.size === 0) throw new Error("The MP4 file is empty.");
+ const prepared = await createConciergeLoomFileUpload({
+ requestId: request.id,
+ loomUrl: fileLoomUrl,
+ userEmail: fileOwnerEmail,
+ spaceName: fileSpaceName,
+ videoTitle: fileTitle.trim() || file.name.replace(/\.mp4$/i, ""),
+ });
+ setUploadPercent(0);
+ await uploadWithTarget({
+ target: prepared.uploadTarget,
+ body: file,
+ fileName: file.name,
+ onProgress: ({ loaded, total }) => {
+ if (total > 0) setUploadPercent(Math.round((loaded / total) * 100));
+ },
+ });
+ return finishConciergeLoomFileUpload({
+ requestId: request.id,
+ videoId: prepared.videoId,
+ });
+ },
+ onSuccess: async (result) => {
+ await refresh();
+ setFile(null);
+ setUploadPercent(0);
+ toast.success(
+ result.status === "started"
+ ? "Loom file uploaded. Processing has started."
+ : "Loom file uploaded. Processing was already underway.",
+ );
+ },
+ onError: async (error) => {
+ await refresh();
+ toast.error(
+ `${error.message} The same Loom URL can be retried after checking its upload.`,
+ );
+ },
+ });
+
+ return (
+
+
+
+
+ {request.organizationName}
+
+
+ {request.requestedByEmail} · {request.organizationId} · Requested{" "}
+ {new Date(request.createdAt).toLocaleDateString()}
+
+
+
+ {LOOM_MIGRATION_STATUS_LABELS[request.status]}
+
+
+
+
Loom workspace: {request.workspaceName || "Not provided"}
+
+ Loom invite:{" "}
+ {request.invitedAt ? "Marked as sent" : "Waiting for customer"}
+
+
Jobs started: {request.queuedVideoCount}
+
Verified videos: {request.importedVideoCount}
+
+ {request.customerNote && (
+
+ Customer note: {request.customerNote}
+
+ )}
+ {request.customerReply && (
+
+ Latest customer answer: {request.customerReply}
+
+ )}
+
+
+
Import a mapped Loom CSV
+
+ Use loom_video_url,user_email,space_name. The destination is this Cap
+ organization; imports run in batches of 10. Started jobs still need
+ terminal processing checks.
+
+
+ Loom CSV
+
+
{
+ const file = event.target.files?.[0];
+ if (!file) return;
+ try {
+ setCsvRows(parseConciergeLoomCsv(await file.text()));
+ setProcessedRows(0);
+ setStartedRows(0);
+ setFailedRows([]);
+ } catch (error) {
+ setCsvRows([]);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Could not read the CSV.",
+ );
+ }
+ }}
+ type="file"
+ />
+ {csvRows.length > 0 && (
+
+ {csvRows.length} rows ready. Review the source inventory and owner
+ mapping before import.
+
+ )}
+
importMutation.mutate()}
+ size="sm"
+ variant="dark"
+ >
+ {importMutation.isPending
+ ? `Importing ${processedRows}/${csvRows.length}`
+ : "Start import"}
+
+ {processedRows > 0 && (
+
+ {startedRows} jobs started; {failedRows.length} rows failed to
+ start.
+
+ )}
+ {failedRows.length > 0 && (
+
+ {failedRows.map((row) => (
+
+ Row {row.rowNumber}: {row.error || "Could not start import."}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export function LoomMigrationQueue() {
+ const queueQuery = useQuery({
+ queryKey: ["loom-migration-queue"],
+ queryFn: getLoomMigrationOperatorQueue,
+ refetchInterval: 20_000,
+ });
+ return (
+
+
+
+ Loom migration queue
+
+
+ Process Cap Pro concierge requests, reconcile every import, and update
+ customers here.
+
+
+ {queueQuery.isPending && (
+
+ Loading requests…
+
+ )}
+ {queueQuery.isError && (
+
+ Could not load the queue. {queueQuery.error.message}
+ queueQuery.refetch()}
+ type="button"
+ >
+ Try again
+
+
+ )}
+ {queueQuery.data?.length === 0 && (
+
+ No active Loom migration requests.
+
+ )}
+ {queueQuery.data?.map((request) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/web/app/(org)/dashboard/admin/loom-migrations/page.tsx b/apps/web/app/(org)/dashboard/admin/loom-migrations/page.tsx
new file mode 100644
index 00000000000..2dc9ff503ae
--- /dev/null
+++ b/apps/web/app/(org)/dashboard/admin/loom-migrations/page.tsx
@@ -0,0 +1,17 @@
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { requireMigrationOperator } from "@/lib/loom-concierge";
+import { LoomMigrationQueue } from "./LoomMigrationQueue";
+
+export const metadata: Metadata = {
+ title: "Loom Migration Queue — Cap",
+};
+
+export default async function Page() {
+ try {
+ await requireMigrationOperator();
+ } catch {
+ notFound();
+ }
+ return ;
+}
diff --git a/apps/web/app/(org)/dashboard/import/ImportPage.tsx b/apps/web/app/(org)/dashboard/import/ImportPage.tsx
index 9712a3af2ec..1cf0fe84795 100644
--- a/apps/web/app/(org)/dashboard/import/ImportPage.tsx
+++ b/apps/web/app/(org)/dashboard/import/ImportPage.tsx
@@ -1,18 +1,34 @@
"use client";
-import { faUpload } from "@fortawesome/free-solid-svg-icons";
+import { buildEnv } from "@cap/env";
+import { faArrowsRotate, faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Link from "next/link";
+import { useDashboardContext } from "@/app/(org)/dashboard/Contexts";
import {
type LoomImportDestination,
loomImportPageHref,
} from "@/lib/loom-import-destination";
+import {
+ canManageOrganizationSettings,
+ getEffectiveOrganizationRole,
+} from "@/lib/permissions/roles";
export const ImportPage = ({
initialDestination = {},
}: {
initialDestination?: LoomImportDestination;
}) => {
+ const { user, activeOrganization } = useDashboardContext();
+ const currentMember = activeOrganization?.members.find(
+ (member) => member.userId === user.id,
+ );
+ const currentRole = getEffectiveOrganizationRole({
+ userId: user.id,
+ ownerId: activeOrganization?.organization.ownerId,
+ memberRole: currentMember?.role,
+ });
+ const canRequestMigration = canManageOrganizationSettings(currentRole);
return (
@@ -71,6 +87,26 @@ export const ImportPage = ({
+ {buildEnv.NEXT_PUBLIC_IS_CAP && canRequestMigration && (
+
+
+
+
+ Concierge Loom Migration
+
+
+ Let Cap move your Loom workspace. Free with Cap Pro.
+
+
+
+ )}
);
diff --git a/apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx b/apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx
index ebca8079829..8e5db52a251 100644
--- a/apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx
+++ b/apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx
@@ -1,5 +1,6 @@
"use client";
+import { buildEnv } from "@cap/env";
import {
Button,
Dialog,
@@ -55,6 +56,7 @@ import {
} from "@/actions/loom";
import { useDashboardContext } from "@/app/(org)/dashboard/Contexts";
import { UpgradeModal } from "@/components/UpgradeModal";
+import { parseLoomCsvRecords } from "@/lib/loom-csv";
import {
type LoomImportDestination,
loomImportDestinationHref,
@@ -132,57 +134,8 @@ function buildCsvImportResult(
};
}
-function parseCsvRecords(text: string) {
- const records: string[][] = [];
- let field = "";
- let row: string[] = [];
- let inQuotes = false;
- const input = text.replace(/^\uFEFF/, "");
-
- for (let index = 0; index < input.length; index += 1) {
- const char = input.charAt(index);
- const next = input.charAt(index + 1);
-
- if (char === '"') {
- if (inQuotes && next === '"') {
- field += '"';
- index += 1;
- } else {
- inQuotes = !inQuotes;
- }
- continue;
- }
-
- if (char === "," && !inQuotes) {
- row.push(field.trim());
- field = "";
- continue;
- }
-
- if ((char === "\n" || char === "\r") && !inQuotes) {
- if (char === "\r" && next === "\n") index += 1;
- row.push(field.trim());
- if (row.some((cell) => cell.length > 0)) records.push(row);
- row = [];
- field = "";
- continue;
- }
-
- field += char;
- }
-
- if (inQuotes) throw new Error("CSV has an unclosed quoted field.");
-
- if (field.length > 0 || row.length > 0) {
- row.push(field.trim());
- if (row.some((cell) => cell.length > 0)) records.push(row);
- }
-
- return records;
-}
-
function parseCsv(text: string, fileName: string): CsvData {
- const records = parseCsvRecords(text);
+ const records = parseLoomCsvRecords(text);
const headers = records[0]?.map((header) => header.trim()) ?? [];
const rows = records
.slice(1)
@@ -601,6 +554,14 @@ export const ImportLoomPage = ({
? "Bring a single Loom video into Cap, or bulk import recordings for organization members and new users from a CSV."
: "Paste a Loom share link to bring it into Cap."}
+ {buildEnv.NEXT_PUBLIC_IS_CAP && canUseCsvImport && (
+
+ Want Cap to move your whole Loom workspace for you?
+
+ )}
diff --git a/apps/web/app/(org)/dashboard/migrations/loom/LoomMigrationPage.tsx b/apps/web/app/(org)/dashboard/migrations/loom/LoomMigrationPage.tsx
new file mode 100644
index 00000000000..63129848ccf
--- /dev/null
+++ b/apps/web/app/(org)/dashboard/migrations/loom/LoomMigrationPage.tsx
@@ -0,0 +1,398 @@
+"use client";
+
+import { Button } from "@cap/ui";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { CheckCircle2, CircleHelp, Clock3, LoaderCircle } from "lucide-react";
+import Link from "next/link";
+import { useId, useState } from "react";
+import { toast } from "sonner";
+import {
+ answerLoomMigrationQuestion,
+ confirmLoomMigrationInvite,
+ getLoomMigrationDashboard,
+ requestLoomMigration,
+} from "@/actions/loom-concierge";
+import { useDashboardContext } from "@/app/(org)/dashboard/Contexts";
+import type { LoomMigrationView } from "@/lib/loom-concierge";
+import {
+ LOOM_MIGRATION_STATUS_LABELS,
+ type LoomMigrationStatus,
+} from "@/lib/loom-migration-state";
+
+const STATUS_STYLE: Record = {
+ pending: "bg-amber-3 text-amber-11",
+ in_progress: "bg-blue-3 text-blue-11",
+ needs_information: "bg-orange-3 text-orange-11",
+ completed: "bg-green-3 text-green-11",
+};
+
+function MigrationStatus({ request }: { request: LoomMigrationView }) {
+ return (
+
+
+
+
Your migration
+
+ Requested on {new Date(request.createdAt).toLocaleDateString()}
+ {request.workspaceName ? ` for ${request.workspaceName}` : ""}
+
+
+
+ {LOOM_MIGRATION_STATUS_LABELS[request.status]}
+
+
+ {request.status === "pending" && (
+
+ Your request is in Cap’s queue. We’ll review your Loom workspace and
+ confirm the migration plan.
+
+ )}
+ {request.status === "in_progress" && (
+
+ Cap is moving your videos. We’ll mark this complete after checking the
+ imported library.
+
+ )}
+ {request.status === "completed" && (
+
+
+
+
Your migration is complete.
+ {request.expectedVideoCount !== null && (
+
+ {request.importedVideoCount} of {request.expectedVideoCount}{" "}
+ videos verified in Cap.
+
+ )}
+
+ View your Caps
+
+
+
+ )}
+ {request.capMessage && (
+
+
Update from Cap
+
{request.capMessage}
+
+ )}
+ {request.customerReply && (
+
+ Your latest reply: {request.customerReply}
+
+ )}
+
+ );
+}
+
+export function LoomMigrationPage() {
+ const { activeOrganization, user } = useDashboardContext();
+ const organizationId = activeOrganization?.organization.id;
+ const queryClient = useQueryClient();
+ const [requestFormOpen, setRequestFormOpen] = useState(false);
+ const [workspaceName, setWorkspaceName] = useState("");
+ const [note, setNote] = useState("");
+ const [answer, setAnswer] = useState("");
+ const answerId = useId();
+ const workspaceNameId = useId();
+ const noteId = useId();
+
+ const migrationQuery = useQuery({
+ queryKey: ["loom-concierge", organizationId],
+ queryFn: () => {
+ if (!organizationId) throw new Error("Select a Cap workspace.");
+ return getLoomMigrationDashboard(organizationId);
+ },
+ enabled: Boolean(organizationId),
+ refetchInterval: 30_000,
+ });
+
+ const refresh = () =>
+ queryClient.invalidateQueries({
+ queryKey: ["loom-concierge", organizationId],
+ });
+ const requestMutation = useMutation({
+ mutationFn: () => {
+ if (!organizationId) throw new Error("Select a Cap workspace.");
+ return requestLoomMigration({ organizationId, workspaceName, note });
+ },
+ onSuccess: async (result) => {
+ await refresh();
+ setRequestFormOpen(false);
+ toast.success(
+ result.alreadyRequested
+ ? "Your migration request is already in the queue."
+ : "Migration requested. Invite hello@cap.so in Loom next.",
+ );
+ },
+ onError: (error) => toast.error(error.message),
+ });
+ const inviteMutation = useMutation({
+ mutationFn: confirmLoomMigrationInvite,
+ onSuccess: async () => {
+ await refresh();
+ toast.success("Thanks. Cap can now review your Loom invite.");
+ },
+ onError: (error) => toast.error(error.message),
+ });
+ const answerMutation = useMutation({
+ mutationFn: answerLoomMigrationQuestion,
+ onSuccess: async () => {
+ setAnswer("");
+ await refresh();
+ toast.success("Your answer has been sent to Cap.");
+ },
+ onError: (error) => toast.error(error.message),
+ });
+
+ const requests = migrationQuery.data?.requests ?? [];
+ const activeRequest = requests.find(
+ (request) => request.status !== "completed",
+ );
+ const completedRequest = requests.find(
+ (request) => request.status === "completed",
+ );
+ const isPro = migrationQuery.data?.isPro ?? false;
+
+ return (
+
+
+
+ Switch from Loom
+
+
+ Move your Loom library to Cap
+
+
+ Import it yourself, or let Cap handle the move. Concierge migration is
+ included with Cap Pro at no extra cost.
+
+
+
+ {!organizationId && (
+
+ Select a Cap workspace to request a Loom migration.
+
+ )}
+ {organizationId && migrationQuery.isPending && (
+
+ Loading your
+ migration…
+
+ )}
+ {migrationQuery.isError && (
+
+ Could not load your migration. {migrationQuery.error.message}
+ migrationQuery.refetch()}
+ type="button"
+ >
+ Try again
+
+
+ )}
+
+ {activeRequest &&
}
+ {!activeRequest && completedRequest && (
+
+ )}
+
+ {activeRequest && !activeRequest.invitedAt && (
+
+
+
+
+
+ One step left: invite Cap to Loom
+
+
+ In your Loom workspace settings, invite{" "}
+ hello@cap.so with video access and download
+ permission. Some private Library videos need access shared
+ separately. If we find any, we’ll ask you here. You can keep
+ using Loom until we confirm the migration is complete.
+
+
+ Loom’s workspace invite steps
+
+
inviteMutation.mutate(activeRequest.id)}
+ size="sm"
+ variant="dark"
+ >
+ I sent the Loom invite
+
+
+
+
+ )}
+ {activeRequest?.invitedAt && activeRequest.status !== "completed" && (
+
+ Loom invite marked as sent. Cap will take it from here.
+
+ )}
+ {activeRequest?.status === "needs_information" && (
+
{
+ event.preventDefault();
+ answerMutation.mutate({ requestId: activeRequest.id, answer });
+ }}
+ >
+
+ Cap needs a little
+ more information
+
+
+ Your answer
+
+ setAnswer(event.target.value)}
+ value={answer}
+ />
+
+ Send answer
+
+
+ )}
+
+
+
+
Do it yourself
+
+ Best when you already have a list of Loom links and video owners.
+
+
+
+ Prepare a CSV with loom_video_url and user_email. space_name is
+ optional.
+
+
+ Upload up to 500 videos per CSV and review the owner mapping.
+
+
+ Check the import results and your Cap library before leaving Loom.
+
+
+ {user.isPro ? (
+
+ Open CSV importer
+
+ ) : (
+
+ The self-service importer requires your own Cap Pro account.
+
+ )}
+
+
+
+
+ Cap handles it for you
+
+
+ Free with Pro
+
+
+
+ Request the migration and invite hello@cap.so to Loom. We’ll
+ inventory, move, and verify your videos. No CSV preparation needed.
+ If Loom restricts a video, we’ll ask for the specific access we
+ need.
+
+ {!migrationQuery.isPending && !migrationQuery.isError && !isPro && (
+
+ Your organization owner needs Cap Pro to request this service.
+
+ View billing
+
+
+ )}
+ {isPro && !activeRequest && !requestFormOpen && (
+
setRequestFormOpen(true)}
+ size="sm"
+ variant="dark"
+ >
+ Enable concierge migration
+
+ )}
+ {isPro && !activeRequest && requestFormOpen && (
+
{
+ event.preventDefault();
+ requestMutation.mutate();
+ }}
+ >
+
+
+ Loom workspace name{" "}
+ (optional)
+
+ setWorkspaceName(event.target.value)}
+ value={workspaceName}
+ />
+
+
+
+ Anything we should know?{" "}
+ (optional)
+
+ setNote(event.target.value)}
+ value={note}
+ />
+
+
+ Request migration
+
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/app/(org)/dashboard/migrations/loom/page.tsx b/apps/web/app/(org)/dashboard/migrations/loom/page.tsx
new file mode 100644
index 00000000000..bb06782ed70
--- /dev/null
+++ b/apps/web/app/(org)/dashboard/migrations/loom/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from "next";
+import { LoomMigrationPage } from "./LoomMigrationPage";
+
+export const metadata: Metadata = {
+ title: "Loom Migration — Cap",
+};
+
+export default function Page() {
+ return ;
+}
diff --git a/apps/web/content/docs/migrating-to-cap.mdx b/apps/web/content/docs/migrating-to-cap.mdx
index 50a99dedfe4..dcc74ca840e 100644
--- a/apps/web/content/docs/migrating-to-cap.mdx
+++ b/apps/web/content/docs/migrating-to-cap.mdx
@@ -14,9 +14,21 @@ A successful migration is not just a count of accepted URLs. It preserves owners
|---------|------------------|
| One Loom video | Paste the Loom URL in **Dashboard > Import > Loom** |
| Up to 500 mapped videos | Use the organization CSV importer |
-| More than 500 videos or a complex hierarchy | Run controlled batches with an agent or contact [hello@cap.so](mailto:hello@cap.so) for a managed migration |
+| A library you want Cap to move for you | Request the free Cap Pro concierge migration in **Dashboard > Loom Migration** |
+| More than 500 videos or a complex hierarchy | Request concierge migration or run controlled batches with an agent |
-Loom import is a Cap Pro feature. Bulk CSV import is available to organization owners and admins.
+Loom import is a Cap Pro feature. Bulk CSV import is available to organization owners and admins. Concierge migration is included at no extra cost when the Cap organization owner has Pro.
+
+## Request concierge migration
+
+1. Open **Dashboard > Loom Migration** and select your Cap organization.
+2. Choose **Enable concierge migration**, then **Request migration**. The request enters Cap's queue as **Pending**.
+3. Invite [hello@cap.so](mailto:hello@cap.so) to your Loom workspace with access and download permission for the videos you want moved, then mark the invite as sent in Cap.
+4. Cap inventories the source, maps owners and spaces, imports in batches, and checks the destination. The dashboard shows **In progress** while this is underway.
+5. If a video needs separate access or another detail, the dashboard shows **Information needed** with Cap's exact request. Reply there so the migration can continue.
+6. Cap marks **Migration complete** only after the agreed source count and imported library are reconciled and checked. Keep Loom available until that confirmation.
+
+Loom workspace membership may not expose private Library videos automatically. An account without Loom download rights also cannot export every video. Cap will ask for specific access when the workspace invite does not cover a source video.
## Prepare the source inventory
diff --git a/apps/web/lib/loom-concierge.ts b/apps/web/lib/loom-concierge.ts
new file mode 100644
index 00000000000..a8f50bf2845
--- /dev/null
+++ b/apps/web/lib/loom-concierge.ts
@@ -0,0 +1,134 @@
+import "server-only";
+
+import { db } from "@cap/database";
+import { getCurrentUser } from "@cap/database/auth/session";
+import {
+ loomMigrationRequests,
+ organizations,
+ users,
+} from "@cap/database/schema";
+import { buildEnv } from "@cap/env";
+import type { Organisation } from "@cap/web-domain";
+import { and, asc, desc, eq, isNull, ne } from "drizzle-orm";
+import { requireOrganizationSettingsManager } from "@/actions/organization/authorization";
+import { MESSENGER_ADMIN_EMAIL } from "@/lib/messenger/constants";
+import { isOrganizationOwnerPro } from "@/lib/org-pro";
+import type { LoomMigrationStatus } from "./loom-migration-state";
+
+type MigrationRecord = typeof loomMigrationRequests.$inferSelect;
+
+export type LoomMigrationView = {
+ id: string;
+ organizationId: Organisation.OrganisationId;
+ status: LoomMigrationStatus;
+ workspaceName: string | null;
+ customerNote: string | null;
+ customerReply: string | null;
+ invitedAt: string | null;
+ capMessage: string | null;
+ expectedVideoCount: number | null;
+ importedVideoCount: number;
+ queuedVideoCount: number;
+ completedAt: string | null;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type OperatorLoomMigrationView = LoomMigrationView & {
+ organizationName: string;
+ requestedByEmail: string;
+};
+
+export function migrationToView(record: MigrationRecord): LoomMigrationView {
+ return {
+ id: record.id,
+ organizationId: record.organizationId,
+ status: record.status,
+ workspaceName: record.workspaceName,
+ customerNote: record.customerNote,
+ customerReply: record.customerReply,
+ invitedAt: record.invitedAt?.toISOString() ?? null,
+ capMessage: record.capMessage,
+ expectedVideoCount: record.expectedVideoCount,
+ importedVideoCount: record.importedVideoCount,
+ queuedVideoCount: record.queuedVideoCount,
+ completedAt: record.completedAt?.toISOString() ?? null,
+ createdAt: record.createdAt.toISOString(),
+ updatedAt: record.updatedAt.toISOString(),
+ };
+}
+
+export async function requireCustomerMigrationAccess(
+ organizationId: Organisation.OrganisationId,
+) {
+ const user = await getCurrentUser();
+ if (!user) throw new Error("Unauthorized");
+ await requireOrganizationSettingsManager(user.id, organizationId);
+ return user;
+}
+
+export async function requireProCustomerMigrationAccess(
+ organizationId: Organisation.OrganisationId,
+) {
+ const user = await requireCustomerMigrationAccess(organizationId);
+ if (!buildEnv.NEXT_PUBLIC_IS_CAP) {
+ throw new Error("Concierge migration is available on Cap Cloud.");
+ }
+ if (!(await isOrganizationOwnerPro(organizationId))) {
+ throw new Error("Concierge Loom migration requires Cap Pro.");
+ }
+ return user;
+}
+
+export async function requireMigrationOperator() {
+ const user = await getCurrentUser();
+ if (
+ !buildEnv.NEXT_PUBLIC_IS_CAP ||
+ !user ||
+ user.email.toLowerCase() !== MESSENGER_ADMIN_EMAIL
+ ) {
+ throw new Error("Unauthorized");
+ }
+ return user;
+}
+
+export async function getCustomerMigrationRequests(
+ organizationId: Organisation.OrganisationId,
+) {
+ await requireCustomerMigrationAccess(organizationId);
+ const requests = await db()
+ .select()
+ .from(loomMigrationRequests)
+ .where(eq(loomMigrationRequests.organizationId, organizationId))
+ .orderBy(desc(loomMigrationRequests.createdAt))
+ .limit(20);
+ return requests.map(migrationToView);
+}
+
+export async function getOperatorMigrationQueue() {
+ await requireMigrationOperator();
+ const queue = await db()
+ .select({
+ request: loomMigrationRequests,
+ organizationName: organizations.name,
+ requestedByEmail: users.email,
+ })
+ .from(loomMigrationRequests)
+ .innerJoin(
+ organizations,
+ eq(organizations.id, loomMigrationRequests.organizationId),
+ )
+ .innerJoin(users, eq(users.id, loomMigrationRequests.requestedByUserId))
+ .where(
+ and(
+ ne(loomMigrationRequests.status, "completed"),
+ isNull(organizations.tombstoneAt),
+ ),
+ )
+ .orderBy(asc(loomMigrationRequests.createdAt));
+ return queue.map((item) => ({
+ ...migrationToView(item.request),
+ organizationName: item.organizationName,
+ requestedByEmail: item.requestedByEmail,
+ }));
+}
diff --git a/apps/web/lib/loom-csv.ts b/apps/web/lib/loom-csv.ts
new file mode 100644
index 00000000000..871eef779a5
--- /dev/null
+++ b/apps/web/lib/loom-csv.ts
@@ -0,0 +1,72 @@
+import type { LoomCsvImportRow } from "@/actions/loom";
+
+export function parseLoomCsvRecords(text: string) {
+ const records: string[][] = [];
+ let field = "";
+ let row: string[] = [];
+ let inQuotes = false;
+ const input = text.replace(/^\uFEFF/, "");
+
+ for (let index = 0; index < input.length; index += 1) {
+ const char = input.charAt(index);
+ const next = input.charAt(index + 1);
+
+ if (char === '"') {
+ if (inQuotes && next === '"') {
+ field += '"';
+ index += 1;
+ } else {
+ inQuotes = !inQuotes;
+ }
+ continue;
+ }
+
+ if (char === "," && !inQuotes) {
+ row.push(field.trim());
+ field = "";
+ continue;
+ }
+
+ if ((char === "\n" || char === "\r") && !inQuotes) {
+ if (char === "\r" && next === "\n") index += 1;
+ row.push(field.trim());
+ if (row.some((cell) => cell.length > 0)) records.push(row);
+ row = [];
+ field = "";
+ continue;
+ }
+
+ field += char;
+ }
+
+ if (inQuotes) throw new Error("CSV has an unclosed quoted field.");
+
+ if (field.length > 0 || row.length > 0) {
+ row.push(field.trim());
+ if (row.some((cell) => cell.length > 0)) records.push(row);
+ }
+
+ return records;
+}
+
+export function parseConciergeLoomCsv(text: string): LoomCsvImportRow[] {
+ const records = parseLoomCsvRecords(text);
+ const headers = records[0]?.map((value) => value.toLowerCase()) ?? [];
+ const loomUrlIndex = headers.indexOf("loom_video_url");
+ const userEmailIndex = headers.indexOf("user_email");
+ const spaceNameIndex = headers.indexOf("space_name");
+ if (loomUrlIndex < 0 || userEmailIndex < 0) {
+ throw new Error("CSV needs loom_video_url and user_email columns.");
+ }
+ const rows = records.slice(1).map((values, index) => ({
+ rowNumber: index + 2,
+ loomUrl: values[loomUrlIndex] ?? "",
+ userEmail: values[userEmailIndex] ?? "",
+ spaceName: spaceNameIndex < 0 ? undefined : values[spaceNameIndex],
+ }));
+ if (rows.length === 0) throw new Error("CSV has no video rows.");
+ if (rows.length > 500) {
+ throw new Error("Split the library into CSV files of up to 500 videos.");
+ }
+ return rows;
+}
diff --git a/apps/web/lib/loom-migration-state.ts b/apps/web/lib/loom-migration-state.ts
new file mode 100644
index 00000000000..1e2791df990
--- /dev/null
+++ b/apps/web/lib/loom-migration-state.ts
@@ -0,0 +1,80 @@
+export const LOOM_MIGRATION_STATUSES = [
+ "pending",
+ "in_progress",
+ "needs_information",
+ "completed",
+] as const;
+
+export type LoomMigrationStatus = (typeof LOOM_MIGRATION_STATUSES)[number];
+
+export const LOOM_MIGRATION_STATUS_LABELS: Record =
+ {
+ pending: "Pending",
+ in_progress: "In progress",
+ needs_information: "Information needed",
+ completed: "Migration complete",
+ };
+
+export function normalizeMigrationText(
+ value: string,
+ label: string,
+ maxLength: number,
+) {
+ if (typeof value !== "string") throw new Error(`${label} is invalid.`);
+ const normalized = value.trim().replace(/\s+/g, " ");
+ if (normalized.length > maxLength) {
+ throw new Error(`${label} must be ${maxLength} characters or fewer.`);
+ }
+ return normalized;
+}
+
+export function validateOperatorMigrationUpdate({
+ currentStatus,
+ nextStatus,
+ message,
+ verified,
+ expectedVideoCount,
+ importedVideoCount,
+}: {
+ currentStatus: LoomMigrationStatus;
+ nextStatus: LoomMigrationStatus;
+ message: string;
+ verified: boolean;
+ expectedVideoCount: number | null;
+ importedVideoCount: number;
+}) {
+ if (currentStatus === "completed") {
+ throw new Error("A completed migration cannot be changed.");
+ }
+ if (!LOOM_MIGRATION_STATUSES.includes(nextStatus)) {
+ throw new Error("Invalid migration status.");
+ }
+ const normalizedMessage = normalizeMigrationText(message, "Message", 2000);
+ if (nextStatus === "needs_information" && !normalizedMessage) {
+ throw new Error("Tell the customer what information you need.");
+ }
+ if (nextStatus === "completed" && !verified) {
+ throw new Error("Verify the migrated library before marking it complete.");
+ }
+ if (nextStatus === "completed" && expectedVideoCount === null) {
+ throw new Error("Enter the agreed source video count before completion.");
+ }
+ if (
+ !Number.isInteger(importedVideoCount) ||
+ importedVideoCount < 0 ||
+ (expectedVideoCount !== null &&
+ (!Number.isInteger(expectedVideoCount) ||
+ expectedVideoCount < 0 ||
+ importedVideoCount > expectedVideoCount))
+ ) {
+ throw new Error("Enter valid video counts.");
+ }
+ if (
+ nextStatus === "completed" &&
+ expectedVideoCount !== null &&
+ importedVideoCount !== expectedVideoCount
+ ) {
+ throw new Error("Reconcile the expected video count before completion.");
+ }
+ return normalizedMessage;
+}
diff --git a/packages/database/emails/loom-migration.tsx b/packages/database/emails/loom-migration.tsx
new file mode 100644
index 00000000000..f65ffdfaaff
--- /dev/null
+++ b/packages/database/emails/loom-migration.tsx
@@ -0,0 +1,70 @@
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Link,
+ Preview,
+ Text,
+} from "@react-email/components";
+
+export function LoomMigrationRequestEmail({
+ organizationName,
+ requesterEmail,
+ workspaceName,
+ queueUrl,
+}: {
+ organizationName: string;
+ requesterEmail: string;
+ workspaceName: string | null;
+ queueUrl: string;
+}) {
+ return (
+
+
+ New Loom migration request from {organizationName}
+
+
+ New Loom migration request
+
+ {organizationName} requested a Cap Pro concierge migration.
+
+ Requested by: {requesterEmail}
+ {workspaceName && Loom workspace: {workspaceName} }
+ Open the migration queue
+
+
+
+ );
+}
+
+export function LoomMigrationStatusEmail({
+ organizationName,
+ statusLabel,
+ message,
+ dashboardUrl,
+}: {
+ organizationName: string;
+ statusLabel: string;
+ message: string | null;
+ dashboardUrl: string;
+}) {
+ return (
+
+
+ Your Loom migration is {statusLabel.toLowerCase()}
+
+
+ Loom migration update
+
+ Your migration for {organizationName} is {statusLabel.toLowerCase()}
+ .
+
+ {message && {message} }
+ View your migration
+
+
+
+ );
+}
diff --git a/packages/database/migrations/0046_loom_concierge.sql b/packages/database/migrations/0046_loom_concierge.sql
new file mode 100644
index 00000000000..778f5ef9a7e
--- /dev/null
+++ b/packages/database/migrations/0046_loom_concierge.sql
@@ -0,0 +1,25 @@
+CREATE TABLE `loom_migration_requests` (
+ `id` varchar(15) NOT NULL,
+ `organizationId` varchar(15) NOT NULL,
+ `activeOrganizationId` varchar(15),
+ `requestedByUserId` varchar(15) NOT NULL,
+ `workspaceName` varchar(255),
+ `customerNote` text,
+ `customerReply` text,
+ `invitedAt` datetime,
+ `status` varchar(32) NOT NULL DEFAULT 'pending',
+ `capMessage` text,
+ `expectedVideoCount` int,
+ `importedVideoCount` int NOT NULL DEFAULT 0,
+ `queuedVideoCount` int NOT NULL DEFAULT 0,
+ `lastOperatorUserId` varchar(15),
+ `lastOperatorAt` datetime,
+ `completedAt` datetime,
+ `createdAt` datetime NOT NULL,
+ `updatedAt` datetime NOT NULL,
+ CONSTRAINT `loom_migration_requests_id` PRIMARY KEY(`id`),
+ CONSTRAINT `loom_migration_active_org_idx` UNIQUE(`activeOrganizationId`)
+);
+--> statement-breakpoint
+CREATE INDEX `loom_migration_org_created_idx` ON `loom_migration_requests` (`organizationId`,`createdAt`);--> statement-breakpoint
+CREATE INDEX `loom_migration_status_created_idx` ON `loom_migration_requests` (`status`,`createdAt`);
\ No newline at end of file
diff --git a/packages/database/migrations/meta/0046_snapshot.json b/packages/database/migrations/meta/0046_snapshot.json
new file mode 100644
index 00000000000..26dad24dff2
--- /dev/null
+++ b/packages/database/migrations/meta/0046_snapshot.json
@@ -0,0 +1,4735 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "e73cfec4-89dd-4fc2-9068-4a86b6e10171",
+ "prevId": "0d80e4e5-6d93-4d28-b235-bf1231baa7a9",
+ "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
+ }
+ },
+ "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_ref_dedup_idx": {
+ "name": "account_ref_dedup_idx",
+ "columns": ["accountId", "referenceId", "referenceType"],
+ "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_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
+ },
+ "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": {
+ "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
+ }
+ },
+ "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": {}
+ },
+ "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_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
+ },
+ "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": {}
+ },
+ "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": {}
+ },
+ "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
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "notifications_id": {
+ "name": "notifications_id",
+ "columns": ["id"]
+ }
+ },
+ "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
+ },
+ "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
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "video_uploads_video_id": {
+ "name": "video_uploads_video_id",
+ "columns": ["video_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
+ },
+ "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_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 014bc42ce7d..d8e7a8780af 100644
--- a/packages/database/migrations/meta/_journal.json
+++ b/packages/database/migrations/meta/_journal.json
@@ -323,6 +323,13 @@
"when": 1789136779085,
"tag": "0045_steep_forge",
"breakpoints": true
+ },
+ {
+ "idx": 46,
+ "version": "5",
+ "when": 1789985586222,
+ "tag": "0046_loom_concierge",
+ "breakpoints": true
}
]
}
diff --git a/packages/database/schema.ts b/packages/database/schema.ts
index 7df0825ede2..d91b21bf257 100644
--- a/packages/database/schema.ts
+++ b/packages/database/schema.ts
@@ -1548,6 +1548,57 @@ export const importedVideos = mysqlTable(
],
);
+export const loomMigrationRequests = mysqlTable(
+ "loom_migration_requests",
+ {
+ id: nanoId("id").notNull().primaryKey(),
+ organizationId: nanoId("organizationId")
+ .notNull()
+ .$type(),
+ activeOrganizationId: nanoIdNullable(
+ "activeOrganizationId",
+ ).$type(),
+ requestedByUserId: nanoId("requestedByUserId")
+ .notNull()
+ .$type(),
+ workspaceName: varchar("workspaceName", { length: 255 }),
+ customerNote: text("customerNote"),
+ customerReply: text("customerReply"),
+ invitedAt: datetime("invitedAt", { mode: "date" }),
+ status: varchar("status", {
+ length: 32,
+ enum: ["pending", "in_progress", "needs_information", "completed"],
+ })
+ .notNull()
+ .default("pending"),
+ capMessage: text("capMessage"),
+ expectedVideoCount: int("expectedVideoCount"),
+ importedVideoCount: int("importedVideoCount").notNull().default(0),
+ queuedVideoCount: int("queuedVideoCount").notNull().default(0),
+ lastOperatorUserId:
+ nanoIdNullable("lastOperatorUserId").$type(),
+ lastOperatorAt: datetime("lastOperatorAt", { mode: "date" }),
+ completedAt: datetime("completedAt", { mode: "date" }),
+ createdAt: datetime("createdAt", { mode: "date" })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ updatedAt: datetime("updatedAt", { mode: "date" })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ },
+ (table) => [
+ uniqueIndex("loom_migration_active_org_idx").on(table.activeOrganizationId),
+ index("loom_migration_org_created_idx").on(
+ table.organizationId,
+ table.createdAt,
+ ),
+ index("loom_migration_status_created_idx").on(
+ table.status,
+ table.createdAt,
+ ),
+ ],
+);
+
export const developerApps = mysqlTable(
"developer_apps",
{
From 8df6730293da54bdfac1ed34f653a0b8d19e7997 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 21 Sep 2026 11:52:20 +0100
Subject: [PATCH 2/7] fix: register Loom migration emails
---
emails/CATALOG.md | 7 +++++++
emails/application.ts | 10 ++++++++++
2 files changed, 17 insertions(+)
diff --git a/emails/CATALOG.md b/emails/CATALOG.md
index ccc962d4bae..4b8bc43a5a3 100644
--- a/emails/CATALOG.md
+++ b/emails/CATALOG.md
@@ -618,6 +618,7 @@ These are repository send paths and retained templates, not confirmation of prod
| Messenger support notification | An eligible support conversation requests an email notification | Cap support; replies go to the user | [messenger-support-email](../packages/database/emails/messenger-support-email.tsx) |
| Account deletion request notification | An account deletion request is submitted | Cap support; replies go to the requester | [messenger-support-email](../packages/database/emails/messenger-support-email.tsx) |
| Mobile content report notification | A mobile content report is submitted | Cap support; replies go to the reporter | [messenger-support-email](../packages/database/emails/messenger-support-email.tsx) |
+| Concierge Loom migration | A Cap Pro organization requests migration or its status changes | Cap support for new requests; the requester for status updates | [loom-migration](../packages/database/emails/loom-migration.tsx) |
| Legacy login link template | No current send call found in this checkout | None configured | [login-link](../packages/database/emails/login-link.tsx) |
### Login verification code
@@ -692,6 +693,12 @@ Deduplicated by report ID.
Send source: [apps/web/lib/account-deletion-request.ts](../apps/web/lib/account-deletion-request.ts).
+### Concierge Loom migration
+
+The request stays in the dashboard even if email delivery fails.
+
+Send source: [apps/web/actions/loom-concierge.tsx](../apps/web/actions/loom-concierge.tsx).
+
### Legacy login link template
Retained source template; current email login uses verification codes.
diff --git a/emails/application.ts b/emails/application.ts
index fed7fbd3f43..60fe3d64287 100644
--- a/emails/application.ts
+++ b/emails/application.ts
@@ -131,6 +131,16 @@ export const applicationEmails: ApplicationEmail[] = [
template: "messenger-support-email",
sources: ["apps/web/lib/account-deletion-request.ts"],
},
+ {
+ id: "loom-migration",
+ name: "Concierge Loom migration",
+ trigger: "A Cap Pro organization requests migration or its status changes",
+ recipients:
+ "Cap support for new requests; the requester for status updates",
+ notes: "The request stays in the dashboard even if email delivery fails.",
+ template: "loom-migration",
+ sources: ["apps/web/actions/loom-concierge.tsx"],
+ },
{
id: "login-link",
name: "Legacy login link template",
From 1b6cb9dd439ffe378539b651f2c42b7c4a56bb8f Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 21 Sep 2026 12:01:09 +0100
Subject: [PATCH 3/7] fix: coordinate concierge import completion
---
apps/web/actions/loom-concierge.tsx | 20 +-
apps/web/actions/loom.ts | 545 +++++++++---------
.../loom-migrations/LoomMigrationQueue.tsx | 4 +-
apps/web/lib/loom-concierge.ts | 67 ++-
.../migrations/0046_loom_concierge.sql | 9 +
.../migrations/meta/0046_snapshot.json | 52 +-
.../database/migrations/meta/_journal.json | 2 +-
packages/database/schema.ts | 13 +
8 files changed, 434 insertions(+), 278 deletions(-)
diff --git a/apps/web/actions/loom-concierge.tsx b/apps/web/actions/loom-concierge.tsx
index b1a91d30f14..65835952a30 100644
--- a/apps/web/actions/loom-concierge.tsx
+++ b/apps/web/actions/loom-concierge.tsx
@@ -8,7 +8,7 @@ import {
} from "@cap/database/emails/loom-migration";
import { nanoId } from "@cap/database/helpers";
import {
- importedVideos,
+ loomMigrationImports,
loomMigrationRequests,
organizations,
users,
@@ -246,12 +246,14 @@ export async function updateLoomMigrationStatus({
if (nextStatus === "completed") {
const [unfinishedImport] = await db()
.select({ videoId: videoUploads.videoId })
- .from(importedVideos)
- .innerJoin(videoUploads, eq(videoUploads.videoId, importedVideos.id))
+ .from(loomMigrationImports)
+ .innerJoin(
+ videoUploads,
+ eq(videoUploads.videoId, loomMigrationImports.videoId),
+ )
.where(
and(
- eq(importedVideos.orgId, request.organizationId),
- eq(importedVideos.source, "loom"),
+ eq(loomMigrationImports.requestId, requestId),
inArray(videoUploads.phase, [
"uploading",
"processing",
@@ -290,10 +292,16 @@ export async function updateLoomMigrationStatus({
and(
eq(loomMigrationRequests.id, requestId),
eq(loomMigrationRequests.status, request.status),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ nextStatus === "completed"
+ ? eq(loomMigrationRequests.activeImportCount, 0)
+ : undefined,
),
);
if (affectedRows(result) === 0) {
- throw new Error("The migration request changed. Refresh and try again.");
+ throw new Error(
+ "The migration request changed or imports are starting. Refresh and try again.",
+ );
}
const [requester] = await db()
diff --git a/apps/web/actions/loom.ts b/apps/web/actions/loom.ts
index 7cb49e13fac..2083d85dc0d 100644
--- a/apps/web/actions/loom.ts
+++ b/apps/web/actions/loom.ts
@@ -7,6 +7,7 @@ import { nanoId } from "@cap/database/helpers";
import {
folders,
importedVideos,
+ loomMigrationImports,
loomMigrationRequests,
organizationMembers,
organizations,
@@ -39,7 +40,11 @@ import {
requireOrganizationSettingsManager,
} from "@/actions/organization/authorization";
import { requireSpaceManager } from "@/actions/organization/space-authorization";
-import { requireMigrationOperator } from "@/lib/loom-concierge";
+import {
+ releaseConciergeImport,
+ requireMigrationOperator,
+ reserveConciergeImport,
+} from "@/lib/loom-concierge";
import type { LoomImportDestination } from "@/lib/loom-import-destination";
import { isOrganizationOwnerPro } from "@/lib/org-pro";
import { provisionOrganizationInvitee } from "@/lib/organization-provisioning";
@@ -104,15 +109,6 @@ const LOOM_CSV_LIMIT_ERROR = `CSV imports are limited to ${MAX_LOOM_CSV_ROWS} ro
const LOOM_CSV_PERMISSION_ERROR =
"Only organization admins and owners can import Loom videos from a CSV.";
-function affectedRows(result: unknown) {
- if (Array.isArray(result)) {
- return (
- (result[0] as { affectedRows?: number } | undefined)?.affectedRows ?? 0
- );
- }
- return (result as { affectedRows?: number } | undefined)?.affectedRows ?? 0;
-}
-
function extractLoomVideoId(url: string): string | null {
try {
const parsed = new URL(url);
@@ -319,11 +315,13 @@ async function importLoomVideoForOwner({
orgId,
ownerId,
destination = {},
+ migrationRequestId,
}: {
loomUrl: string;
orgId: Organisation.OrganisationId;
ownerId: User.UserId;
destination?: LoomImportDestination;
+ migrationRequestId?: string;
}): Promise {
const loomVideoId = extractLoomVideoId(loomUrl.trim());
if (!loomVideoId) {
@@ -442,6 +440,12 @@ async function importLoomVideoForOwner({
source: "loom",
sourceId: loomVideoId,
});
+ if (migrationRequestId) {
+ await tx.insert(loomMigrationImports).values({
+ videoId,
+ requestId: migrationRequestId,
+ });
+ }
if (destination.spaceId === orgId) {
await tx.insert(sharedVideos).values({
@@ -796,10 +800,12 @@ async function processLoomCsvRows({
rows,
orgId,
actorId,
+ migrationRequestId,
}: {
rows: LoomCsvImportRow[];
orgId: Organisation.OrganisationId;
actorId: User.UserId;
+ migrationRequestId?: string;
}): Promise {
const inputRows = Array.isArray(rows) ? rows : [];
const normalizedRows = inputRows
@@ -909,6 +915,7 @@ async function processLoomCsvRows({
loomUrl: row.loomUrl,
orgId,
ownerId: member.userId,
+ migrationRequestId,
});
let spaceName = row.spaceName || undefined;
@@ -1012,52 +1019,31 @@ export async function importFromLoomCsvForConcierge({
if (!(await isOrganizationOwnerPro(request.organizationId))) {
throw new Error("The destination workspace needs Cap Pro.");
}
- const claimed = await db()
- .update(loomMigrationRequests)
- .set({
- status: "in_progress",
- lastOperatorUserId: operator.id,
- lastOperatorAt: new Date(),
- updatedAt: new Date(),
- })
- .where(
- and(
- eq(loomMigrationRequests.id, requestId),
- isNotNull(loomMigrationRequests.activeOrganizationId),
- ),
- );
- if (affectedRows(claimed) === 0) {
- throw new Error("The migration request changed. Refresh and try again.");
- }
- const result = await processLoomCsvRows({
- rows,
- orgId: request.organizationId,
- actorId: request.ownerId,
- });
- if (result.importedCount > 0) {
- const updated = await db()
- .update(loomMigrationRequests)
- .set({
- queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + ${result.importedCount}`,
- lastOperatorUserId: operator.id,
- lastOperatorAt: new Date(),
- updatedAt: new Date(),
- })
- .where(
- and(
- eq(loomMigrationRequests.id, requestId),
- isNotNull(loomMigrationRequests.activeOrganizationId),
- ),
- );
- if (affectedRows(updated) === 0) {
- throw new Error(
- "The migration request changed after jobs started. Review them before retrying.",
- );
+ await reserveConciergeImport(requestId, operator.id);
+ try {
+ const result = await processLoomCsvRows({
+ rows,
+ orgId: request.organizationId,
+ actorId: request.ownerId,
+ migrationRequestId: requestId,
+ });
+ if (result.importedCount > 0) {
+ await db()
+ .update(loomMigrationRequests)
+ .set({
+ queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + ${result.importedCount}`,
+ lastOperatorUserId: operator.id,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(eq(loomMigrationRequests.id, requestId));
}
+ revalidatePath("/dashboard/migrations/loom");
+ revalidatePath("/dashboard/admin/loom-migrations");
+ return result;
+ } finally {
+ await releaseConciergeImport(requestId);
}
- revalidatePath("/dashboard/migrations/loom");
- revalidatePath("/dashboard/admin/loom-migrations");
- return result;
}
export async function createConciergeLoomFileUpload({
@@ -1116,161 +1102,173 @@ export async function createConciergeLoomFileUpload({
if (!(await isOrganizationOwnerPro(request.organizationId))) {
throw new Error("The destination workspace needs Cap Pro.");
}
- const [existing] = await db()
- .select({
- id: importedVideos.id,
- ownerId: videos.ownerId,
- ownerEmail: users.email,
- bucket: videos.bucket,
- storageIntegrationId: videos.storageIntegrationId,
- rawFileKey: videoUploads.rawFileKey,
- phase: videoUploads.phase,
- processingMessage: videoUploads.processingMessage,
- })
- .from(importedVideos)
- .leftJoin(
- videos,
- and(
- eq(videos.id, importedVideos.id),
- eq(videos.orgId, importedVideos.orgId),
- ),
- )
- .leftJoin(users, eq(users.id, videos.ownerId))
- .leftJoin(videoUploads, eq(videoUploads.videoId, importedVideos.id))
- .where(
- and(
- eq(importedVideos.orgId, request.organizationId),
- eq(importedVideos.source, "loom"),
- eq(importedVideos.sourceId, loomVideoId),
- ),
- )
- .limit(1);
- if (existing) {
- if (
- !existing.ownerId ||
- !existing.ownerEmail ||
- normalizeImportEmail(existing.ownerEmail) !== normalizedEmail ||
- !existing.rawFileKey ||
- (existing.phase !== "error" &&
- (existing.phase !== "uploading" ||
- existing.processingMessage !== "Uploading Loom video..."))
- ) {
- throw new Error(
- "This Loom video is already in the Cap import inventory.",
- );
+ await reserveConciergeImport(requestId, operator.id);
+ try {
+ const [existing] = await db()
+ .select({
+ id: importedVideos.id,
+ ownerId: videos.ownerId,
+ ownerEmail: users.email,
+ bucket: videos.bucket,
+ storageIntegrationId: videos.storageIntegrationId,
+ rawFileKey: videoUploads.rawFileKey,
+ phase: videoUploads.phase,
+ processingMessage: videoUploads.processingMessage,
+ })
+ .from(importedVideos)
+ .leftJoin(
+ videos,
+ and(
+ eq(videos.id, importedVideos.id),
+ eq(videos.orgId, importedVideos.orgId),
+ ),
+ )
+ .leftJoin(users, eq(users.id, videos.ownerId))
+ .leftJoin(videoUploads, eq(videoUploads.videoId, importedVideos.id))
+ .where(
+ and(
+ eq(importedVideos.orgId, request.organizationId),
+ eq(importedVideos.source, "loom"),
+ eq(importedVideos.sourceId, loomVideoId),
+ ),
+ )
+ .limit(1);
+ if (existing) {
+ if (
+ !existing.ownerId ||
+ !existing.ownerEmail ||
+ normalizeImportEmail(existing.ownerEmail) !== normalizedEmail ||
+ !existing.rawFileKey ||
+ (existing.phase !== "error" &&
+ (existing.phase !== "uploading" ||
+ existing.processingMessage !== "Uploading Loom video..."))
+ ) {
+ throw new Error(
+ "This Loom video is already in the Cap import inventory.",
+ );
+ }
+ const retryUpload = await Storage.createUploadTargetForUser(
+ existing.ownerId,
+ existing.rawFileKey,
+ {
+ contentType: "video/mp4",
+ videoTitle: normalizedTitle,
+ method: "put",
+ fields: { "x-amz-meta-userid": existing.ownerId },
+ },
+ request.organizationId,
+ ).pipe(runPromise);
+ if (
+ Option.getOrNull(retryUpload.bucketId) !== existing.bucket ||
+ Option.getOrNull(retryUpload.storageIntegrationId) !==
+ existing.storageIntegrationId
+ ) {
+ throw new Error(
+ "The Cap storage destination changed. Review this upload before retrying.",
+ );
+ }
+ const [mapped] = await db()
+ .select({ requestId: loomMigrationImports.requestId })
+ .from(loomMigrationImports)
+ .where(
+ eq(loomMigrationImports.videoId, Video.VideoId.make(existing.id)),
+ )
+ .limit(1);
+ if (mapped && mapped.requestId !== requestId) {
+ throw new Error(
+ "This Loom import belongs to another migration request.",
+ );
+ }
+ if (!mapped) {
+ await db()
+ .insert(loomMigrationImports)
+ .values({
+ videoId: Video.VideoId.make(existing.id),
+ requestId,
+ });
+ }
+ return {
+ videoId: Video.VideoId.make(existing.id),
+ uploadTarget: retryUpload.upload,
+ };
+ }
+ let member = await getOrganizationMemberByEmail(
+ request.organizationId,
+ normalizedEmail,
+ );
+ if (!member) {
+ const provisioned = await provisionOrganizationInvitee({
+ organizationId: request.organizationId,
+ email: normalizedEmail,
+ invitedByUserId: request.ownerId,
+ role: "member",
+ });
+ member = { userId: provisioned.userId, email: normalizedEmail };
}
- const retryUpload = await Storage.createUploadTargetForUser(
- existing.ownerId,
- existing.rawFileKey,
+ const space = normalizedSpaceName
+ ? await getOrCreateImportSpace({
+ orgId: request.organizationId,
+ createdById: request.ownerId,
+ name: normalizedSpaceName,
+ spaceCache: new Map(),
+ })
+ : null;
+ if (space) {
+ await addImportOwnerToSpace({ spaceId: space.id, userId: member.userId });
+ }
+ const videoId = Video.VideoId.make(nanoId());
+ const rawFileKey = `${member.userId}/${videoId}/raw-upload.mp4`;
+ const upload = await Storage.createUploadTargetForUser(
+ member.userId,
+ rawFileKey,
{
contentType: "video/mp4",
videoTitle: normalizedTitle,
method: "put",
- fields: { "x-amz-meta-userid": existing.ownerId },
+ fields: { "x-amz-meta-userid": member.userId },
},
request.organizationId,
).pipe(runPromise);
- if (
- Option.getOrNull(retryUpload.bucketId) !== existing.bucket ||
- Option.getOrNull(retryUpload.storageIntegrationId) !==
- existing.storageIntegrationId
- ) {
- throw new Error(
- "The Cap storage destination changed. Review this upload before retrying.",
- );
- }
- return {
- videoId: Video.VideoId.make(existing.id),
- uploadTarget: retryUpload.upload,
- };
- }
- let member = await getOrganizationMemberByEmail(
- request.organizationId,
- normalizedEmail,
- );
- if (!member) {
- const provisioned = await provisionOrganizationInvitee({
- organizationId: request.organizationId,
- email: normalizedEmail,
- invitedByUserId: request.ownerId,
- role: "member",
- });
- member = { userId: provisioned.userId, email: normalizedEmail };
- }
- const space = normalizedSpaceName
- ? await getOrCreateImportSpace({
+ await db().transaction(async (tx) => {
+ await tx.insert(videos).values({
+ id: videoId,
+ name: normalizedTitle,
+ ownerId: member.userId,
orgId: request.organizationId,
- createdById: request.ownerId,
- name: normalizedSpaceName,
- spaceCache: new Map(),
- })
- : null;
- if (space) {
- await addImportOwnerToSpace({ spaceId: space.id, userId: member.userId });
- }
- const videoId = Video.VideoId.make(nanoId());
- const rawFileKey = `${member.userId}/${videoId}/raw-upload.mp4`;
- const upload = await Storage.createUploadTargetForUser(
- member.userId,
- rawFileKey,
- {
- contentType: "video/mp4",
- videoTitle: normalizedTitle,
- method: "put",
- fields: { "x-amz-meta-userid": member.userId },
- },
- request.organizationId,
- ).pipe(runPromise);
- await db().transaction(async (tx) => {
- await tx.insert(videos).values({
- id: videoId,
- name: normalizedTitle,
- ownerId: member.userId,
- orgId: request.organizationId,
- source: { type: "webMP4" as const },
- bucket: Option.getOrNull(upload.bucketId),
- storageIntegrationId: Option.getOrNull(upload.storageIntegrationId),
- public: serverEnv().CAP_VIDEOS_DEFAULT_PUBLIC,
- });
- await tx.insert(videoUploads).values({
- videoId,
- mode: "singlepart",
- phase: "uploading",
- processingProgress: 0,
- processingMessage: "Uploading Loom video...",
- rawFileKey,
- });
- await tx.insert(importedVideos).values({
- id: videoId,
- orgId: request.organizationId,
- source: "loom",
- sourceId: loomVideoId,
- });
- if (space) {
- await tx.insert(spaceVideos).values({
- id: nanoId(),
+ source: { type: "webMP4" as const },
+ bucket: Option.getOrNull(upload.bucketId),
+ storageIntegrationId: Option.getOrNull(upload.storageIntegrationId),
+ public: serverEnv().CAP_VIDEOS_DEFAULT_PUBLIC,
+ });
+ await tx.insert(videoUploads).values({
videoId,
- spaceId: space.id,
- addedById: request.ownerId,
+ mode: "singlepart",
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: "Uploading Loom video...",
+ rawFileKey,
});
- }
- });
- await db()
- .update(loomMigrationRequests)
- .set({
- status: "in_progress",
- lastOperatorUserId: operator.id,
- lastOperatorAt: new Date(),
- updatedAt: new Date(),
- })
- .where(
- and(
- eq(loomMigrationRequests.id, requestId),
- isNotNull(loomMigrationRequests.activeOrganizationId),
- ),
- );
- revalidatePath("/dashboard/admin/loom-migrations");
- return { videoId, uploadTarget: upload.upload };
+ await tx.insert(importedVideos).values({
+ id: videoId,
+ orgId: request.organizationId,
+ source: "loom",
+ sourceId: loomVideoId,
+ });
+ await tx.insert(loomMigrationImports).values({ videoId, requestId });
+ if (space) {
+ await tx.insert(spaceVideos).values({
+ id: nanoId(),
+ videoId,
+ spaceId: space.id,
+ addedById: request.ownerId,
+ });
+ }
+ });
+ revalidatePath("/dashboard/admin/loom-migrations");
+ return { videoId, uploadTarget: upload.upload };
+ } finally {
+ await releaseConciergeImport(requestId);
+ }
}
export async function finishConciergeLoomFileUpload({
@@ -1284,80 +1282,93 @@ export async function finishConciergeLoomFileUpload({
if (typeof requestId !== "string" || !/^[A-Za-z0-9_-]{15}$/.test(requestId)) {
throw new Error("Invalid migration request.");
}
- const [record] = await db()
- .select({
- video: videos,
- rawFileKey: videoUploads.rawFileKey,
- phase: videoUploads.phase,
- processingMessage: videoUploads.processingMessage,
- })
- .from(videos)
- .innerJoin(
- importedVideos,
- and(
- eq(importedVideos.id, videos.id),
- eq(importedVideos.orgId, videos.orgId),
- ),
- )
- .innerJoin(videoUploads, eq(videoUploads.videoId, videos.id))
- .innerJoin(
- loomMigrationRequests,
- eq(loomMigrationRequests.organizationId, importedVideos.orgId),
- )
- .where(
- and(
- eq(videos.id, videoId),
- eq(loomMigrationRequests.id, requestId),
- eq(importedVideos.source, "loom"),
- isNotNull(loomMigrationRequests.activeOrganizationId),
- ),
- )
- .limit(1);
- if (
- !record ||
- !record.rawFileKey ||
- (record.phase !== "error" &&
- (record.phase !== "uploading" ||
- record.processingMessage !== "Uploading Loom video..."))
- ) {
- throw new Error("This Loom upload is no longer available.");
- }
- const [bucket] = await Storage.getAccessForVideo(
- decodeStorageVideo(record.video),
- ).pipe(runPromise);
- const head = await bucket
- .headObject(record.rawFileKey)
- .pipe(
- Effect.retry({ times: 3, schedule: Schedule.exponential("100 millis") }),
- runPromise,
- );
- if ((head.ContentLength ?? 0) <= 0)
- throw new Error("The uploaded file is empty.");
- const status = await startVideoProcessingWorkflow({
- videoId,
- userId: record.video.ownerId,
- rawFileKey: record.rawFileKey,
- bucketId: record.video.bucket,
- processingMessage: "Processing Loom video...",
- startFailureMessage: "Loom file processing could not start.",
- });
- if (status === "started") {
- await db()
- .update(loomMigrationRequests)
- .set({
- queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + 1`,
- lastOperatorUserId: operator.id,
- lastOperatorAt: new Date(),
- updatedAt: new Date(),
+ await reserveConciergeImport(requestId, operator.id);
+ try {
+ const [record] = await db()
+ .select({
+ video: videos,
+ rawFileKey: videoUploads.rawFileKey,
+ phase: videoUploads.phase,
+ processingMessage: videoUploads.processingMessage,
})
+ .from(videos)
+ .innerJoin(
+ importedVideos,
+ and(
+ eq(importedVideos.id, videos.id),
+ eq(importedVideos.orgId, videos.orgId),
+ ),
+ )
+ .innerJoin(videoUploads, eq(videoUploads.videoId, videos.id))
+ .innerJoin(
+ loomMigrationImports,
+ eq(loomMigrationImports.videoId, videos.id),
+ )
+ .innerJoin(
+ loomMigrationRequests,
+ and(
+ eq(loomMigrationRequests.id, loomMigrationImports.requestId),
+ eq(loomMigrationRequests.organizationId, importedVideos.orgId),
+ ),
+ )
.where(
and(
+ eq(videos.id, videoId),
eq(loomMigrationRequests.id, requestId),
+ eq(importedVideos.source, "loom"),
isNotNull(loomMigrationRequests.activeOrganizationId),
),
- );
+ )
+ .limit(1);
+ if (
+ !record ||
+ !record.rawFileKey ||
+ (record.phase !== "error" &&
+ (record.phase !== "uploading" ||
+ record.processingMessage !== "Uploading Loom video..."))
+ ) {
+ throw new Error("This Loom upload is no longer available.");
+ }
+ const [bucket] = await Storage.getAccessForVideo(
+ decodeStorageVideo(record.video),
+ ).pipe(runPromise);
+ const head = await bucket.headObject(record.rawFileKey).pipe(
+ Effect.retry({
+ times: 3,
+ schedule: Schedule.exponential("100 millis"),
+ }),
+ runPromise,
+ );
+ if ((head.ContentLength ?? 0) <= 0)
+ throw new Error("The uploaded file is empty.");
+ const status = await startVideoProcessingWorkflow({
+ videoId,
+ userId: record.video.ownerId,
+ rawFileKey: record.rawFileKey,
+ bucketId: record.video.bucket,
+ processingMessage: "Processing Loom video...",
+ startFailureMessage: "Loom file processing could not start.",
+ });
+ if (status === "started") {
+ await db()
+ .update(loomMigrationRequests)
+ .set({
+ queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + 1`,
+ lastOperatorUserId: operator.id,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ }
+ revalidatePath("/dashboard/migrations/loom");
+ revalidatePath("/dashboard/admin/loom-migrations");
+ return { status };
+ } finally {
+ await releaseConciergeImport(requestId);
}
- revalidatePath("/dashboard/migrations/loom");
- revalidatePath("/dashboard/admin/loom-migrations");
- return { status };
}
diff --git a/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx b/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
index d256a9aded3..f1cf3806b42 100644
--- a/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
+++ b/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
@@ -195,6 +195,7 @@ function QueueItem({ request }: { request: OperatorLoomMigrationView }) {
Jobs started: {request.queuedVideoCount}
Verified videos: {request.importedVideoCount}
+ Imports starting: {request.activeImportCount}
{request.customerNote && (
@@ -467,7 +468,8 @@ function QueueItem({ request }: { request: OperatorLoomMigrationView }) {
disabled={
statusMutation.isPending ||
importMutation.isPending ||
- fileMutation.isPending
+ fileMutation.isPending ||
+ (nextStatus === "completed" && request.activeImportCount > 0)
}
size="sm"
type="submit"
diff --git a/apps/web/lib/loom-concierge.ts b/apps/web/lib/loom-concierge.ts
index a8f50bf2845..aed27433a37 100644
--- a/apps/web/lib/loom-concierge.ts
+++ b/apps/web/lib/loom-concierge.ts
@@ -8,8 +8,18 @@ import {
users,
} from "@cap/database/schema";
import { buildEnv } from "@cap/env";
-import type { Organisation } from "@cap/web-domain";
-import { and, asc, desc, eq, isNull, ne } from "drizzle-orm";
+import type { Organisation, User } from "@cap/web-domain";
+import {
+ and,
+ asc,
+ desc,
+ eq,
+ gt,
+ isNotNull,
+ isNull,
+ ne,
+ sql,
+} from "drizzle-orm";
import { requireOrganizationSettingsManager } from "@/actions/organization/authorization";
import { MESSENGER_ADMIN_EMAIL } from "@/lib/messenger/constants";
import { isOrganizationOwnerPro } from "@/lib/org-pro";
@@ -29,6 +39,7 @@ export type LoomMigrationView = {
expectedVideoCount: number | null;
importedVideoCount: number;
queuedVideoCount: number;
+ activeImportCount: number;
completedAt: string | null;
createdAt: string;
updatedAt: string;
@@ -52,6 +63,7 @@ export function migrationToView(record: MigrationRecord): LoomMigrationView {
expectedVideoCount: record.expectedVideoCount,
importedVideoCount: record.importedVideoCount,
queuedVideoCount: record.queuedVideoCount,
+ activeImportCount: record.activeImportCount,
completedAt: record.completedAt?.toISOString() ?? null,
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
@@ -92,6 +104,57 @@ export async function requireMigrationOperator() {
return user;
}
+function affectedRows(result: unknown) {
+ if (Array.isArray(result)) {
+ return (
+ (result[0] as { affectedRows?: number } | undefined)?.affectedRows ?? 0
+ );
+ }
+ return (result as { affectedRows?: number } | undefined)?.affectedRows ?? 0;
+}
+
+export async function reserveConciergeImport(
+ requestId: string,
+ operatorId: User.UserId,
+) {
+ const result = await db()
+ .update(loomMigrationRequests)
+ .set({
+ activeImportCount: sql`${loomMigrationRequests.activeImportCount} + 1`,
+ status: "in_progress",
+ lastOperatorUserId: operatorId,
+ lastOperatorAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ if (affectedRows(result) === 0) {
+ throw new Error("The migration request changed. Refresh and try again.");
+ }
+}
+
+export async function releaseConciergeImport(requestId: string) {
+ const result = await db()
+ .update(loomMigrationRequests)
+ .set({
+ activeImportCount: sql`${loomMigrationRequests.activeImportCount} - 1`,
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ gt(loomMigrationRequests.activeImportCount, 0),
+ ),
+ );
+ if (affectedRows(result) === 0) {
+ throw new Error("Could not release the migration import reservation.");
+ }
+}
+
export async function getCustomerMigrationRequests(
organizationId: Organisation.OrganisationId,
) {
diff --git a/packages/database/migrations/0046_loom_concierge.sql b/packages/database/migrations/0046_loom_concierge.sql
index 778f5ef9a7e..6e787e42967 100644
--- a/packages/database/migrations/0046_loom_concierge.sql
+++ b/packages/database/migrations/0046_loom_concierge.sql
@@ -1,3 +1,10 @@
+CREATE TABLE `loom_migration_imports` (
+ `videoId` varchar(15) NOT NULL,
+ `requestId` varchar(15) NOT NULL,
+ `createdAt` datetime NOT NULL,
+ CONSTRAINT `loom_migration_imports_videoId` PRIMARY KEY(`videoId`)
+);
+--> statement-breakpoint
CREATE TABLE `loom_migration_requests` (
`id` varchar(15) NOT NULL,
`organizationId` varchar(15) NOT NULL,
@@ -12,6 +19,7 @@ CREATE TABLE `loom_migration_requests` (
`expectedVideoCount` int,
`importedVideoCount` int NOT NULL DEFAULT 0,
`queuedVideoCount` int NOT NULL DEFAULT 0,
+ `activeImportCount` int NOT NULL DEFAULT 0,
`lastOperatorUserId` varchar(15),
`lastOperatorAt` datetime,
`completedAt` datetime,
@@ -21,5 +29,6 @@ CREATE TABLE `loom_migration_requests` (
CONSTRAINT `loom_migration_active_org_idx` UNIQUE(`activeOrganizationId`)
);
--> statement-breakpoint
+CREATE INDEX `loom_migration_imports_request_idx` ON `loom_migration_imports` (`requestId`);--> statement-breakpoint
CREATE INDEX `loom_migration_org_created_idx` ON `loom_migration_requests` (`organizationId`,`createdAt`);--> statement-breakpoint
CREATE INDEX `loom_migration_status_created_idx` ON `loom_migration_requests` (`status`,`createdAt`);
\ No newline at end of file
diff --git a/packages/database/migrations/meta/0046_snapshot.json b/packages/database/migrations/meta/0046_snapshot.json
index 26dad24dff2..20273a73004 100644
--- a/packages/database/migrations/meta/0046_snapshot.json
+++ b/packages/database/migrations/meta/0046_snapshot.json
@@ -1,7 +1,7 @@
{
"version": "5",
"dialect": "mysql",
- "id": "e73cfec4-89dd-4fc2-9068-4a86b6e10171",
+ "id": "d78f7c74-5007-4c78-96c3-50bf45012ad1",
"prevId": "0d80e4e5-6d93-4d28-b235-bf1231baa7a9",
"tables": {
"accounts": {
@@ -1694,6 +1694,48 @@
"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": {
@@ -1791,6 +1833,14 @@
"autoincrement": false,
"default": 0
},
+ "activeImportCount": {
+ "name": "activeImportCount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
"lastOperatorUserId": {
"name": "lastOperatorUserId",
"type": "varchar(15)",
diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json
index d8e7a8780af..49b1b749d03 100644
--- a/packages/database/migrations/meta/_journal.json
+++ b/packages/database/migrations/meta/_journal.json
@@ -327,7 +327,7 @@
{
"idx": 46,
"version": "5",
- "when": 1789985586222,
+ "when": 1789988285195,
"tag": "0046_loom_concierge",
"breakpoints": true
}
diff --git a/packages/database/schema.ts b/packages/database/schema.ts
index d91b21bf257..7f6b03c5c82 100644
--- a/packages/database/schema.ts
+++ b/packages/database/schema.ts
@@ -1575,6 +1575,7 @@ export const loomMigrationRequests = mysqlTable(
expectedVideoCount: int("expectedVideoCount"),
importedVideoCount: int("importedVideoCount").notNull().default(0),
queuedVideoCount: int("queuedVideoCount").notNull().default(0),
+ activeImportCount: int("activeImportCount").notNull().default(0),
lastOperatorUserId:
nanoIdNullable("lastOperatorUserId").$type
(),
lastOperatorAt: datetime("lastOperatorAt", { mode: "date" }),
@@ -1599,6 +1600,18 @@ export const loomMigrationRequests = mysqlTable(
],
);
+export const loomMigrationImports = mysqlTable(
+ "loom_migration_imports",
+ {
+ videoId: nanoId("videoId").notNull().primaryKey().$type(),
+ requestId: nanoId("requestId").notNull(),
+ createdAt: datetime("createdAt", { mode: "date" })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ },
+ (table) => [index("loom_migration_imports_request_idx").on(table.requestId)],
+);
+
export const developerApps = mysqlTable(
"developer_apps",
{
From e9fb8511a280bd928ef7391624b8fe29608ddd01 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 21 Sep 2026 12:07:46 +0100
Subject: [PATCH 4/7] fix: cover Loom concierge link in importer fixture
---
apps/web/__tests__/unit/loom-import-ui.test.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/apps/web/__tests__/unit/loom-import-ui.test.ts b/apps/web/__tests__/unit/loom-import-ui.test.ts
index d92f8e90913..d228ec466cd 100644
--- a/apps/web/__tests__/unit/loom-import-ui.test.ts
+++ b/apps/web/__tests__/unit/loom-import-ui.test.ts
@@ -96,6 +96,9 @@ vi.mock("next/link", () => ({
}: React.PropsWithChildren>) =>
React.createElement("a", props, children),
}));
+vi.mock("@cap/env", () => ({
+ buildEnv: { NEXT_PUBLIC_IS_CAP: "true" },
+}));
vi.mock("@/actions/loom", () => ({
getLoomImportFolders: mocks.folders,
importFromLoom: mocks.import,
@@ -186,6 +189,10 @@ describe("Loom importer component", () => {
it("shows the inherited subfolder and submits it then returns there", async () => {
await render({ folderId: Folder.FolderId.make("child") });
await ready();
+ expect(
+ container.querySelector('a[href="/dashboard/migrations/loom"]')
+ ?.textContent,
+ ).toContain("Want Cap to move your whole Loom workspace for you?");
expect(
getByRole(container, "combobox", { name: "Import to" }).textContent,
).toContain("My Caps / Course / Live Calls - Two");
From 52605264f5df12208a7b670e8dce4f6dc5e9360b Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 21 Sep 2026 12:16:23 +0100
Subject: [PATCH 5/7] fix: recover interrupted Loom concierge imports
---
apps/web/__tests__/unit/loom-csv.test.ts | 4 +-
.../web/__tests__/unit/loom-import-ui.test.ts | 5 +-
apps/web/actions/loom-concierge.tsx | 10 +-
apps/web/actions/loom.ts | 90 +-
apps/web/lib/loom-concierge.ts | 44 +-
apps/web/lib/loom-csv.ts | 7 +-
.../migrations/0047_loom_concierge_lease.sql | 2 +
.../migrations/meta/0047_snapshot.json | 4799 +++++++++++++++++
.../database/migrations/meta/_journal.json | 7 +
packages/database/schema.ts | 4 +
10 files changed, 4940 insertions(+), 32 deletions(-)
create mode 100644 packages/database/migrations/0047_loom_concierge_lease.sql
create mode 100644 packages/database/migrations/meta/0047_snapshot.json
diff --git a/apps/web/__tests__/unit/loom-csv.test.ts b/apps/web/__tests__/unit/loom-csv.test.ts
index 5338285db28..90d8db8f19e 100644
--- a/apps/web/__tests__/unit/loom-csv.test.ts
+++ b/apps/web/__tests__/unit/loom-csv.test.ts
@@ -5,7 +5,9 @@ describe("concierge Loom CSV", () => {
it("keeps quoted workspace names and source row numbers", () => {
const rows = parseConciergeLoomCsv(
"\uFEFFuser_email,space_name,loom_video_url\r\n" +
- 'owner@example.com,"Sales, Europe",https://www.loom.com/share/0123456789abcdef\r\n',
+ "owner@example.com," +
+ JSON.stringify("Sales, Europe") +
+ ",https://www.loom.com/share/0123456789abcdef\r\n",
);
expect(rows).toEqual([
{
diff --git a/apps/web/__tests__/unit/loom-import-ui.test.ts b/apps/web/__tests__/unit/loom-import-ui.test.ts
index d228ec466cd..07fd55309d5 100644
--- a/apps/web/__tests__/unit/loom-import-ui.test.ts
+++ b/apps/web/__tests__/unit/loom-import-ui.test.ts
@@ -190,8 +190,9 @@ describe("Loom importer component", () => {
await render({ folderId: Folder.FolderId.make("child") });
await ready();
expect(
- container.querySelector('a[href="/dashboard/migrations/loom"]')
- ?.textContent,
+ Array.from(container.querySelectorAll("a")).find(
+ (link) => link.getAttribute("href") === "/dashboard/migrations/loom",
+ )?.textContent,
).toContain("Want Cap to move your whole Loom workspace for you?");
expect(
getByRole(container, "combobox", { name: "Import to" }).textContent,
diff --git a/apps/web/actions/loom-concierge.tsx b/apps/web/actions/loom-concierge.tsx
index 65835952a30..007584ce806 100644
--- a/apps/web/actions/loom-concierge.tsx
+++ b/apps/web/actions/loom-concierge.tsx
@@ -16,7 +16,7 @@ import {
} from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import type { Organisation } from "@cap/web-domain";
-import { and, eq, inArray, isNotNull } from "drizzle-orm";
+import { and, eq, inArray, isNotNull, lte, or } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import {
getCustomerMigrationRequests,
@@ -281,6 +281,9 @@ export async function updateLoomMigrationStatus({
capMessage: normalizedMessage || null,
expectedVideoCount,
importedVideoCount,
+ activeImportCount: nextStatus === "completed" ? 0 : undefined,
+ activeImportLeaseToken: nextStatus === "completed" ? null : undefined,
+ activeImportLeaseUntil: nextStatus === "completed" ? null : undefined,
activeOrganizationId:
nextStatus === "completed" ? null : request.organizationId,
completedAt: nextStatus === "completed" ? new Date() : null,
@@ -294,7 +297,10 @@ export async function updateLoomMigrationStatus({
eq(loomMigrationRequests.status, request.status),
isNotNull(loomMigrationRequests.activeOrganizationId),
nextStatus === "completed"
- ? eq(loomMigrationRequests.activeImportCount, 0)
+ ? or(
+ eq(loomMigrationRequests.activeImportCount, 0),
+ lte(loomMigrationRequests.activeImportLeaseUntil, new Date()),
+ )
: undefined,
),
);
diff --git a/apps/web/actions/loom.ts b/apps/web/actions/loom.ts
index 2083d85dc0d..b85a1f0db00 100644
--- a/apps/web/actions/loom.ts
+++ b/apps/web/actions/loom.ts
@@ -316,12 +316,14 @@ async function importLoomVideoForOwner({
ownerId,
destination = {},
migrationRequestId,
+ migrationLeaseToken,
}: {
loomUrl: string;
orgId: Organisation.OrganisationId;
ownerId: User.UserId;
destination?: LoomImportDestination;
migrationRequestId?: string;
+ migrationLeaseToken?: string;
}): Promise {
const loomVideoId = extractLoomVideoId(loomUrl.trim());
if (!loomVideoId) {
@@ -412,6 +414,27 @@ async function importLoomVideoForOwner({
`Loom Import - ${new Date().toLocaleDateString("en-US", { day: "numeric", month: "long", year: "numeric" })}`;
await db().transaction(async (tx) => {
+ if (migrationRequestId) {
+ const [reservation] = await tx
+ .select({
+ activeOrganizationId: loomMigrationRequests.activeOrganizationId,
+ leaseToken: loomMigrationRequests.activeImportLeaseToken,
+ leaseUntil: loomMigrationRequests.activeImportLeaseUntil,
+ })
+ .from(loomMigrationRequests)
+ .where(eq(loomMigrationRequests.id, migrationRequestId))
+ .limit(1)
+ .for("update");
+ if (
+ !migrationLeaseToken ||
+ !reservation?.activeOrganizationId ||
+ reservation.leaseToken !== migrationLeaseToken ||
+ !reservation.leaseUntil ||
+ reservation.leaseUntil <= new Date()
+ ) {
+ throw new Error("The concierge import expired. Retry this video.");
+ }
+ }
await tx.insert(videos).values({
id: videoId,
name,
@@ -801,11 +824,13 @@ async function processLoomCsvRows({
orgId,
actorId,
migrationRequestId,
+ migrationLeaseToken,
}: {
rows: LoomCsvImportRow[];
orgId: Organisation.OrganisationId;
actorId: User.UserId;
migrationRequestId?: string;
+ migrationLeaseToken?: string;
}): Promise {
const inputRows = Array.isArray(rows) ? rows : [];
const normalizedRows = inputRows
@@ -916,6 +941,7 @@ async function processLoomCsvRows({
orgId,
ownerId: member.userId,
migrationRequestId,
+ migrationLeaseToken,
});
let spaceName = row.spaceName || undefined;
@@ -1019,13 +1045,14 @@ export async function importFromLoomCsvForConcierge({
if (!(await isOrganizationOwnerPro(request.organizationId))) {
throw new Error("The destination workspace needs Cap Pro.");
}
- await reserveConciergeImport(requestId, operator.id);
+ const leaseToken = await reserveConciergeImport(requestId, operator.id);
try {
const result = await processLoomCsvRows({
rows,
orgId: request.organizationId,
actorId: request.ownerId,
migrationRequestId: requestId,
+ migrationLeaseToken: leaseToken,
});
if (result.importedCount > 0) {
await db()
@@ -1036,13 +1063,19 @@ export async function importFromLoomCsvForConcierge({
lastOperatorAt: new Date(),
updatedAt: new Date(),
})
- .where(eq(loomMigrationRequests.id, requestId));
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ eq(loomMigrationRequests.activeImportLeaseToken, leaseToken),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
}
revalidatePath("/dashboard/migrations/loom");
revalidatePath("/dashboard/admin/loom-migrations");
return result;
} finally {
- await releaseConciergeImport(requestId);
+ await releaseConciergeImport(requestId, leaseToken);
}
}
@@ -1102,7 +1135,7 @@ export async function createConciergeLoomFileUpload({
if (!(await isOrganizationOwnerPro(request.organizationId))) {
throw new Error("The destination workspace needs Cap Pro.");
}
- await reserveConciergeImport(requestId, operator.id);
+ const leaseToken = await reserveConciergeImport(requestId, operator.id);
try {
const [existing] = await db()
.select({
@@ -1180,12 +1213,30 @@ export async function createConciergeLoomFileUpload({
);
}
if (!mapped) {
- await db()
- .insert(loomMigrationImports)
- .values({
+ await db().transaction(async (tx) => {
+ const [reservation] = await tx
+ .select({
+ activeOrganizationId: loomMigrationRequests.activeOrganizationId,
+ leaseToken: loomMigrationRequests.activeImportLeaseToken,
+ leaseUntil: loomMigrationRequests.activeImportLeaseUntil,
+ })
+ .from(loomMigrationRequests)
+ .where(eq(loomMigrationRequests.id, requestId))
+ .limit(1)
+ .for("update");
+ if (
+ !reservation?.activeOrganizationId ||
+ reservation.leaseToken !== leaseToken ||
+ !reservation.leaseUntil ||
+ reservation.leaseUntil <= new Date()
+ ) {
+ throw new Error("The concierge import expired. Retry this video.");
+ }
+ await tx.insert(loomMigrationImports).values({
videoId: Video.VideoId.make(existing.id),
requestId,
});
+ });
}
return {
videoId: Video.VideoId.make(existing.id),
@@ -1230,6 +1281,24 @@ export async function createConciergeLoomFileUpload({
request.organizationId,
).pipe(runPromise);
await db().transaction(async (tx) => {
+ const [reservation] = await tx
+ .select({
+ activeOrganizationId: loomMigrationRequests.activeOrganizationId,
+ leaseToken: loomMigrationRequests.activeImportLeaseToken,
+ leaseUntil: loomMigrationRequests.activeImportLeaseUntil,
+ })
+ .from(loomMigrationRequests)
+ .where(eq(loomMigrationRequests.id, requestId))
+ .limit(1)
+ .for("update");
+ if (
+ !reservation?.activeOrganizationId ||
+ reservation.leaseToken !== leaseToken ||
+ !reservation.leaseUntil ||
+ reservation.leaseUntil <= new Date()
+ ) {
+ throw new Error("The concierge import expired. Retry this video.");
+ }
await tx.insert(videos).values({
id: videoId,
name: normalizedTitle,
@@ -1267,7 +1336,7 @@ export async function createConciergeLoomFileUpload({
revalidatePath("/dashboard/admin/loom-migrations");
return { videoId, uploadTarget: upload.upload };
} finally {
- await releaseConciergeImport(requestId);
+ await releaseConciergeImport(requestId, leaseToken);
}
}
@@ -1282,7 +1351,7 @@ export async function finishConciergeLoomFileUpload({
if (typeof requestId !== "string" || !/^[A-Za-z0-9_-]{15}$/.test(requestId)) {
throw new Error("Invalid migration request.");
}
- await reserveConciergeImport(requestId, operator.id);
+ const leaseToken = await reserveConciergeImport(requestId, operator.id);
try {
const [record] = await db()
.select({
@@ -1361,6 +1430,7 @@ export async function finishConciergeLoomFileUpload({
.where(
and(
eq(loomMigrationRequests.id, requestId),
+ eq(loomMigrationRequests.activeImportLeaseToken, leaseToken),
isNotNull(loomMigrationRequests.activeOrganizationId),
),
);
@@ -1369,6 +1439,6 @@ export async function finishConciergeLoomFileUpload({
revalidatePath("/dashboard/admin/loom-migrations");
return { status };
} finally {
- await releaseConciergeImport(requestId);
+ await releaseConciergeImport(requestId, leaseToken);
}
}
diff --git a/apps/web/lib/loom-concierge.ts b/apps/web/lib/loom-concierge.ts
index aed27433a37..0a9a01e5127 100644
--- a/apps/web/lib/loom-concierge.ts
+++ b/apps/web/lib/loom-concierge.ts
@@ -1,5 +1,6 @@
import "server-only";
+import { randomUUID } from "node:crypto";
import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import {
@@ -14,11 +15,11 @@ import {
asc,
desc,
eq,
- gt,
isNotNull,
isNull,
+ lte,
ne,
- sql,
+ or,
} from "drizzle-orm";
import { requireOrganizationSettingsManager } from "@/actions/organization/authorization";
import { MESSENGER_ADMIN_EMAIL } from "@/lib/messenger/constants";
@@ -26,6 +27,7 @@ import { isOrganizationOwnerPro } from "@/lib/org-pro";
import type { LoomMigrationStatus } from "./loom-migration-state";
type MigrationRecord = typeof loomMigrationRequests.$inferSelect;
+const IMPORT_LEASE_MS = 60 * 60 * 1000;
export type LoomMigrationView = {
id: string;
@@ -63,7 +65,11 @@ export function migrationToView(record: MigrationRecord): LoomMigrationView {
expectedVideoCount: record.expectedVideoCount,
importedVideoCount: record.importedVideoCount,
queuedVideoCount: record.queuedVideoCount,
- activeImportCount: record.activeImportCount,
+ activeImportCount:
+ record.activeImportLeaseUntil &&
+ record.activeImportLeaseUntil.getTime() <= Date.now()
+ ? 0
+ : record.activeImportCount,
completedAt: record.completedAt?.toISOString() ?? null,
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
@@ -117,42 +123,52 @@ export async function reserveConciergeImport(
requestId: string,
operatorId: User.UserId,
) {
+ const token = randomUUID();
+ const now = new Date();
const result = await db()
.update(loomMigrationRequests)
.set({
- activeImportCount: sql`${loomMigrationRequests.activeImportCount} + 1`,
+ activeImportCount: 1,
+ activeImportLeaseToken: token,
+ activeImportLeaseUntil: new Date(now.getTime() + IMPORT_LEASE_MS),
status: "in_progress",
lastOperatorUserId: operatorId,
- lastOperatorAt: new Date(),
- updatedAt: new Date(),
+ lastOperatorAt: now,
+ updatedAt: now,
})
.where(
and(
eq(loomMigrationRequests.id, requestId),
isNotNull(loomMigrationRequests.activeOrganizationId),
+ or(
+ eq(loomMigrationRequests.activeImportCount, 0),
+ lte(loomMigrationRequests.activeImportLeaseUntil, now),
+ ),
),
);
if (affectedRows(result) === 0) {
- throw new Error("The migration request changed. Refresh and try again.");
+ throw new Error(
+ "The migration request changed or another import is starting. Refresh and try again.",
+ );
}
+ return token;
}
-export async function releaseConciergeImport(requestId: string) {
- const result = await db()
+export async function releaseConciergeImport(requestId: string, token: string) {
+ await db()
.update(loomMigrationRequests)
.set({
- activeImportCount: sql`${loomMigrationRequests.activeImportCount} - 1`,
+ activeImportCount: 0,
+ activeImportLeaseToken: null,
+ activeImportLeaseUntil: null,
updatedAt: new Date(),
})
.where(
and(
eq(loomMigrationRequests.id, requestId),
- gt(loomMigrationRequests.activeImportCount, 0),
+ eq(loomMigrationRequests.activeImportLeaseToken, token),
),
);
- if (affectedRows(result) === 0) {
- throw new Error("Could not release the migration import reservation.");
- }
}
export async function getCustomerMigrationRequests(
diff --git a/apps/web/lib/loom-csv.ts b/apps/web/lib/loom-csv.ts
index 871eef779a5..3aaea1f1968 100644
--- a/apps/web/lib/loom-csv.ts
+++ b/apps/web/lib/loom-csv.ts
@@ -6,14 +6,15 @@ export function parseLoomCsvRecords(text: string) {
let row: string[] = [];
let inQuotes = false;
const input = text.replace(/^\uFEFF/, "");
+ const quote = "\u0022";
for (let index = 0; index < input.length; index += 1) {
const char = input.charAt(index);
const next = input.charAt(index + 1);
- if (char === '"') {
- if (inQuotes && next === '"') {
- field += '"';
+ if (char === quote) {
+ if (inQuotes && next === quote) {
+ field += quote;
index += 1;
} else {
inQuotes = !inQuotes;
diff --git a/packages/database/migrations/0047_loom_concierge_lease.sql b/packages/database/migrations/0047_loom_concierge_lease.sql
new file mode 100644
index 00000000000..982766242b9
--- /dev/null
+++ b/packages/database/migrations/0047_loom_concierge_lease.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `loom_migration_requests` ADD `activeImportLeaseToken` varchar(36);--> statement-breakpoint
+ALTER TABLE `loom_migration_requests` ADD `activeImportLeaseUntil` datetime;
\ No newline at end of file
diff --git a/packages/database/migrations/meta/0047_snapshot.json b/packages/database/migrations/meta/0047_snapshot.json
new file mode 100644
index 00000000000..2695577b69b
--- /dev/null
+++ b/packages/database/migrations/meta/0047_snapshot.json
@@ -0,0 +1,4799 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "1cd109bd-1e00-49c3-bbf3-97f0025e557c",
+ "prevId": "d78f7c74-5007-4c78-96c3-50bf45012ad1",
+ "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
+ }
+ },
+ "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_ref_dedup_idx": {
+ "name": "account_ref_dedup_idx",
+ "columns": ["accountId", "referenceId", "referenceType"],
+ "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_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
+ },
+ "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": {
+ "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
+ }
+ },
+ "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": {}
+ },
+ "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": {}
+ },
+ "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": {}
+ },
+ "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
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "notifications_id": {
+ "name": "notifications_id",
+ "columns": ["id"]
+ }
+ },
+ "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
+ },
+ "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
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "video_uploads_video_id": {
+ "name": "video_uploads_video_id",
+ "columns": ["video_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
+ },
+ "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_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 49b1b749d03..5cfeb35486d 100644
--- a/packages/database/migrations/meta/_journal.json
+++ b/packages/database/migrations/meta/_journal.json
@@ -330,6 +330,13 @@
"when": 1789988285195,
"tag": "0046_loom_concierge",
"breakpoints": true
+ },
+ {
+ "idx": 47,
+ "version": "5",
+ "when": 1789989245680,
+ "tag": "0047_loom_concierge_lease",
+ "breakpoints": true
}
]
}
diff --git a/packages/database/schema.ts b/packages/database/schema.ts
index 7f6b03c5c82..1843316f436 100644
--- a/packages/database/schema.ts
+++ b/packages/database/schema.ts
@@ -1576,6 +1576,10 @@ export const loomMigrationRequests = mysqlTable(
importedVideoCount: int("importedVideoCount").notNull().default(0),
queuedVideoCount: int("queuedVideoCount").notNull().default(0),
activeImportCount: int("activeImportCount").notNull().default(0),
+ activeImportLeaseToken: varchar("activeImportLeaseToken", { length: 36 }),
+ activeImportLeaseUntil: datetime("activeImportLeaseUntil", {
+ mode: "date",
+ }),
lastOperatorUserId:
nanoIdNullable("lastOperatorUserId").$type(),
lastOperatorAt: datetime("lastOperatorAt", { mode: "date" }),
From 0f4fe25ffc2ce34594656d745f0c23a66ebb2349 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 21 Sep 2026 12:19:27 +0100
Subject: [PATCH 6/7] fix: validate Loom video IDs in server requests
---
apps/web/actions/loom.ts | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/apps/web/actions/loom.ts b/apps/web/actions/loom.ts
index b85a1f0db00..120aa93490b 100644
--- a/apps/web/actions/loom.ts
+++ b/apps/web/actions/loom.ts
@@ -108,6 +108,7 @@ const MAX_LOOM_SPACE_NAME_LENGTH = 255;
const LOOM_CSV_LIMIT_ERROR = `CSV imports are limited to ${MAX_LOOM_CSV_ROWS} rows at a time. Contact support to raise this limit.`;
const LOOM_CSV_PERMISSION_ERROR =
"Only organization admins and owners can import Loom videos from a CSV.";
+const LOOM_VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{10,64}$/;
function extractLoomVideoId(url: string): string | null {
try {
@@ -122,11 +123,11 @@ function extractLoomVideoId(url: string): string | null {
const pathParts = parsed.pathname.split("/").filter(Boolean);
const id = pathParts[pathParts.length - 1] ?? null;
- if (!id || id.length < 10) {
+ if (!id || !LOOM_VIDEO_ID_PATTERN.test(id)) {
return null;
}
- return id.split("?")[0] ?? null;
+ return id;
} catch {
return null;
}
@@ -134,7 +135,7 @@ function extractLoomVideoId(url: string): string | null {
async function fetchLoomEndpoint(
videoId: string,
- endpoint: string,
+ endpoint: "transcoded-url" | "raw-url",
includeBody = true,
): Promise {
try {
@@ -153,7 +154,7 @@ async function fetchLoomEndpoint(
}
const response = await fetch(
- `https://www.loom.com/api/campaigns/sessions/${videoId}/${endpoint}`,
+ `https://www.loom.com/api/campaigns/sessions/${encodeURIComponent(videoId)}/${endpoint}`,
options,
);
@@ -215,7 +216,10 @@ function isDirectMp4Url(url: string): boolean {
}
async function getLoomDownloadUrl(loomVideoId: string): Promise {
- const requestVariants: Array<{ endpoint: string; includeBody: boolean }> = [
+ const requestVariants: Array<{
+ endpoint: "transcoded-url" | "raw-url";
+ includeBody: boolean;
+ }> = [
{ endpoint: "transcoded-url", includeBody: true },
{ endpoint: "raw-url", includeBody: true },
{ endpoint: "transcoded-url", includeBody: false },
@@ -241,7 +245,7 @@ async function fetchLoomOEmbed(
): Promise<{ duration?: number; width?: number; height?: number } | null> {
try {
const response = await fetch(
- `https://www.loom.com/v1/oembed?url=https://www.loom.com/share/${loomVideoId}`,
+ `https://www.loom.com/v1/oembed?url=${encodeURIComponent(`https://www.loom.com/share/${loomVideoId}`)}`,
{ headers: { Accept: "application/json" } },
);
if (!response.ok) return null;
From 5b2bc8ad38c53514b0fcd9c73ca315dba79ad1ba Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Mon, 21 Sep 2026 12:24:56 +0100
Subject: [PATCH 7/7] fix: account for long Loom concierge imports
---
apps/web/actions/loom.ts | 65 +++++++++----------
.../loom-migrations/LoomMigrationQueue.tsx | 2 +-
apps/web/lib/loom-concierge.ts | 24 +++++++
3 files changed, 56 insertions(+), 35 deletions(-)
diff --git a/apps/web/actions/loom.ts b/apps/web/actions/loom.ts
index 120aa93490b..a6ac424ab98 100644
--- a/apps/web/actions/loom.ts
+++ b/apps/web/actions/loom.ts
@@ -42,6 +42,7 @@ import {
import { requireSpaceManager } from "@/actions/organization/space-authorization";
import {
releaseConciergeImport,
+ renewConciergeImport,
requireMigrationOperator,
reserveConciergeImport,
} from "@/lib/loom-concierge";
@@ -472,6 +473,13 @@ async function importLoomVideoForOwner({
videoId,
requestId: migrationRequestId,
});
+ await tx
+ .update(loomMigrationRequests)
+ .set({
+ queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + 1`,
+ updatedAt: new Date(),
+ })
+ .where(eq(loomMigrationRequests.id, migrationRequestId));
}
if (destination.spaceId === orgId) {
@@ -880,6 +888,12 @@ async function processLoomCsvRows({
const touchedSpaceIds = new Set();
for (const row of normalizedRows) {
+ if (migrationRequestId) {
+ if (!migrationLeaseToken) {
+ throw new Error("The concierge import is missing its reservation.");
+ }
+ await renewConciergeImport(migrationRequestId, migrationLeaseToken);
+ }
if (!row.loomUrl) {
results.push({
rowNumber: row.rowNumber,
@@ -1049,6 +1063,9 @@ export async function importFromLoomCsvForConcierge({
if (!(await isOrganizationOwnerPro(request.organizationId))) {
throw new Error("The destination workspace needs Cap Pro.");
}
+ if (!Array.isArray(rows) || rows.length > 10) {
+ throw new Error("Concierge CSV imports run in batches of 10 videos.");
+ }
const leaseToken = await reserveConciergeImport(requestId, operator.id);
try {
const result = await processLoomCsvRows({
@@ -1058,23 +1075,6 @@ export async function importFromLoomCsvForConcierge({
migrationRequestId: requestId,
migrationLeaseToken: leaseToken,
});
- if (result.importedCount > 0) {
- await db()
- .update(loomMigrationRequests)
- .set({
- queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + ${result.importedCount}`,
- lastOperatorUserId: operator.id,
- lastOperatorAt: new Date(),
- updatedAt: new Date(),
- })
- .where(
- and(
- eq(loomMigrationRequests.id, requestId),
- eq(loomMigrationRequests.activeImportLeaseToken, leaseToken),
- isNotNull(loomMigrationRequests.activeOrganizationId),
- ),
- );
- }
revalidatePath("/dashboard/migrations/loom");
revalidatePath("/dashboard/admin/loom-migrations");
return result;
@@ -1240,6 +1240,13 @@ export async function createConciergeLoomFileUpload({
videoId: Video.VideoId.make(existing.id),
requestId,
});
+ await tx
+ .update(loomMigrationRequests)
+ .set({
+ queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + 1`,
+ updatedAt: new Date(),
+ })
+ .where(eq(loomMigrationRequests.id, requestId));
});
}
return {
@@ -1328,6 +1335,13 @@ export async function createConciergeLoomFileUpload({
sourceId: loomVideoId,
});
await tx.insert(loomMigrationImports).values({ videoId, requestId });
+ await tx
+ .update(loomMigrationRequests)
+ .set({
+ queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + 1`,
+ updatedAt: new Date(),
+ })
+ .where(eq(loomMigrationRequests.id, requestId));
if (space) {
await tx.insert(spaceVideos).values({
id: nanoId(),
@@ -1422,23 +1436,6 @@ export async function finishConciergeLoomFileUpload({
processingMessage: "Processing Loom video...",
startFailureMessage: "Loom file processing could not start.",
});
- if (status === "started") {
- await db()
- .update(loomMigrationRequests)
- .set({
- queuedVideoCount: sql`${loomMigrationRequests.queuedVideoCount} + 1`,
- lastOperatorUserId: operator.id,
- lastOperatorAt: new Date(),
- updatedAt: new Date(),
- })
- .where(
- and(
- eq(loomMigrationRequests.id, requestId),
- eq(loomMigrationRequests.activeImportLeaseToken, leaseToken),
- isNotNull(loomMigrationRequests.activeOrganizationId),
- ),
- );
- }
revalidatePath("/dashboard/migrations/loom");
revalidatePath("/dashboard/admin/loom-migrations");
return { status };
diff --git a/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx b/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
index f1cf3806b42..34f6f5e2c42 100644
--- a/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
+++ b/apps/web/app/(org)/dashboard/admin/loom-migrations/LoomMigrationQueue.tsx
@@ -193,7 +193,7 @@ function QueueItem({ request }: { request: OperatorLoomMigrationView }) {
Loom invite:{" "}
{request.invitedAt ? "Marked as sent" : "Waiting for customer"}
- Jobs started: {request.queuedVideoCount}
+ Videos queued: {request.queuedVideoCount}
Verified videos: {request.importedVideoCount}
Imports starting: {request.activeImportCount}
diff --git a/apps/web/lib/loom-concierge.ts b/apps/web/lib/loom-concierge.ts
index 0a9a01e5127..7bd62cf691f 100644
--- a/apps/web/lib/loom-concierge.ts
+++ b/apps/web/lib/loom-concierge.ts
@@ -15,11 +15,13 @@ import {
asc,
desc,
eq,
+ gt,
isNotNull,
isNull,
lte,
ne,
or,
+ sql,
} from "drizzle-orm";
import { requireOrganizationSettingsManager } from "@/actions/organization/authorization";
import { MESSENGER_ADMIN_EMAIL } from "@/lib/messenger/constants";
@@ -171,6 +173,28 @@ export async function releaseConciergeImport(requestId: string, token: string) {
);
}
+export async function renewConciergeImport(requestId: string, token: string) {
+ const now = new Date();
+ const deadline = new Date(now.getTime() + IMPORT_LEASE_MS);
+ const result = await db()
+ .update(loomMigrationRequests)
+ .set({
+ activeImportLeaseUntil: sql`DATE_ADD(GREATEST(${loomMigrationRequests.activeImportLeaseUntil}, ${deadline}), INTERVAL 1 SECOND)`,
+ updatedAt: now,
+ })
+ .where(
+ and(
+ eq(loomMigrationRequests.id, requestId),
+ eq(loomMigrationRequests.activeImportLeaseToken, token),
+ gt(loomMigrationRequests.activeImportLeaseUntil, now),
+ isNotNull(loomMigrationRequests.activeOrganizationId),
+ ),
+ );
+ if (affectedRows(result) === 0) {
+ throw new Error("The concierge import expired. Refresh and retry.");
+ }
+}
+
export async function getCustomerMigrationRequests(
organizationId: Organisation.OrganisationId,
) {