From ff6fd7f6433d25a2ef6b5940c0bfb8ec34aec324 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 01/37] feat(db): add public collection schema for folders and spaces --- .../0028_nebulous_madame_masque.sql | 4 + .../migrations/0029_blushing_pretty_boy.sql | 3 + .../migrations/0030_add_folder_settings.sql | 1 + .../migrations/meta/0028_snapshot.json | 3298 ++++++++++++++++ .../migrations/meta/0029_snapshot.json | 3303 ++++++++++++++++ .../migrations/meta/0030_snapshot.json | 3310 +++++++++++++++++ .../database/migrations/meta/_journal.json | 21 + packages/database/schema.ts | 24 + 8 files changed, 9964 insertions(+) create mode 100644 packages/database/migrations/0028_nebulous_madame_masque.sql create mode 100644 packages/database/migrations/0029_blushing_pretty_boy.sql create mode 100644 packages/database/migrations/0030_add_folder_settings.sql create mode 100644 packages/database/migrations/meta/0028_snapshot.json create mode 100644 packages/database/migrations/meta/0029_snapshot.json create mode 100644 packages/database/migrations/meta/0030_snapshot.json diff --git a/packages/database/migrations/0028_nebulous_madame_masque.sql b/packages/database/migrations/0028_nebulous_madame_masque.sql new file mode 100644 index 0000000000..55240a7205 --- /dev/null +++ b/packages/database/migrations/0028_nebulous_madame_masque.sql @@ -0,0 +1,4 @@ +ALTER TABLE `folders` ADD `public` boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `spaces` ADD `public` boolean DEFAULT false NOT NULL;--> statement-breakpoint +CREATE INDEX `public_parent_id_idx` ON `folders` (`public`,`parentId`);--> statement-breakpoint +CREATE INDEX `public_idx` ON `spaces` (`public`); \ No newline at end of file diff --git a/packages/database/migrations/0029_blushing_pretty_boy.sql b/packages/database/migrations/0029_blushing_pretty_boy.sql new file mode 100644 index 0000000000..03e1d3f1a3 --- /dev/null +++ b/packages/database/migrations/0029_blushing_pretty_boy.sql @@ -0,0 +1,3 @@ +DROP INDEX `public_idx` ON `spaces`;--> statement-breakpoint +CREATE INDEX `public_space_parent_id_idx` ON `folders` (`public`,`spaceId`,`parentId`);--> statement-breakpoint +CREATE INDEX `public_organization_id_idx` ON `spaces` (`public`,`organizationId`); \ No newline at end of file diff --git a/packages/database/migrations/0030_add_folder_settings.sql b/packages/database/migrations/0030_add_folder_settings.sql new file mode 100644 index 0000000000..835183b3ff --- /dev/null +++ b/packages/database/migrations/0030_add_folder_settings.sql @@ -0,0 +1 @@ +ALTER TABLE `folders` ADD `settings` json; \ No newline at end of file diff --git a/packages/database/migrations/meta/0028_snapshot.json b/packages/database/migrations/meta/0028_snapshot.json new file mode 100644 index 0000000000..ebbebeb87d --- /dev/null +++ b/packages/database/migrations/meta/0028_snapshot.json @@ -0,0 +1,3298 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a7eae021-91eb-40ee-abf5-25c72ac44e74", + "prevId": "64acdb33-8e7f-46a4-b84d-7641ac12e93d", + "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": {} + }, + "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 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "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 + } + }, + "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 + }, + "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 + } + }, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "imported_videos_orgId_source_source_id_pk": { + "name": "imported_videos_orgId_source_source_id_pk", + "columns": ["orgId", "source", "source_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": {} + }, + "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": {} + }, + "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 + } + }, + "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": {} + }, + "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_idx": { + "name": "public_idx", + "columns": ["public"], + "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 + }, + "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 + } + }, + "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_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 + } + }, + "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": {} + } +} diff --git a/packages/database/migrations/meta/0029_snapshot.json b/packages/database/migrations/meta/0029_snapshot.json new file mode 100644 index 0000000000..b1c4117e9b --- /dev/null +++ b/packages/database/migrations/meta/0029_snapshot.json @@ -0,0 +1,3303 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "dbb85015-6143-46d2-933a-e77e36f6a4cb", + "prevId": "a7eae021-91eb-40ee-abf5-25c72ac44e74", + "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": {} + }, + "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 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "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 + } + }, + "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 + }, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "imported_videos_orgId_source_source_id_pk": { + "name": "imported_videos_orgId_source_source_id_pk", + "columns": ["orgId", "source", "source_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": {} + }, + "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": {} + }, + "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 + } + }, + "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": {} + }, + "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 + }, + "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 + } + }, + "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_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 + } + }, + "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": {} + } +} diff --git a/packages/database/migrations/meta/0030_snapshot.json b/packages/database/migrations/meta/0030_snapshot.json new file mode 100644 index 0000000000..a1dd211d93 --- /dev/null +++ b/packages/database/migrations/meta/0030_snapshot.json @@ -0,0 +1,3310 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "cb7983a6-9260-4bbe-a1c8-d19c68305ee8", + "prevId": "dbb85015-6143-46d2-933a-e77e36f6a4cb", + "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": {} + }, + "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 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "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 + } + }, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "imported_videos_orgId_source_source_id_pk": { + "name": "imported_videos_orgId_source_source_id_pk", + "columns": ["orgId", "source", "source_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": {} + }, + "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": {} + }, + "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 + } + }, + "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": {} + }, + "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 + }, + "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 + } + }, + "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_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 + } + }, + "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": {} + } +} diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 593dba54be..7fe7a40cf0 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -197,6 +197,27 @@ "when": 1780932760351, "tag": "0027_giant_shinko_yamashiro", "breakpoints": true + }, + { + "idx": 28, + "version": "5", + "when": 1780937790068, + "tag": "0028_nebulous_madame_masque", + "breakpoints": true + }, + { + "idx": 29, + "version": "5", + "when": 1780937848351, + "tag": "0029_blushing_pretty_boy", + "breakpoints": true + }, + { + "idx": 30, + "version": "5", + "when": 1780940100362, + "tag": "0030_add_folder_settings", + "breakpoints": true } ] } diff --git a/packages/database/schema.ts b/packages/database/schema.ts index dfcb724c63..1f00400215 100644 --- a/packages/database/schema.ts +++ b/packages/database/schema.ts @@ -4,6 +4,7 @@ import type { Folder, ImageUpload, Organisation, + PublicCollection, S3Bucket, Space, Storage, @@ -290,6 +291,11 @@ export const folders = mysqlTable( }) .notNull() .default("normal"), + // Internet-facing public collection link (/c/[id]). + public: boolean("public").notNull().default(false), + settings: json("settings").$type<{ + publicPage?: PublicCollection.PublicPageSettings; + }>(), organizationId: nanoId("organizationId") .notNull() .$type(), @@ -304,6 +310,15 @@ export const folders = mysqlTable( createdByIdIndex: index("created_by_id_idx").on(table.createdById), parentIdIndex: index("parent_id_idx").on(table.parentId), spaceIdIndex: index("space_id_idx").on(table.spaceId), + publicParentIdIndex: index("public_parent_id_idx").on( + table.public, + table.parentId, + ), + publicSpaceParentIdIndex: index("public_space_parent_id_idx").on( + table.public, + table.spaceId, + table.parentId, + ), }), ); @@ -950,17 +965,26 @@ export const spaces = mysqlTable( disableReactions?: boolean; disableTranscript?: boolean; disableComments?: boolean; + publicPage?: PublicCollection.PublicPageSettings; }>(), password: encryptedTextNullable("password"), createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow().onUpdateNow(), + // Org-internal browsability: "Public" spaces are visible to all org + // members. Unrelated to the internet-facing `public` flag below. privacy: varchar("privacy", { length: 255, enum: ["Public", "Private"] }) .notNull() .default("Private"), + // Internet-facing public collection link (/c/[id]). + public: boolean("public").notNull().default(false), }, (table) => ({ organizationIdIndex: index("organization_id_idx").on(table.organizationId), createdByIdIndex: index("created_by_id_idx").on(table.createdById), + publicOrganizationIdIndex: index("public_organization_id_idx").on( + table.public, + table.organizationId, + ), }), ); From b784e8c234ac0bb32c7499ba1f28fa0631784a5c Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 02/37] feat(domain): add PublicCollection page settings types --- packages/web-domain/src/PublicCollection.ts | 112 ++++++++++++++++++++ packages/web-domain/src/index.ts | 1 + 2 files changed, 113 insertions(+) create mode 100644 packages/web-domain/src/PublicCollection.ts diff --git a/packages/web-domain/src/PublicCollection.ts b/packages/web-domain/src/PublicCollection.ts new file mode 100644 index 0000000000..4e088cd0f2 --- /dev/null +++ b/packages/web-domain/src/PublicCollection.ts @@ -0,0 +1,112 @@ +import { Schema } from "effect"; + +/** + * Presentation settings for a public collection page (`/c/[id]`), shared by + * public folders and public spaces. Stored as `{ publicPage: PublicPageSettings }` + * inside the `settings` JSON column of both `folders` and `spaces`. + */ + +export const PublicCollectionLayout = Schema.Literal("grid", "list"); +export type PublicCollectionLayout = Schema.Schema.Type< + typeof PublicCollectionLayout +>; + +export const PublicCollectionGridColumns = Schema.Literal(2, 3, 4, 5); +export type PublicCollectionGridColumns = Schema.Schema.Type< + typeof PublicCollectionGridColumns +>; + +/** + * Which logo (if any) is shown in the public collection header. + * - `cap`: the Cap logo + * - `organization`: the organization's own icon + * - `custom`: a logo uploaded specifically for this collection (`logoUrl`) + * - `none`: no logo (and no "Powered by Cap" footer) + */ +export const PublicCollectionLogoMode = Schema.Literal( + "cap", + "organization", + "custom", + "none", +); +export type PublicCollectionLogoMode = Schema.Schema.Type< + typeof PublicCollectionLogoMode +>; + +/** Upper bounds keep stored JSON small and the public page readable. */ +export const PUBLIC_PAGE_TITLE_MAX_LENGTH = 80; +export const PUBLIC_PAGE_SUBTITLE_MAX_LENGTH = 160; +export const PUBLIC_PAGE_CTA_LABEL_MAX_LENGTH = 40; +export const PUBLIC_PAGE_CTA_URL_MAX_LENGTH = 512; +/** S3 object keys are capped at 1024 bytes. */ +export const PUBLIC_PAGE_LOGO_URL_MAX_LENGTH = 1024; + +export const PublicPageSettings = Schema.Struct({ + hideTitle: Schema.optional(Schema.Boolean), + hideCopyLink: Schema.optional(Schema.Boolean), + logoMode: Schema.optional(PublicCollectionLogoMode), + logoUrl: Schema.optional( + Schema.String.pipe(Schema.maxLength(PUBLIC_PAGE_LOGO_URL_MAX_LENGTH)), + ), + title: Schema.optional( + Schema.String.pipe(Schema.maxLength(PUBLIC_PAGE_TITLE_MAX_LENGTH)), + ), + subtitle: Schema.optional( + Schema.String.pipe(Schema.maxLength(PUBLIC_PAGE_SUBTITLE_MAX_LENGTH)), + ), + ctaLabel: Schema.optional( + Schema.String.pipe(Schema.maxLength(PUBLIC_PAGE_CTA_LABEL_MAX_LENGTH)), + ), + ctaUrl: Schema.optional( + Schema.String.pipe(Schema.maxLength(PUBLIC_PAGE_CTA_URL_MAX_LENGTH)), + ), + layout: Schema.optional(PublicCollectionLayout), + gridColumns: Schema.optional(PublicCollectionGridColumns), +}); +export type PublicPageSettings = Schema.Schema.Type; + +/** + * The client-writable subset of `PublicPageSettings`, applied as a partial + * patch merged into the stored value. `logoUrl` is excluded: it is owned by + * the logo upload action, which is the only writer of collection logo keys. + */ +export const PublicPageSettingsUpdate = PublicPageSettings.omit("logoUrl"); +export type PublicPageSettingsUpdate = Schema.Schema.Type< + typeof PublicPageSettingsUpdate +>; + +export const DEFAULT_PUBLIC_PAGE_SETTINGS = { + hideTitle: false, + hideCopyLink: false, + logoMode: "cap", + logoUrl: "", + title: "", + subtitle: "", + ctaLabel: "", + ctaUrl: "", + layout: "grid", + gridColumns: 4, +} as const satisfies Required; + +/** + * Merge a stored (possibly partial / null) `PublicPageSettings` onto the + * defaults so callers always have a fully-resolved presentation config. + */ +export function resolvePublicPageSettings( + settings: PublicPageSettings | null | undefined, +): Required { + return { + hideTitle: settings?.hideTitle ?? DEFAULT_PUBLIC_PAGE_SETTINGS.hideTitle, + hideCopyLink: + settings?.hideCopyLink ?? DEFAULT_PUBLIC_PAGE_SETTINGS.hideCopyLink, + logoMode: settings?.logoMode ?? DEFAULT_PUBLIC_PAGE_SETTINGS.logoMode, + logoUrl: settings?.logoUrl ?? DEFAULT_PUBLIC_PAGE_SETTINGS.logoUrl, + title: settings?.title ?? DEFAULT_PUBLIC_PAGE_SETTINGS.title, + subtitle: settings?.subtitle ?? DEFAULT_PUBLIC_PAGE_SETTINGS.subtitle, + ctaLabel: settings?.ctaLabel ?? DEFAULT_PUBLIC_PAGE_SETTINGS.ctaLabel, + ctaUrl: settings?.ctaUrl ?? DEFAULT_PUBLIC_PAGE_SETTINGS.ctaUrl, + layout: settings?.layout ?? DEFAULT_PUBLIC_PAGE_SETTINGS.layout, + gridColumns: + settings?.gridColumns ?? DEFAULT_PUBLIC_PAGE_SETTINGS.gridColumns, + }; +} diff --git a/packages/web-domain/src/index.ts b/packages/web-domain/src/index.ts index b8aca7049a..c94162ecce 100644 --- a/packages/web-domain/src/index.ts +++ b/packages/web-domain/src/index.ts @@ -12,6 +12,7 @@ export * as Loom from "./Loom.ts"; export * as Organisation from "./Organisation.ts"; export * from "./Organisation.ts"; export * as Policy from "./Policy.ts"; +export * as PublicCollection from "./PublicCollection.ts"; export { Rpcs } from "./Rpcs.ts"; export * as S3Bucket from "./S3Bucket.ts"; export { S3Error } from "./S3Bucket.ts"; From cda945b34cc8479656cf751e2535e148e537ed48 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 03/37] feat(domain): add public flag and settings to folder model --- packages/web-domain/src/Folder.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/web-domain/src/Folder.ts b/packages/web-domain/src/Folder.ts index 824b499553..64cbfd294f 100644 --- a/packages/web-domain/src/Folder.ts +++ b/packages/web-domain/src/Folder.ts @@ -5,6 +5,7 @@ import { RpcAuthMiddleware } from "./Authentication.ts"; import { InternalError } from "./Errors.ts"; import { OrganisationId } from "./Organisation.ts"; import { PolicyDeniedError } from "./Policy.ts"; +import { PublicPageSettingsUpdate } from "./PublicCollection.ts"; import { SpaceIdOrOrganisationId } from "./Space.ts"; import { UserId } from "./User.ts"; @@ -38,6 +39,7 @@ export class Folder extends Schema.Class("Folder")({ id: FolderId, name: Schema.String, color: FolderColor, + public: Schema.Boolean, organizationId: OrganisationId, createdById: UserId, spaceId: Schema.OptionFromNullOr(SpaceIdOrOrganisationId), @@ -48,6 +50,9 @@ export const FolderUpdate = Schema.Struct({ id: FolderId, name: Schema.optional(Schema.String), color: Schema.optional(FolderColor), + public: Schema.optional(Schema.Boolean), + /** Partial patch merged into the stored `settings.publicPage`. */ + publicPage: Schema.optional(PublicPageSettingsUpdate), parentId: Schema.optional(Schema.Option(FolderId)), }); export type FolderUpdate = Schema.Schema.Type; @@ -61,6 +66,7 @@ export class FolderRpcs extends RpcGroup.make( payload: Schema.Struct({ name: Schema.String, color: FolderColor, + public: Schema.optional(Schema.Boolean), spaceId: Schema.OptionFromUndefinedOr(SpaceIdOrOrganisationId), parentId: Schema.OptionFromUndefinedOr(FolderId), }), From 4ed52df613c25927e08c5cdf49322159b527b438 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 04/37] feat(backend): gate folder publishing on org owner Pro plan --- packages/web-backend/src/Folders/index.ts | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/web-backend/src/Folders/index.ts b/packages/web-backend/src/Folders/index.ts index 3000cf94b9..bd2f915e97 100644 --- a/packages/web-backend/src/Folders/index.ts +++ b/packages/web-backend/src/Folders/index.ts @@ -1,4 +1,5 @@ import * as Db from "@cap/database/schema"; +import { userIsPro } from "@cap/utils"; import { CurrentUser, type DatabaseError, @@ -22,6 +23,32 @@ export class Folders extends Effect.Service()("Folders", { const policy = yield* FoldersPolicy; const repo = yield* FoldersRepo; + /** + * Making a collection public is a Pro feature, gated on the organization + * OWNER's plan (any manager can publish while the owner is Pro). Disabling + * public never requires Pro, so a downgraded org can always un-publish. + */ + const requireOwnerPro = (organizationId: Organisation.OrganisationId) => + Effect.gen(function* () { + const [owner] = yield* db.use((db) => + db + .select({ + stripeSubscriptionStatus: Db.users.stripeSubscriptionStatus, + thirdPartyStripeSubscriptionId: + Db.users.thirdPartyStripeSubscriptionId, + }) + .from(Db.organizations) + .innerJoin(Db.users, Dz.eq(Db.organizations.ownerId, Db.users.id)) + .where(Dz.eq(Db.organizations.id, organizationId)) + .limit(1), + ); + + if (!userIsPro(owner ?? null)) + return yield* new Policy.PolicyDeniedError({ + reason: "Upgrade to Cap Pro to create a public collection link", + }); + }); + const deleteFolder = (folder: { id: Folder.FolderId; parentId: Folder.FolderId | null; @@ -75,11 +102,17 @@ export class Folders extends Effect.Service()("Folders", { create: Effect.fn("Folders.create")(function* (data: { name: string; color: Folder.FolderColor; + public?: boolean; spaceId: Option.Option; parentId: Option.Option; }) { const user = yield* CurrentUser; + if (data.public === true) + yield* requireOwnerPro( + Organisation.OrganisationId.make(user.activeOrganizationId), + ); + if (Option.isSome(data.spaceId)) { yield* policy.canCreateIn(data.spaceId.value); } @@ -111,6 +144,7 @@ export class Folders extends Effect.Service()("Folders", { user.activeOrganizationId, ), createdById: User.UserId.make(user.id), + public: data.public ?? false, spaceId: data.spaceId, parentId: data.parentId, }); @@ -144,6 +178,24 @@ export class Folders extends Effect.Service()("Folders", { ), ); + // Drizzle throws on an all-undefined .set(); a payload with only an + // id is a no-op, not an error. + if ( + data.name === undefined && + data.color === undefined && + data.public === undefined && + data.publicPage === undefined && + data.parentId === undefined + ) + return; + + // Publishing, or customizing the public page, is Pro-gated on the + // org owner. Un-publishing (public: false) is always allowed. + if (data.public === true || data.publicPage !== undefined) + yield* requireOwnerPro( + Organisation.OrganisationId.make(folder.organizationId), + ); + // If parentId is provided and not null, verify it exists and belongs to the same organization if (data.parentId && Option.isSome(data.parentId)) { const parentId = data.parentId.value; @@ -191,6 +243,16 @@ export class Folders extends Effect.Service()("Folders", { .set({ name: data.name, color: data.color, + public: data.public, + // Atomic merge so concurrent patches (and the logo upload + // action, which also writes settings.publicPage) can't + // overwrite each other's keys. + settings: + data.publicPage !== undefined + ? Dz.sql`JSON_MERGE_PATCH(COALESCE(${Db.folders.settings}, '{}'), CAST(${JSON.stringify( + { publicPage: data.publicPage }, + )} AS JSON))` + : undefined, parentId: data.parentId ? Option.getOrNull(data.parentId) : undefined, From 2338cc247273cb810b1f9b05c96ab90ae5b2bce7 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 05/37] feat(backend): expose public flag in space queries --- packages/web-backend/src/Spaces/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web-backend/src/Spaces/index.ts b/packages/web-backend/src/Spaces/index.ts index 7634d93756..8c46856ec0 100644 --- a/packages/web-backend/src/Spaces/index.ts +++ b/packages/web-backend/src/Spaces/index.ts @@ -28,6 +28,7 @@ export class Spaces extends Effect.Service()("Spaces", { createdById: Db.spaces.createdById, iconUrl: Db.spaces.iconUrl, settings: Db.spaces.settings, + public: Db.spaces.public, hasPassword: Dz.sql`${Db.spaces.password} IS NOT NULL`.mapWith( Boolean, ), From f7a31cc9ec9cca576e391b0ffb0c36c6564770ef Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 06/37] feat(web): add org owner Pro plan helper --- apps/web/lib/org-pro.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/web/lib/org-pro.ts diff --git a/apps/web/lib/org-pro.ts b/apps/web/lib/org-pro.ts new file mode 100644 index 0000000000..14014cf257 --- /dev/null +++ b/apps/web/lib/org-pro.ts @@ -0,0 +1,27 @@ +import "server-only"; + +import { db } from "@cap/database"; +import { organizations, users } from "@cap/database/schema"; +import { userIsPro } from "@cap/utils"; +import type { Organisation } from "@cap/web-domain"; +import { eq } from "drizzle-orm"; + +/** + * Whether an organization's OWNER is on a Pro plan. Public collection links are + * an org-wide Pro entitlement: any manager can publish while the owner is Pro. + */ +export async function isOrganizationOwnerPro( + organizationId: Organisation.OrganisationId, +): Promise { + const [owner] = await db() + .select({ + stripeSubscriptionStatus: users.stripeSubscriptionStatus, + thirdPartyStripeSubscriptionId: users.thirdPartyStripeSubscriptionId, + }) + .from(organizations) + .innerJoin(users, eq(organizations.ownerId, users.id)) + .where(eq(organizations.id, organizationId)) + .limit(1); + + return userIsPro(owner ?? null); +} From b0849e29e2fbc2720b9f67102435d39894a1214e Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 07/37] feat(web): add public collection policy helpers --- apps/web/lib/public-collections-policy.ts | 80 +++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 apps/web/lib/public-collections-policy.ts diff --git a/apps/web/lib/public-collections-policy.ts b/apps/web/lib/public-collections-policy.ts new file mode 100644 index 0000000000..0dd6172898 --- /dev/null +++ b/apps/web/lib/public-collections-policy.ts @@ -0,0 +1,80 @@ +import { isEmailAllowedByRestriction } from "@cap/utils"; + +export const PUBLIC_COLLECTION_PAGE_SIZE = 15; + +export type PublicCollectionKind = "folder" | "space"; + +export type PublicCollectionCandidate = { + kind: PublicCollectionKind; + public: boolean; + organizationTombstoneAt: Date | null; +}; + +export type PublicCollectionAccess = + | { state: "allowed" } + | { state: "email_restriction_login_required" } + | { state: "email_restriction_denied" } + | { state: "password_required" }; + +export function parsePublicCollectionPage( + page: string | string[] | undefined, +): number { + const value = Array.isArray(page) ? page[0] : page; + const parsed = Number(value); + + if (!Number.isInteger(parsed) || parsed < 1) return 1; + + return parsed; +} + +export function getPublicCollectionHref(id: string, page: number): string { + return page <= 1 ? `/c/${id}` : `/c/${id}?page=${page}`; +} + +export function resolvePublicCollectionCandidate< + TFolder extends PublicCollectionCandidate, +>(folder: TFolder | null, space: null): TFolder | null; +export function resolvePublicCollectionCandidate< + TSpace extends PublicCollectionCandidate, +>(folder: null, space: TSpace | null): TSpace | null; +export function resolvePublicCollectionCandidate< + TFolder extends PublicCollectionCandidate, + TSpace extends PublicCollectionCandidate, +>(folder: TFolder | null, space: TSpace | null): TFolder | TSpace | null; +export function resolvePublicCollectionCandidate( + folder: PublicCollectionCandidate | null, + space: PublicCollectionCandidate | null, +): PublicCollectionCandidate | null { + if (folder?.public && !folder.organizationTombstoneAt) return folder; + if (space?.public && !space.organizationTombstoneAt) return space; + + return null; +} + +export function resolvePublicCollectionAccess({ + allowedEmailDomain, + viewerEmail, + passwordHash, + verifiedPasswordHash, +}: { + allowedEmailDomain?: string | null; + viewerEmail?: string | null; + passwordHash?: string | null; + verifiedPasswordHash?: string | null; +}): PublicCollectionAccess { + const restriction = allowedEmailDomain?.trim() ?? ""; + + if (restriction.length > 0) { + if (!viewerEmail) return { state: "email_restriction_login_required" }; + + if (!isEmailAllowedByRestriction(viewerEmail, restriction)) { + return { state: "email_restriction_denied" }; + } + } + + if (passwordHash && passwordHash !== verifiedPasswordHash) { + return { state: "password_required" }; + } + + return { state: "allowed" }; +} From e1df0681ef5aa1a0b5e5b7f74afb78385d0ee504 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 08/37] test(web): add public collection policy unit tests --- .../unit/public-collections-policy.test.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 apps/web/__tests__/unit/public-collections-policy.test.ts diff --git a/apps/web/__tests__/unit/public-collections-policy.test.ts b/apps/web/__tests__/unit/public-collections-policy.test.ts new file mode 100644 index 0000000000..c05dc14a2f --- /dev/null +++ b/apps/web/__tests__/unit/public-collections-policy.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from "vitest"; +import { + getPublicCollectionHref, + parsePublicCollectionPage, + resolvePublicCollectionAccess, + resolvePublicCollectionCandidate, +} from "@/lib/public-collections-policy"; + +describe("public collections policy", () => { + it("parses invalid collection pages as the first page", () => { + expect(parsePublicCollectionPage(undefined)).toBe(1); + expect(parsePublicCollectionPage("0")).toBe(1); + expect(parsePublicCollectionPage("-2")).toBe(1); + expect(parsePublicCollectionPage("1.5")).toBe(1); + expect(parsePublicCollectionPage(["3", "4"])).toBe(3); + }); + + it("builds canonical collection page hrefs", () => { + expect(getPublicCollectionHref("abc123", 1)).toBe("/c/abc123"); + expect(getPublicCollectionHref("abc123", 2)).toBe("/c/abc123?page=2"); + }); + + it("resolves public folder before public space on id collisions", () => { + const folder = { + kind: "folder" as const, + public: true, + organizationTombstoneAt: null, + name: "Folder", + }; + const space = { + kind: "space" as const, + public: true, + organizationTombstoneAt: null, + name: "Space", + }; + + expect(resolvePublicCollectionCandidate(folder, space)).toBe(folder); + }); + + it("falls through private or tombstoned folders to public spaces", () => { + const privateFolder = { + kind: "folder" as const, + public: false, + organizationTombstoneAt: null, + }; + const tombstonedFolder = { + kind: "folder" as const, + public: true, + organizationTombstoneAt: new Date("2026-01-01T00:00:00.000Z"), + }; + const space = { + kind: "space" as const, + public: true, + organizationTombstoneAt: null, + }; + + expect(resolvePublicCollectionCandidate(privateFolder, space)).toBe(space); + expect(resolvePublicCollectionCandidate(tombstonedFolder, space)).toBe( + space, + ); + }); + + it("blocks tombstoned spaces", () => { + const space = { + kind: "space" as const, + public: true, + organizationTombstoneAt: new Date("2026-01-01T00:00:00.000Z"), + }; + + expect(resolvePublicCollectionCandidate(null, space)).toBeNull(); + }); + + it("applies email restrictions before password checks", () => { + expect( + resolvePublicCollectionAccess({ + allowedEmailDomain: "company.com", + viewerEmail: null, + passwordHash: "hash", + verifiedPasswordHash: null, + }), + ).toEqual({ state: "email_restriction_login_required" }); + + expect( + resolvePublicCollectionAccess({ + allowedEmailDomain: "company.com", + viewerEmail: "person@example.com", + passwordHash: "hash", + verifiedPasswordHash: null, + }), + ).toEqual({ state: "email_restriction_denied" }); + }); + + it("requires the collection password when present", () => { + expect( + resolvePublicCollectionAccess({ + viewerEmail: "person@company.com", + passwordHash: "space-hash", + verifiedPasswordHash: null, + }), + ).toEqual({ state: "password_required" }); + + expect( + resolvePublicCollectionAccess({ + viewerEmail: "person@company.com", + passwordHash: "space-hash", + verifiedPasswordHash: "space-hash", + }), + ).toEqual({ state: "allowed" }); + }); +}); From 40fbfa41fcbaddf1a7bb82e0767e47a2ed1545c6 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 09/37] test(web): add public page settings unit tests --- .../unit/public-page-settings.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 apps/web/__tests__/unit/public-page-settings.test.ts diff --git a/apps/web/__tests__/unit/public-page-settings.test.ts b/apps/web/__tests__/unit/public-page-settings.test.ts new file mode 100644 index 0000000000..4c8b51c54e --- /dev/null +++ b/apps/web/__tests__/unit/public-page-settings.test.ts @@ -0,0 +1,64 @@ +import { PublicCollection } from "@cap/web-domain"; +import { Either, Schema } from "effect"; +import { describe, expect, it } from "vitest"; + +const decode = Schema.decodeUnknownEither( + PublicCollection.PublicPageSettingsUpdate, +); + +describe("PublicPageSettingsUpdate", () => { + it("accepts a valid partial patch", () => { + const result = decode({ title: "Launch videos", gridColumns: 3 }); + expect(Either.isRight(result)).toBe(true); + }); + + it("strips logoUrl — only the upload action may write it", () => { + const result = decode({ + logoUrl: "organizations/other-org/logo.svg", + title: "t", + }); + expect(Either.isRight(result)).toBe(true); + if (Either.isRight(result)) { + expect("logoUrl" in result.right).toBe(false); + } + }); + + it("rejects oversized text fields", () => { + const over = (length: number) => "x".repeat(length + 1); + expect( + Either.isLeft( + decode({ + title: over(PublicCollection.PUBLIC_PAGE_TITLE_MAX_LENGTH), + }), + ), + ).toBe(true); + expect( + Either.isLeft( + decode({ + subtitle: over(PublicCollection.PUBLIC_PAGE_SUBTITLE_MAX_LENGTH), + }), + ), + ).toBe(true); + expect( + Either.isLeft( + decode({ + ctaLabel: over(PublicCollection.PUBLIC_PAGE_CTA_LABEL_MAX_LENGTH), + }), + ), + ).toBe(true); + expect( + Either.isLeft( + decode({ + ctaUrl: over(PublicCollection.PUBLIC_PAGE_CTA_URL_MAX_LENGTH), + }), + ), + ).toBe(true); + }); + + it("rejects values outside the literal unions", () => { + expect(Either.isLeft(decode({ gridColumns: 7 }))).toBe(true); + expect(Either.isLeft(decode({ layout: "carousel" }))).toBe(true); + expect(Either.isLeft(decode({ logoMode: "remote" }))).toBe(true); + expect(Either.isLeft(decode({ hideTitle: "yes" }))).toBe(true); + }); +}); From f4ced2a9be587b34ecf0666d3d2a994720d43218 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 10/37] feat(web): add share page branding resolver --- apps/web/lib/share-branding.ts | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 apps/web/lib/share-branding.ts diff --git a/apps/web/lib/share-branding.ts b/apps/web/lib/share-branding.ts new file mode 100644 index 0000000000..a84e819aee --- /dev/null +++ b/apps/web/lib/share-branding.ts @@ -0,0 +1,57 @@ +import type { ImageUpload } from "@cap/web-domain"; + +/** + * Branding shown on public-facing pages (`/s/[videoId]` and `/c/[id]`). + * Pro organizations can show a custom icon or hide the Cap logo entirely; + * free organizations always fall back to the Cap logo. + */ +export type SharePageBranding = + | { + type: "custom"; + imageUrl: ImageUpload.ImageUrl; + name: string; + } + | { + type: "cap"; + }; + +export type SharePageBrandingInput = { + owner: { isPro: boolean }; + orgSettings?: { + shareableLinkUseOrganizationIcon?: boolean; + hideShareableLinkCapLogo?: boolean; + } | null; + organizationName?: string | null; + organizationIconUrl?: ImageUpload.ImageUrl | null; + shareableLinkIconUrl?: ImageUpload.ImageUrl | null; +}; + +/** + * Resolves how a shared page should be branded. Returns `null` when a Pro + * organization has opted to hide the Cap logo and has no custom icon set. + */ +export function getSharePageBranding( + data: SharePageBrandingInput, +): SharePageBranding | null { + if (!data.owner.isPro) { + return { type: "cap" }; + } + + const brandedIcon = data.orgSettings?.shareableLinkUseOrganizationIcon + ? data.organizationIconUrl + : data.shareableLinkIconUrl; + + if (brandedIcon) { + return { + type: "custom", + imageUrl: brandedIcon, + name: data.organizationName ?? "Organization", + }; + } + + if (data.orgSettings?.hideShareableLinkCapLogo) { + return null; + } + + return { type: "cap" }; +} From e02b748316b8de3ae0e1f029b2b26c3cebd0f1e8 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 11/37] feat(web): add public collection page settings constants --- apps/web/lib/public-collection-settings.ts | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 apps/web/lib/public-collection-settings.ts diff --git a/apps/web/lib/public-collection-settings.ts b/apps/web/lib/public-collection-settings.ts new file mode 100644 index 0000000000..1ceba7668a --- /dev/null +++ b/apps/web/lib/public-collection-settings.ts @@ -0,0 +1,62 @@ +import type { PublicCollection } from "@cap/web-domain"; + +/** + * Resolves a user-provided CTA link into a safe, absolute http(s) URL. Bare + * hosts (e.g. `example.com`) are upgraded to `https://`, and anything that + * isn't http/https (e.g. `javascript:`) is rejected so the public page can't be + * turned into an injection vector. + */ +export function sanitizeCtaUrl(raw: string | null | undefined): string | null { + const value = raw?.trim(); + if (!value) return null; + + const withProtocol = /^https?:\/\//i.test(value) ? value : `https://${value}`; + + try { + const url = new URL(withProtocol); + if (url.protocol !== "http:" && url.protocol !== "https:") return null; + return url.toString(); + } catch { + return null; + } +} + +/** + * Static Tailwind class per grid density (kept as literal strings so the JIT + * compiler keeps them). Applied at the `lg` breakpoint; smaller screens always + * collapse to 1–2 columns. + */ +export const GRID_COLUMN_CLASS: Record< + PublicCollection.PublicCollectionGridColumns, + string +> = { + 2: "lg:grid-cols-2", + 3: "lg:grid-cols-3", + 4: "lg:grid-cols-4", + 5: "lg:grid-cols-5", +}; + +export const PUBLIC_LAYOUT_OPTIONS = [ + { value: "grid" as const, label: "Grid" }, + { value: "list" as const, label: "List" }, +]; + +export const PUBLIC_LOGO_OPTIONS: { + value: PublicCollection.PublicCollectionLogoMode; + label: string; +}[] = [ + { value: "cap", label: "Cap logo" }, + { value: "organization", label: "Organization logo" }, + { value: "custom", label: "Custom logo" }, + { value: "none", label: "No logo" }, +]; + +export const PUBLIC_GRID_COLUMN_OPTIONS: { + value: PublicCollection.PublicCollectionGridColumns; + label: string; +}[] = [ + { value: 2, label: "2 per row" }, + { value: 3, label: "3 per row" }, + { value: 4, label: "4 per row" }, + { value: 5, label: "5 per row" }, +]; From 50c8c4d6f840e4dc966067fbfdcc6d2e34bb0789 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 12/37] feat(web): add public collection data layer --- apps/web/lib/public-collections.ts | 802 +++++++++++++++++++++++++++++ 1 file changed, 802 insertions(+) create mode 100644 apps/web/lib/public-collections.ts diff --git a/apps/web/lib/public-collections.ts b/apps/web/lib/public-collections.ts new file mode 100644 index 0000000000..fbe48d8b25 --- /dev/null +++ b/apps/web/lib/public-collections.ts @@ -0,0 +1,802 @@ +import "server-only"; + +import { db } from "@cap/database"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { decrypt } from "@cap/database/crypto"; +import { + comments, + folders, + organizations, + sharedVideos, + spaces, + spaceVideos, + users, + videos, + videoUploads, +} from "@cap/database/schema"; +import type { VideoMetadata } from "@cap/database/types"; +import { userIsPro } from "@cap/utils"; +import { ImageUploads } from "@cap/web-backend"; +import { + type Folder, + type ImageUpload, + type Organisation, + PublicCollection as PublicCollectionDomain, + type Space, + Video, +} from "@cap/web-domain"; +import { and, desc, eq, inArray, isNull, type SQL, sql } from "drizzle-orm"; +import { Effect } from "effect"; +import { cookies } from "next/headers"; +import { cache } from "react"; +import { + PUBLIC_COLLECTION_PAGE_SIZE, + type PublicCollectionAccess, + type PublicCollectionKind, + resolvePublicCollectionAccess, + resolvePublicCollectionCandidate, +} from "@/lib/public-collections-policy"; +import { runPromise } from "@/lib/server"; + +export type PublicCollection = { + id: string; + kind: PublicCollectionKind; + name: string; + color: "normal" | "blue" | "red" | "yellow" | null; + description: string | null; + spaceId: Space.SpaceIdOrOrganisationId | null; + organizationId: Organisation.OrganisationId; + organizationName: string; + allowedEmailDomain: string | null; + passwordHash: string | null; + publicPage: Required; + organizationIconUrl: ImageUpload.ImageUrl | null; + collectionLogoUrl: ImageUpload.ImageUrl | null; +}; + +export type PublicCollectionFolder = { + id: Folder.FolderId; + name: string; + color: "normal" | "blue" | "red" | "yellow"; + parentId: Folder.FolderId | null; + videoCount: number; +}; + +/** + * Exactly the fields the public cap card renders — this object is serialized + * into the RSC payload of an anonymous page, so internal video settings, + * sources, and owner/org ids deliberately stay out of it. + */ +export type PublicCollectionVideo = { + id: Video.VideoId; + name: string; + createdAt: Date; + // Only the owner-set display date — never the full metadata JSON, which + // holds internal fields (sourceName, AI summary, processing state). + metadata: Pick | undefined; + duration: number | null; + totalComments: number; + totalReactions: number; + ownerName: string; + hasPassword: boolean; + hasActiveUpload: boolean; +}; + +type PublicCollectionVideoRow = { + id: string; + name: string; + createdAt: Date; + metadata: (typeof videos.$inferSelect)["metadata"]; + duration: number | null; + totalComments: number; + totalReactions: number; + ownerName: string | null; + hasPassword: boolean; + hasActiveUpload: boolean; +}; + +export type PublicCollectionPageData = { + collection: PublicCollection; + access: PublicCollectionAccess; + folders: PublicCollectionFolder[]; + videos: PublicCollectionVideo[]; + currentPage: number; + totalCount: number; + totalPages: number; + viewerIsSignedIn: boolean; +}; + +function videoPasswordPredicate( + videoId: SQL, + videoPassword: SQL, + verifiedPasswordHash: string | null, +) { + const noPassword = sql`(${videoPassword} IS NULL AND NOT EXISTS ( + SELECT 1 FROM space_videos sv_password + INNER JOIN spaces s_password ON sv_password.spaceId = s_password.id + WHERE sv_password.videoId = ${videoId} + AND s_password.password IS NOT NULL + ))`; + + if (!verifiedPasswordHash) return noPassword; + + return sql`(${noPassword} + OR ${videoPassword} = ${verifiedPasswordHash} + OR EXISTS ( + SELECT 1 FROM space_videos sv_password + INNER JOIN spaces s_password ON sv_password.spaceId = s_password.id + WHERE sv_password.videoId = ${videoId} + AND s_password.password = ${verifiedPasswordHash} + ) + )`; +} + +function hasPasswordExpression(videoId: SQL, videoPassword: SQL) { + return sql`${videoPassword} IS NOT NULL OR EXISTS ( + SELECT 1 FROM space_videos sv_password + INNER JOIN spaces s_password ON sv_password.spaceId = s_password.id + WHERE sv_password.videoId = ${videoId} + AND s_password.password IS NOT NULL + )`; +} + +async function getVerifiedPasswordHash() { + const cookieValue = (await cookies()).get("x-cap-password")?.value; + if (!cookieValue) return null; + + try { + // decrypt is async — without the await its rejection (corrupt or stale + // cookie, rotated encryption key) would escape this catch and crash the + // page instead of falling back to the password prompt. + return await decrypt(cookieValue); + } catch { + return null; + } +} + +export async function getPublicCollectionMetadata(collectionId: string) { + return resolvePublicCollection(collectionId); +} + +export async function getPublicCollectionPageData( + collectionId: string, + page: number, +): Promise { + const [collection, user, verifiedPasswordHash] = await Promise.all([ + resolvePublicCollection(collectionId), + getCurrentUser(), + getVerifiedPasswordHash(), + ]); + + if (!collection) return null; + + const access = resolvePublicCollectionAccess({ + allowedEmailDomain: collection.allowedEmailDomain, + viewerEmail: user?.email, + passwordHash: collection.passwordHash, + verifiedPasswordHash, + }); + + if (access.state !== "allowed") { + return { + collection, + access, + folders: [], + videos: [], + currentPage: page, + totalCount: 0, + totalPages: 0, + viewerIsSignedIn: Boolean(user), + }; + } + + const [childFolders, videoPage] = await Promise.all([ + getPublicChildFolders(collection, verifiedPasswordHash), + getPublicCollectionVideos(collection, page, verifiedPasswordHash), + ]); + + return { + collection, + access, + folders: childFolders, + videos: videoPage.videos, + currentPage: page, + totalCount: videoPage.totalCount, + totalPages: Math.ceil(videoPage.totalCount / PUBLIC_COLLECTION_PAGE_SIZE), + viewerIsSignedIn: Boolean(user), + }; +} + +async function resolveIconUrls(keys: { + organizationIconUrl: ImageUpload.ImageUrlOrKey | null; + collectionLogoUrl: ImageUpload.ImageUrlOrKey | null; +}): Promise<{ + organizationIconUrl: ImageUpload.ImageUrl | null; + collectionLogoUrl: ImageUpload.ImageUrl | null; +}> { + const empty = { + organizationIconUrl: null, + collectionLogoUrl: null, + }; + + if (!keys.organizationIconUrl && !keys.collectionLogoUrl) { + return empty; + } + + return Effect.gen(function* () { + const imageUploads = yield* ImageUploads; + const resolve = (key: ImageUpload.ImageUrlOrKey | null) => + key ? imageUploads.resolveImageUrl(key) : Effect.succeed(null); + + return { + organizationIconUrl: yield* resolve(keys.organizationIconUrl), + collectionLogoUrl: yield* resolve(keys.collectionLogoUrl), + }; + }).pipe(runPromise); +} + +function collectionLogoKey( + publicPage: Required, +): ImageUpload.ImageUrlOrKey | null { + return publicPage.logoUrl + ? (publicPage.logoUrl as ImageUpload.ImageUrlOrKey) + : null; +} + +function publicPageIconKeys( + publicPage: Required, + keys: { + organizationIconUrl: ImageUpload.ImageUrlOrKey | null; + }, +) { + return { + organizationIconUrl: + publicPage.logoMode === "organization" ? keys.organizationIconUrl : null, + collectionLogoUrl: + publicPage.logoMode === "custom" ? collectionLogoKey(publicPage) : null, + }; +} + +/** + * Custom presentation (branding, CTA, hidden Cap logo) is a Pro entitlement + * gated on the org OWNER's plan at render time, mirroring `/s/` + * (`getSharePageBranding`): when the owner downgrades, already-public + * collections stay reachable but fall back to the default presentation. + */ +function resolveEffectivePublicPage( + ownerIsPro: boolean, + stored: PublicCollectionDomain.PublicPageSettings | null | undefined, +) { + return PublicCollectionDomain.resolvePublicPageSettings( + ownerIsPro ? stored : null, + ); +} + +const resolvePublicCollection = cache( + async (collectionId: string): Promise => { + // Fetched by primary key, then gated in `resolvePublicCollectionCandidate` + // so the public/tombstone policy lives in one tested place. + const [folderRow] = await db() + .select({ + id: folders.id, + name: folders.name, + color: folders.color, + public: folders.public, + settings: folders.settings, + spaceId: folders.spaceId, + organizationId: folders.organizationId, + organizationName: organizations.name, + organizationTombstoneAt: organizations.tombstoneAt, + allowedEmailDomain: organizations.allowedEmailDomain, + organizationIconUrl: organizations.iconUrl, + ownerStripeSubscriptionStatus: users.stripeSubscriptionStatus, + ownerThirdPartyStripeSubscriptionId: + users.thirdPartyStripeSubscriptionId, + passwordHash: spaces.password, + }) + .from(folders) + .innerJoin(organizations, eq(folders.organizationId, organizations.id)) + .innerJoin(users, eq(organizations.ownerId, users.id)) + .leftJoin(spaces, eq(folders.spaceId, spaces.id)) + .where(eq(folders.id, collectionId as Folder.FolderId)) + .limit(1); + + const folder = resolvePublicCollectionCandidate( + folderRow ? { kind: "folder" as const, ...folderRow } : null, + null, + ); + + if (folder) { + const publicPage = resolveEffectivePublicPage( + userIsPro({ + stripeSubscriptionStatus: folder.ownerStripeSubscriptionStatus, + thirdPartyStripeSubscriptionId: + folder.ownerThirdPartyStripeSubscriptionId, + }), + folder.settings?.publicPage, + ); + const icons = await resolveIconUrls({ + ...publicPageIconKeys(publicPage, { + organizationIconUrl: folder.organizationIconUrl, + }), + }); + + return { + id: folder.id, + kind: "folder", + name: folder.name, + color: folder.color, + description: null, + spaceId: folder.spaceId, + organizationId: folder.organizationId, + organizationName: folder.organizationName, + allowedEmailDomain: folder.allowedEmailDomain, + passwordHash: folder.passwordHash, + publicPage, + ...icons, + }; + } + + const [spaceRow] = await db() + .select({ + id: spaces.id, + name: spaces.name, + description: spaces.description, + public: spaces.public, + settings: spaces.settings, + organizationId: spaces.organizationId, + organizationName: organizations.name, + organizationTombstoneAt: organizations.tombstoneAt, + allowedEmailDomain: organizations.allowedEmailDomain, + organizationIconUrl: organizations.iconUrl, + ownerStripeSubscriptionStatus: users.stripeSubscriptionStatus, + ownerThirdPartyStripeSubscriptionId: + users.thirdPartyStripeSubscriptionId, + passwordHash: spaces.password, + }) + .from(spaces) + .innerJoin(organizations, eq(spaces.organizationId, organizations.id)) + .innerJoin(users, eq(organizations.ownerId, users.id)) + .where(eq(spaces.id, collectionId as Space.SpaceIdOrOrganisationId)) + .limit(1); + + const space = resolvePublicCollectionCandidate( + null, + spaceRow ? { kind: "space" as const, ...spaceRow } : null, + ); + + if (!space) return null; + + const publicPage = resolveEffectivePublicPage( + userIsPro({ + stripeSubscriptionStatus: space.ownerStripeSubscriptionStatus, + thirdPartyStripeSubscriptionId: + space.ownerThirdPartyStripeSubscriptionId, + }), + space.settings?.publicPage, + ); + const icons = await resolveIconUrls({ + ...publicPageIconKeys(publicPage, { + organizationIconUrl: space.organizationIconUrl, + }), + }); + + return { + id: space.id, + kind: "space", + name: space.name, + color: null, + description: space.description, + spaceId: space.id, + organizationId: space.organizationId, + organizationName: space.organizationName, + allowedEmailDomain: space.allowedEmailDomain, + passwordHash: space.passwordHash, + publicPage, + ...icons, + }; + }, +); + +/** + * Single source of truth for which password protects a public collection + * (a folder inherits its parent space's password). Reused by the password + * verification action so it can never drift from what the page checks. + */ +export async function getPublicCollectionPasswordHash( + collectionId: string, +): Promise { + const collection = await resolvePublicCollection(collectionId); + return collection?.passwordHash ?? null; +} + +/** + * Folders in the org-wide shared area carry the ORGANIZATION id as their + * spaceId (the "all spaces" entry) and track membership in `shared_videos`, + * not `space_videos`. + */ +function isOrgLevelFolder(collection: PublicCollection) { + return ( + collection.kind === "folder" && + collection.spaceId === collection.organizationId + ); +} + +async function getPublicCollectionVideos( + collection: PublicCollection, + page: number, + verifiedPasswordHash: string | null, +) { + if (collection.kind === "space") { + return getPublicSpaceVideos(collection, page, verifiedPasswordHash); + } + + if (isOrgLevelFolder(collection)) { + return getPublicOrgFolderVideos(collection, page, verifiedPasswordHash); + } + + if (collection.spaceId) { + return getPublicSpaceFolderVideos(collection, page, verifiedPasswordHash); + } + + return getPublicUserFolderVideos(collection, page, verifiedPasswordHash); +} + +const videoSelect = { + id: videos.id, + name: videos.name, + createdAt: videos.createdAt, + metadata: videos.metadata, + duration: videos.duration, + totalComments: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'text' THEN ${comments.id} END)`, + totalReactions: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'emoji' THEN ${comments.id} END)`, + ownerName: users.name, + hasPassword: hasPasswordExpression( + sql`${videos.id}`, + sql`${videos.password}`, + ).mapWith(Boolean), + hasActiveUpload: sql`MAX(${videoUploads.videoId} IS NOT NULL)`.mapWith( + Boolean, + ), +}; + +const videoGroupBy = [ + videos.id, + videos.ownerId, + videos.orgId, + videos.name, + videos.createdAt, + videos.metadata, + videos.source, + videos.isScreenshot, + videos.duration, + videos.public, + videos.settings, + videos.password, + users.name, +]; + +function toPublicCollectionVideos( + videoRows: PublicCollectionVideoRow[], +): PublicCollectionVideo[] { + return videoRows.map((video) => ({ + id: Video.VideoId.make(video.id), + name: video.name, + createdAt: video.createdAt, + metadata: video.metadata?.customCreatedAt + ? { customCreatedAt: video.metadata.customCreatedAt } + : undefined, + duration: video.duration, + totalComments: video.totalComments, + totalReactions: video.totalReactions, + ownerName: video.ownerName ?? "", + hasPassword: video.hasPassword, + hasActiveUpload: video.hasActiveUpload, + })); +} + +async function getPublicSpaceVideos( + collection: PublicCollection, + page: number, + verifiedPasswordHash: string | null, +) { + const offset = (page - 1) * PUBLIC_COLLECTION_PAGE_SIZE; + const where = and( + eq(spaceVideos.spaceId, collection.id as Space.SpaceIdOrOrganisationId), + eq(videos.public, true), + isNull(organizations.tombstoneAt), + videoPasswordPredicate( + sql`${videos.id}`, + sql`${videos.password}`, + verifiedPasswordHash, + ), + ); + + const [videoRows, totalCountResult] = await Promise.all([ + db() + .select(videoSelect) + .from(spaceVideos) + .innerJoin(videos, eq(spaceVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .leftJoin(comments, eq(videos.id, comments.videoId)) + .leftJoin(users, eq(videos.ownerId, users.id)) + .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) + .where(where) + .groupBy(...videoGroupBy) + .orderBy(desc(videos.effectiveCreatedAt)) + .limit(PUBLIC_COLLECTION_PAGE_SIZE) + .offset(offset), + db() + .select({ count: sql`COUNT(DISTINCT ${videos.id})` }) + .from(spaceVideos) + .innerJoin(videos, eq(spaceVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .where(where), + ]); + + return { + videos: toPublicCollectionVideos(videoRows), + totalCount: totalCountResult[0]?.count ?? 0, + }; +} + +async function getPublicSpaceFolderVideos( + collection: PublicCollection, + page: number, + verifiedPasswordHash: string | null, +) { + const offset = (page - 1) * PUBLIC_COLLECTION_PAGE_SIZE; + const where = and( + eq(spaceVideos.folderId, collection.id as Folder.FolderId), + eq(videos.public, true), + isNull(organizations.tombstoneAt), + videoPasswordPredicate( + sql`${videos.id}`, + sql`${videos.password}`, + verifiedPasswordHash, + ), + ); + + const [videoRows, totalCountResult] = await Promise.all([ + db() + .select(videoSelect) + .from(spaceVideos) + .innerJoin(videos, eq(spaceVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .leftJoin(comments, eq(videos.id, comments.videoId)) + .leftJoin(users, eq(videos.ownerId, users.id)) + .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) + .where(where) + .groupBy(...videoGroupBy) + .orderBy(desc(videos.effectiveCreatedAt)) + .limit(PUBLIC_COLLECTION_PAGE_SIZE) + .offset(offset), + db() + .select({ count: sql`COUNT(DISTINCT ${videos.id})` }) + .from(spaceVideos) + .innerJoin(videos, eq(spaceVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .where(where), + ]); + + return { + videos: toPublicCollectionVideos(videoRows), + totalCount: totalCountResult[0]?.count ?? 0, + }; +} + +async function getPublicOrgFolderVideos( + collection: PublicCollection, + page: number, + verifiedPasswordHash: string | null, +) { + const offset = (page - 1) * PUBLIC_COLLECTION_PAGE_SIZE; + const where = and( + eq(sharedVideos.folderId, collection.id as Folder.FolderId), + eq(videos.public, true), + isNull(organizations.tombstoneAt), + videoPasswordPredicate( + sql`${videos.id}`, + sql`${videos.password}`, + verifiedPasswordHash, + ), + ); + + const [videoRows, totalCountResult] = await Promise.all([ + db() + .select(videoSelect) + .from(sharedVideos) + .innerJoin(videos, eq(sharedVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .leftJoin(comments, eq(videos.id, comments.videoId)) + .leftJoin(users, eq(videos.ownerId, users.id)) + .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) + .where(where) + .groupBy(...videoGroupBy) + .orderBy(desc(videos.effectiveCreatedAt)) + .limit(PUBLIC_COLLECTION_PAGE_SIZE) + .offset(offset), + db() + .select({ count: sql`COUNT(DISTINCT ${videos.id})` }) + .from(sharedVideos) + .innerJoin(videos, eq(sharedVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .where(where), + ]); + + return { + videos: toPublicCollectionVideos(videoRows), + totalCount: totalCountResult[0]?.count ?? 0, + }; +} + +async function getPublicUserFolderVideos( + collection: PublicCollection, + page: number, + verifiedPasswordHash: string | null, +) { + const offset = (page - 1) * PUBLIC_COLLECTION_PAGE_SIZE; + const where = and( + eq(videos.folderId, collection.id as Folder.FolderId), + eq(videos.public, true), + isNull(organizations.tombstoneAt), + videoPasswordPredicate( + sql`${videos.id}`, + sql`${videos.password}`, + verifiedPasswordHash, + ), + ); + + const [videoRows, totalCountResult] = await Promise.all([ + db() + .select(videoSelect) + .from(videos) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .leftJoin(comments, eq(videos.id, comments.videoId)) + .leftJoin(users, eq(videos.ownerId, users.id)) + .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) + .where(where) + .groupBy(...videoGroupBy) + .orderBy(desc(videos.effectiveCreatedAt)) + .limit(PUBLIC_COLLECTION_PAGE_SIZE) + .offset(offset), + db() + .select({ count: sql`COUNT(DISTINCT ${videos.id})` }) + .from(videos) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .where(where), + ]); + + return { + videos: toPublicCollectionVideos(videoRows), + totalCount: totalCountResult[0]?.count ?? 0, + }; +} + +async function getPublicChildFolders( + collection: PublicCollection, + verifiedPasswordHash: string | null, +): Promise { + const where = + collection.kind === "space" + ? and( + eq(folders.spaceId, collection.id as Space.SpaceIdOrOrganisationId), + isNull(folders.parentId), + eq(folders.public, true), + isNull(organizations.tombstoneAt), + ) + : and( + eq(folders.parentId, collection.id as Folder.FolderId), + eq(folders.public, true), + isNull(organizations.tombstoneAt), + ); + + const childFolders = await db() + .select({ + id: folders.id, + name: folders.name, + color: folders.color, + parentId: folders.parentId, + }) + .from(folders) + .innerJoin(organizations, eq(folders.organizationId, organizations.id)) + .where(where) + .orderBy(folders.name); + + if (childFolders.length === 0) return []; + + const folderIds = childFolders.map((folder) => folder.id); + const counts = isOrgLevelFolder(collection) + ? await getPublicOrgFolderVideoCounts(folderIds, verifiedPasswordHash) + : collection.kind === "space" || Boolean(collection.spaceId) + ? await getPublicSpaceFolderVideoCounts(folderIds, verifiedPasswordHash) + : await getPublicUserFolderVideoCounts(folderIds, verifiedPasswordHash); + const countByFolderId = new Map( + counts.map((row) => [row.folderId, row.videoCount]), + ); + + return childFolders.map((folder) => ({ + ...folder, + videoCount: countByFolderId.get(folder.id) ?? 0, + })); +} + +async function getPublicSpaceFolderVideoCounts( + folderIds: Folder.FolderId[], + verifiedPasswordHash: string | null, +) { + return db() + .select({ + folderId: spaceVideos.folderId, + videoCount: sql`COUNT(DISTINCT ${videos.id})`, + }) + .from(spaceVideos) + .innerJoin(videos, eq(spaceVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .where( + and( + inArray(spaceVideos.folderId, folderIds), + eq(videos.public, true), + isNull(organizations.tombstoneAt), + videoPasswordPredicate( + sql`${videos.id}`, + sql`${videos.password}`, + verifiedPasswordHash, + ), + ), + ) + .groupBy(spaceVideos.folderId); +} + +async function getPublicOrgFolderVideoCounts( + folderIds: Folder.FolderId[], + verifiedPasswordHash: string | null, +) { + return db() + .select({ + folderId: sharedVideos.folderId, + videoCount: sql`COUNT(DISTINCT ${videos.id})`, + }) + .from(sharedVideos) + .innerJoin(videos, eq(sharedVideos.videoId, videos.id)) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .where( + and( + inArray(sharedVideos.folderId, folderIds), + eq(videos.public, true), + isNull(organizations.tombstoneAt), + videoPasswordPredicate( + sql`${videos.id}`, + sql`${videos.password}`, + verifiedPasswordHash, + ), + ), + ) + .groupBy(sharedVideos.folderId); +} + +async function getPublicUserFolderVideoCounts( + folderIds: Folder.FolderId[], + verifiedPasswordHash: string | null, +) { + return db() + .select({ + folderId: videos.folderId, + videoCount: sql`COUNT(DISTINCT ${videos.id})`, + }) + .from(videos) + .innerJoin(organizations, eq(videos.orgId, organizations.id)) + .where( + and( + inArray(videos.folderId, folderIds), + eq(videos.public, true), + isNull(organizations.tombstoneAt), + videoPasswordPredicate( + sql`${videos.id}`, + sql`${videos.password}`, + verifiedPasswordHash, + ), + ), + ) + .groupBy(videos.folderId); +} From cc97031e532535399bb99cc167fabd96d1866280 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 13/37] feat(web): extract verified password cookie helper --- apps/web/lib/password-cookie.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/web/lib/password-cookie.ts diff --git a/apps/web/lib/password-cookie.ts b/apps/web/lib/password-cookie.ts new file mode 100644 index 0000000000..138371047e --- /dev/null +++ b/apps/web/lib/password-cookie.ts @@ -0,0 +1,18 @@ +import "server-only"; + +import { encrypt } from "@cap/database/crypto"; +import { cookies } from "next/headers"; + +/** + * Marks a share password (video or collection) as verified for this browser + * session. The cookie is shared by the `/s/` and `/c/` flows, holds only the + * encrypted hash, and is never read client-side — so it can be locked down. + */ +export async function setVerifiedPasswordCookie(passwordHash: string) { + (await cookies()).set("x-cap-password", await encrypt(passwordHash), { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + }); +} From 5c8c917cacf362f38603dfeaa91517906bec352f Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 14/37] refactor(web): use password cookie helper in video share action --- apps/web/actions/videos/password.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/web/actions/videos/password.ts b/apps/web/actions/videos/password.ts index 864828d12b..ec199d2e4e 100644 --- a/apps/web/actions/videos/password.ts +++ b/apps/web/actions/videos/password.ts @@ -3,7 +3,6 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { - encrypt, hashPassword, verifyPassword as verifyPlainPassword, } from "@cap/database/crypto"; @@ -12,7 +11,7 @@ import { collectPasswordHashes } from "@cap/web-backend"; import type { Video } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; -import { cookies } from "next/headers"; +import { setVerifiedPasswordCookie } from "@/lib/password-cookie"; export async function setVideoPassword( videoId: Video.VideoId, @@ -115,7 +114,7 @@ export async function verifyVideoPassword( for (const passwordHash of passwordHashes) { const valid = await verifyPlainPassword(passwordHash, password); if (valid) { - (await cookies()).set("x-cap-password", await encrypt(passwordHash)); + await setVerifiedPasswordCookie(passwordHash); return { success: true, value: "Password verified" }; } } From 68268981a83d9c8c24a48c2bac9997dc5898c468 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 15/37] feat(web): add collection password verification action --- apps/web/actions/collections/password.ts | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 apps/web/actions/collections/password.ts diff --git a/apps/web/actions/collections/password.ts b/apps/web/actions/collections/password.ts new file mode 100644 index 0000000000..2443a9813e --- /dev/null +++ b/apps/web/actions/collections/password.ts @@ -0,0 +1,68 @@ +"use server"; + +import { verifyPassword as verifyPlainPassword } from "@cap/database/crypto"; +import { NODE_ENV } from "@cap/env"; +import { checkRateLimit } from "@vercel/firewall"; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { setVerifiedPasswordCookie } from "@/lib/password-cookie"; +import { getPublicCollectionPasswordHash } from "@/lib/public-collections"; + +const COLLECTION_PASSWORD_RATE_LIMIT_ID = "rl_collection_password"; + +/** Per-IP throttle so the public action isn't an open brute-force oracle. */ +async function isRateLimited() { + if (NODE_ENV !== "production") return false; + + try { + const headersList = await headers(); + const request = new Request("https://cap.so/api/collection-password", { + method: "POST", + headers: headersList, + }); + + const { rateLimited } = await checkRateLimit( + COLLECTION_PASSWORD_RATE_LIMIT_ID, + { request }, + ); + return rateLimited; + } catch (error) { + // Best-effort: self-hosted deploys without the Vercel firewall (or an + // x-real-ip header) must not lose password verification entirely; the + // PBKDF2 verification cost still slows brute force. + console.warn("Collection password rate limit check failed:", error); + return false; + } +} + +export async function verifyCollectionPassword( + collectionId: string, + password: string, +) { + try { + if (!collectionId || typeof password !== "string") { + throw new Error("Missing data"); + } + + if (await isRateLimited()) { + return { + success: false, + error: "Too many attempts. Please try again later.", + }; + } + + const passwordHash = await getPublicCollectionPasswordHash(collectionId); + if (!passwordHash) throw new Error("No password set"); + + const valid = await verifyPlainPassword(passwordHash, password); + if (!valid) throw new Error("Invalid password"); + + await setVerifiedPasswordCookie(passwordHash); + revalidatePath(`/c/${encodeURIComponent(collectionId)}`); + + return { success: true, value: "Password verified" }; + } catch (error) { + console.error("Error verifying collection password:", error); + return { success: false, error: "Failed to verify password" }; + } +} From 79caae5af55b6a4b9bc1d02d12e7f61b92612189 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 16/37] feat(web): add space collection visibility action --- apps/web/actions/collections/visibility.ts | 98 ++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 apps/web/actions/collections/visibility.ts diff --git a/apps/web/actions/collections/visibility.ts b/apps/web/actions/collections/visibility.ts new file mode 100644 index 0000000000..b5871c6bce --- /dev/null +++ b/apps/web/actions/collections/visibility.ts @@ -0,0 +1,98 @@ +"use server"; + +import { db } from "@cap/database"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { spaces } from "@cap/database/schema"; +import { PublicCollection, Space } from "@cap/web-domain"; +import { eq, type SQL, sql } from "drizzle-orm"; +import { Either, Schema } from "effect"; +import { revalidatePath } from "next/cache"; +import { isOrganizationOwnerPro } from "@/lib/org-pro"; +import { requireSpaceManager } from "../organization/space-authorization"; + +const decodeSettingsPatch = Schema.decodeUnknownEither( + PublicCollection.PublicPageSettingsUpdate, +); + +/** + * Toggles a space's public collection link and/or its public-page presentation + * settings from the dashboard space page. Enabling public (or customizing the + * page) requires the org owner to be on Pro; un-publishing is always allowed. + * + * `settings` is a partial patch merged into the stored `settings.publicPage`, + * mirroring the folder path (FolderUpdate RPC). + */ +export async function setSpaceCollectionVisibility(input: { + spaceId: string; + public?: boolean; + settings?: PublicCollection.PublicPageSettingsUpdate; +}) { + const user = await getCurrentUser(); + if (!user) return { success: false, error: "Unauthorized" }; + + // Server actions are publicly callable; the TS parameter types are + // compile-time only, so validate everything before it reaches the database. + if (typeof input.spaceId !== "string" || input.spaceId.length === 0) { + return { success: false, error: "Invalid request" }; + } + if (input.public !== undefined && typeof input.public !== "boolean") { + return { success: false, error: "Invalid request" }; + } + + let settingsPatch: PublicCollection.PublicPageSettingsUpdate | undefined; + if (input.settings !== undefined) { + const decoded = decodeSettingsPatch(input.settings); + if (Either.isLeft(decoded)) { + return { success: false, error: "Invalid public page settings" }; + } + settingsPatch = decoded.right; + } + + const id = Space.SpaceId.make(input.spaceId); + + const [[space], access] = await Promise.all([ + db() + .select({ + organizationId: spaces.organizationId, + public: spaces.public, + }) + .from(spaces) + .where(eq(spaces.id, id)) + .limit(1), + requireSpaceManager(user.id, id).catch(() => null), + ]); + + if (!space) return { success: false, error: "Space not found" }; + if (!access) return { success: false, error: "Unauthorized" }; + + const enablingPublic = input.public === true && !space.public; + const changingSettings = settingsPatch !== undefined; + + if ( + (enablingPublic || changingSettings) && + !(await isOrganizationOwnerPro(space.organizationId)) + ) { + return { + success: false, + error: "Upgrade to Cap Pro to create a public collection link", + }; + } + + const update: { public?: boolean; settings?: SQL } = {}; + if (input.public !== undefined) update.public = input.public; + if (settingsPatch !== undefined) { + // Atomic merge so concurrent patches (and the logo upload action, which + // also writes settings.publicPage) can't overwrite each other's keys. + update.settings = sql`JSON_MERGE_PATCH(COALESCE(${spaces.settings}, '{}'), CAST(${JSON.stringify( + { publicPage: settingsPatch }, + )} AS JSON))`; + } + + if (Object.keys(update).length > 0) + await db().update(spaces).set(update).where(eq(spaces.id, id)); + + revalidatePath("/dashboard"); + revalidatePath(`/dashboard/spaces/${id}`); + revalidatePath(`/c/${id}`); + return { success: true }; +} From 27e6d8a0da4671dc4aac44f058ef940cf6f82607 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 17/37] feat(web): add collection logo upload action --- apps/web/actions/collections/logo.ts | 226 +++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 apps/web/actions/collections/logo.ts diff --git a/apps/web/actions/collections/logo.ts b/apps/web/actions/collections/logo.ts new file mode 100644 index 0000000000..0ee0a7fad3 --- /dev/null +++ b/apps/web/actions/collections/logo.ts @@ -0,0 +1,226 @@ +"use server"; + +import { db } from "@cap/database"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { folders, spaces } from "@cap/database/schema"; +import { ImageUploads } from "@cap/web-backend"; +import { + type Folder, + type ImageUpload, + Space, + type User, +} from "@cap/web-domain"; +import { eq, sql } from "drizzle-orm"; +import { Effect, Option } from "effect"; +import { revalidatePath } from "next/cache"; +import { isOrganizationOwnerPro } from "@/lib/org-pro"; +import { canManageSpace } from "@/lib/permissions/roles"; +import { sanitizeFile } from "@/lib/sanitizeFile"; +import { runPromise } from "@/lib/server"; +import { getOrganizationAccess } from "../organization/authorization"; +import { getSpaceAccess } from "../organization/space-authorization"; + +const MAX_LOGO_BYTES = 1024 * 1024; +const ALLOWED_LOGO_TYPES = new Set([ + "image/png", + "image/jpeg", + "image/svg+xml", + "image/webp", +]); + +function logoKey(value: string | undefined) { + return value ? (value as ImageUpload.ImageUrlOrKey) : null; +} + +async function readFilePayload( + formData: FormData, +): Promise { + if (formData.get("remove") === "true") return Option.none(); + + const file = formData.get("logo"); + if (!(file instanceof File) || file.size === 0) { + return { error: "No file provided" }; + } + if (!ALLOWED_LOGO_TYPES.has(file.type.toLowerCase())) { + return { error: "Please upload a PNG, JPEG, SVG or WebP image" }; + } + if (file.size > MAX_LOGO_BYTES) { + return { error: "Logo must be 1MB or less" }; + } + + // Strips scripts/event handlers from SVGs (same treatment as space icons); + // other formats pass through untouched. + const sanitized = await sanitizeFile(file); + const data = new Uint8Array(await sanitized.arrayBuffer()); + return Option.some({ + contentType: file.type, + fileName: file.name, + data, + }); +} + +/** + * Uploads (or removes) the custom logo shown on a collection's public page. + * Publishing/customizing collections is a Pro entitlement, so this mirrors the + * Pro gate enforced on the rest of the public-page settings. + */ +export async function setCollectionLogo(formData: FormData) { + const user = await getCurrentUser(); + if (!user) return { success: false, error: "Unauthorized" }; + + const collectionId = String(formData.get("collectionId") ?? ""); + const kind = String(formData.get("kind") ?? ""); + if (!collectionId || (kind !== "folder" && kind !== "space")) { + return { success: false, error: "Invalid request" }; + } + + const payloadResult = await readFilePayload(formData); + if ("error" in payloadResult) { + return { success: false, error: payloadResult.error }; + } + + if (kind === "space") { + return setSpaceLogo(collectionId, user.id, payloadResult); + } + return setFolderLogo(collectionId, user.id, payloadResult); +} + +async function setSpaceLogo( + collectionId: string, + userId: User.UserId, + payload: ImageUpload.ImageUpdatePayload, +) { + const id = Space.SpaceId.make(collectionId); + const [space] = await db() + .select({ + organizationId: spaces.organizationId, + settings: spaces.settings, + }) + .from(spaces) + .where(eq(spaces.id, id)) + .limit(1); + + if (!space) return { success: false, error: "Space not found" }; + + const access = await getSpaceAccess(userId, id).catch(() => null); + if (!access?.canManage) return { success: false, error: "Unauthorized" }; + + if (!(await isOrganizationOwnerPro(space.organizationId))) { + return { + success: false, + error: "Upgrade to Cap Pro to customize the collection logo", + }; + } + + const existing = space.settings ?? {}; + + await Effect.gen(function* () { + const imageUploads = yield* ImageUploads; + yield* imageUploads.applyUpdate({ + payload, + existing: Option.fromNullable(logoKey(existing.publicPage?.logoUrl)), + keyPrefix: `organizations/${space.organizationId}/collections/${id}/logo`, + update: (database, key) => + database + .update(spaces) + .set({ + // Atomic merge (a JSON null deletes the key per RFC 7396) so + // concurrent settings patches can't overwrite the logo write. + settings: sql`JSON_MERGE_PATCH(COALESCE(${spaces.settings}, '{}'), CAST(${JSON.stringify( + { + publicPage: { + logoUrl: key ?? null, + logoMode: key ? "custom" : "cap", + }, + }, + )} AS JSON))`, + }) + .where(eq(spaces.id, id)), + }); + }).pipe(runPromise); + + revalidateCollection(collectionId); + return { success: true }; +} + +async function setFolderLogo( + collectionId: string, + userId: User.UserId, + payload: ImageUpload.ImageUpdatePayload, +) { + const id = collectionId as Folder.FolderId; + const [folder] = await db() + .select({ + organizationId: folders.organizationId, + spaceId: folders.spaceId, + createdById: folders.createdById, + settings: folders.settings, + }) + .from(folders) + .where(eq(folders.id, id)) + .limit(1); + + if (!folder) return { success: false, error: "Folder not found" }; + + // Mirrors FoldersPolicy.canEdit: folders in a real space require space/org + // management; folders in the org-wide area (spaceId === organizationId) + // require org management; personal folders are creator-only. + const canManage = !folder.spaceId + ? folder.createdById === userId + : folder.spaceId === folder.organizationId + ? canManageSpace({ + organizationRole: ( + await getOrganizationAccess(userId, folder.organizationId).catch( + () => null, + ) + )?.role, + spaceRole: null, + }) + : ((await getSpaceAccess(userId, folder.spaceId).catch(() => null)) + ?.canManage ?? false); + if (!canManage) return { success: false, error: "Unauthorized" }; + + if (!(await isOrganizationOwnerPro(folder.organizationId))) { + return { + success: false, + error: "Upgrade to Cap Pro to customize the collection logo", + }; + } + + const existing = folder.settings ?? {}; + + await Effect.gen(function* () { + const imageUploads = yield* ImageUploads; + yield* imageUploads.applyUpdate({ + payload, + existing: Option.fromNullable(logoKey(existing.publicPage?.logoUrl)), + keyPrefix: `organizations/${folder.organizationId}/collections/${id}/logo`, + update: (database, key) => + database + .update(folders) + .set({ + // Atomic merge (a JSON null deletes the key per RFC 7396) so + // concurrent settings patches can't overwrite the logo write. + settings: sql`JSON_MERGE_PATCH(COALESCE(${folders.settings}, '{}'), CAST(${JSON.stringify( + { + publicPage: { + logoUrl: key ?? null, + logoMode: key ? "custom" : "cap", + }, + }, + )} AS JSON))`, + }) + .where(eq(folders.id, id)), + }); + }).pipe(runPromise); + + revalidateCollection(collectionId); + return { success: true }; +} + +function revalidateCollection(collectionId: string) { + revalidatePath("/dashboard"); + revalidatePath(`/dashboard/spaces/${collectionId}`); + revalidatePath(`/dashboard/folder/${collectionId}`); + revalidatePath(`/c/${collectionId}`); +} From 83f0236d645ea6ad41f4b926700b2b88004608b3 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 18/37] feat(web): add public collection client helpers --- apps/web/lib/public-collection-client.ts | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 apps/web/lib/public-collection-client.ts diff --git a/apps/web/lib/public-collection-client.ts b/apps/web/lib/public-collection-client.ts new file mode 100644 index 0000000000..6ba27d61fb --- /dev/null +++ b/apps/web/lib/public-collection-client.ts @@ -0,0 +1,43 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import { usePublicEnv } from "@/utils/public-env"; + +/** + * Canonical public collection link + copy-to-clipboard with the shared + * 2s "copied" feedback state. Builds the URL from the configured web URL so + * every dashboard surface produces the same link regardless of the origin the + * dashboard happens to be served from. + */ +export function useCopyCollectionLink(collectionId: string | undefined) { + const { webUrl } = usePublicEnv(); + const [copied, setCopied] = useState(false); + const timeoutRef = useRef | null>(null); + + useEffect( + () => () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }, + [], + ); + + const url = `${webUrl}/c/${collectionId ?? ""}`; + + const copy = async () => { + if (!collectionId) return false; + try { + await navigator.clipboard.writeText(url); + } catch { + toast.error("Failed to copy public collection link"); + return false; + } + toast.success("Public collection link copied"); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setCopied(true); + timeoutRef.current = setTimeout(() => setCopied(false), 2000); + return true; + }; + + return { url, copied, copy }; +} From e18f2f256a44ea3589fa1c14a86d74aa164a57cc Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:34 +0100 Subject: [PATCH 19/37] fix(web): handle corrupt password cookies gracefully --- apps/web/lib/server.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/server.ts b/apps/web/lib/server.ts index 8d744a3976..dce31de2e2 100644 --- a/apps/web/lib/server.ts +++ b/apps/web/lib/server.ts @@ -51,7 +51,14 @@ const CookiePasswordAttachmentLive = Layer.effect( const password = Option.fromNullable( yield* Effect.promise(async () => { const pw = (await cookies()).get("x-cap-password")?.value; - if (pw) return decrypt(pw); + if (!pw) return undefined; + try { + return await decrypt(pw); + } catch { + // Corrupt or stale cookie (e.g. rotated encryption key) — treat + // as absent rather than crashing the page with a defect. + return undefined; + } }), ); return { password }; From 11aede59ebf8d2742030212b8096c4580c362cee Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:42 +0100 Subject: [PATCH 20/37] feat(web): add public collection route layout --- apps/web/app/c/layout.tsx | 8 ++++++++ apps/web/app/c/page.tsx | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 apps/web/app/c/layout.tsx create mode 100644 apps/web/app/c/page.tsx diff --git a/apps/web/app/c/layout.tsx b/apps/web/app/c/layout.tsx new file mode 100644 index 0000000000..8f853e14d7 --- /dev/null +++ b/apps/web/app/c/layout.tsx @@ -0,0 +1,8 @@ +import type { PropsWithChildren } from "react"; +import { AppProviders } from "../Layout/AppProviders"; + +export const dynamic = "force-dynamic"; + +export default function CollectionsLayout({ children }: PropsWithChildren) { + return {children}; +} diff --git a/apps/web/app/c/page.tsx b/apps/web/app/c/page.tsx new file mode 100644 index 0000000000..dd90e99cc9 --- /dev/null +++ b/apps/web/app/c/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default async function CollectionsPage() { + redirect("/"); +} From ba14fff1749d4b481bfc9c3087378c6d64eeaf09 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:42 +0100 Subject: [PATCH 21/37] feat(web): add public collection password overlay --- .../app/c/[id]/CollectionPasswordOverlay.tsx | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 apps/web/app/c/[id]/CollectionPasswordOverlay.tsx diff --git a/apps/web/app/c/[id]/CollectionPasswordOverlay.tsx b/apps/web/app/c/[id]/CollectionPasswordOverlay.tsx new file mode 100644 index 0000000000..53a21d80e3 --- /dev/null +++ b/apps/web/app/c/[id]/CollectionPasswordOverlay.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { Button, Dialog, DialogContent, Input, Logo } from "@cap/ui"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { useId, useState } from "react"; +import { toast } from "sonner"; +import { verifyCollectionPassword } from "@/actions/collections/password"; +import type { SharePageBranding } from "@/lib/share-branding"; + +export function CollectionPasswordOverlay({ + collectionId, + collectionName, + organizationName, + branding, + isOpen, +}: { + collectionId: string; + collectionName: string; + organizationName: string; + branding: SharePageBranding | null; + isOpen: boolean; +}) { + const [password, setPassword] = useState(""); + const passwordInputId = useId(); + const router = useRouter(); + + const verifyPassword = useMutation({ + mutationFn: () => + verifyCollectionPassword(collectionId, password).then((value) => { + if (value.success) return value.value; + throw new Error(value.error); + }), + onSuccess: (result) => { + toast.success(result); + router.refresh(); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + + return ( + + +
+
+ {branding?.type === "custom" ? ( + // biome-ignore lint/performance/noImgElement: arbitrary org-uploaded icon + {branding.name} + ) : branding?.type === "cap" ? ( + + ) : null} +
+

+ {collectionName} +

+

+ This collection from {organizationName} is password protected. + Enter the password to continue. +

+
+
+ +
+
+ + setPassword(event.target.value)} + placeholder="Enter password" + className="w-full" + autoFocus + onKeyDown={(event) => { + if ( + event.key === "Enter" && + password.trim() && + !verifyPassword.isPending + ) + verifyPassword.mutate(); + }} + /> +
+ +
+
+
+
+ ); +} From 26445115f90920f8be971100a6a314b1514e5bd2 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:42 +0100 Subject: [PATCH 22/37] feat(web): add collection copy link button --- .../app/c/[id]/CollectionCopyLinkButton.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/web/app/c/[id]/CollectionCopyLinkButton.tsx diff --git a/apps/web/app/c/[id]/CollectionCopyLinkButton.tsx b/apps/web/app/c/[id]/CollectionCopyLinkButton.tsx new file mode 100644 index 0000000000..4399b87475 --- /dev/null +++ b/apps/web/app/c/[id]/CollectionCopyLinkButton.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Button } from "@cap/ui"; +import { faLink } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { toast } from "sonner"; + +export function CollectionCopyLinkButton() { + return ( + + ); +} From a50e4bf71350c3df20c6108a1528ac9337501deb Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:42 +0100 Subject: [PATCH 23/37] feat(web): add public collection view page --- apps/web/app/c/[id]/page.tsx | 463 +++++++++++++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 apps/web/app/c/[id]/page.tsx diff --git a/apps/web/app/c/[id]/page.tsx b/apps/web/app/c/[id]/page.tsx new file mode 100644 index 0000000000..b158f73045 --- /dev/null +++ b/apps/web/app/c/[id]/page.tsx @@ -0,0 +1,463 @@ +import { buttonVariants, Logo } from "@cap/ui"; +import { + faArrowLeft, + faArrowUpRightFromSquare, + faFolder, + faLock, + faVideo, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import type { ReactNode } from "react"; +import { PublicCapCard } from "@/components/PublicCapCard"; +import { + GRID_COLUMN_CLASS, + sanitizeCtaUrl, +} from "@/lib/public-collection-settings"; +import { + getPublicCollectionMetadata, + getPublicCollectionPageData, + type PublicCollection, + type PublicCollectionFolder, +} from "@/lib/public-collections"; +import { + getPublicCollectionHref, + parsePublicCollectionPage, +} from "@/lib/public-collections-policy"; +import type { SharePageBranding } from "@/lib/share-branding"; +import { CapPagination } from "../../(org)/dashboard/caps/components/CapPagination"; +import { CollectionCopyLinkButton } from "./CollectionCopyLinkButton"; +import { CollectionPasswordOverlay } from "./CollectionPasswordOverlay"; + +export async function generateMetadata( + props: PageProps<"/c/[id]">, +): Promise { + const params = await props.params; + const collection = await getPublicCollectionMetadata(params.id); + + if (!collection) notFound(); + + const title = collection.publicPage.title.trim() || collection.name; + const description = + collection.publicPage.subtitle.trim() || + collection.description?.trim() || + `View public videos in ${title} on Cap.`; + + return { + title: `${title} | Cap Collection`, + description, + robots: "noindex, nofollow", + }; +} + +function resolveBranding( + collection: PublicCollection, +): SharePageBranding | null { + const { logoMode } = collection.publicPage; + + if (logoMode === "none") return null; + + if (logoMode === "custom" && collection.collectionLogoUrl) { + return { + type: "custom", + imageUrl: collection.collectionLogoUrl, + name: collection.organizationName, + }; + } + + if (logoMode === "organization" && collection.organizationIconUrl) { + return { + type: "custom", + imageUrl: collection.organizationIconUrl, + name: collection.organizationName, + }; + } + + return { type: "cap" }; +} + +function BrandingMark({ + branding, + className, +}: { + branding: SharePageBranding; + className?: string; +}) { + if (branding.type === "custom") { + return ( + // biome-ignore lint/performance/noImgElement: arbitrary org-uploaded icon + {branding.name} + ); + } + return ; +} + +export default async function PublicCollectionPage( + props: PageProps<"/c/[id]">, +) { + const params = await props.params; + const searchParams = await props.searchParams; + const page = parsePublicCollectionPage(searchParams.page); + const data = await getPublicCollectionPageData(params.id, page); + + if (!data) notFound(); + + const branding = resolveBranding(data.collection); + + if (data.access.state === "email_restriction_login_required") { + return ( + + The owner has restricted access. Please{" "} + + sign in + {" "} + with an authorized email address to view. + + } + /> + ); + } + + if (data.access.state === "email_restriction_denied") { + return ( + + ); + } + + const isPasswordRequired = data.access.state === "password_required"; + const showCopyLink = + !isPasswordRequired && !data.collection.publicPage.hideCopyLink; + const showHeader = Boolean(branding) || showCopyLink; + + return ( +
+ + {data.viewerIsSignedIn && ( +
+
+ + + Back to dashboard + +
+
+ )} + {showHeader && ( +
+
+ {branding ? ( + branding.type === "cap" ? ( + + + + ) : ( + + ) + ) : ( + + )} + {showCopyLink && } +
+
+ )} + +
+ + + {isPasswordRequired ? ( + + ) : ( + + )} +
+ + +
+ ); +} + +function CollectionHero({ + collection, + videoCount, + folderCount, +}: { + collection: PublicCollection; + videoCount: number; + folderCount: number; +}) { + const { publicPage } = collection; + + const title = publicPage.title.trim() || collection.name; + const subtitle = + publicPage.subtitle.trim() || collection.description?.trim() || ""; + const ctaLabel = publicPage.ctaLabel.trim(); + const ctaUrl = sanitizeCtaUrl(publicPage.ctaUrl); + + const metaParts = [collection.organizationName]; + if (videoCount > 0) + metaParts.push(`${videoCount} ${videoCount === 1 ? "video" : "videos"}`); + if (folderCount > 0) + metaParts.push( + `${folderCount} ${folderCount === 1 ? "folder" : "folders"}`, + ); + + return ( +
+
+ {!publicPage.hideTitle && ( +

+ {title} +

+ )} + {subtitle && ( +

{subtitle}

+ )} +

{metaParts.join(" · ")}

+ {ctaLabel && ctaUrl && ( + + {ctaLabel} + + + )} +
+
+ ); +} + +function CollectionContent({ + data, +}: { + data: NonNullable>>; +}) { + const renderedAt = Date.now(); + const hasContent = data.folders.length > 0 || data.videos.length > 0; + const { layout, gridColumns } = data.collection.publicPage; + const containerClass = + layout === "list" + ? "flex flex-col gap-2" + : `grid grid-cols-1 gap-4 sm:grid-cols-2 ${GRID_COLUMN_CLASS[gridColumns]}`; + + return ( +
+ {data.folders.length > 0 && ( +
+ +
+ {data.folders.map((folder) => ( + + ))} +
+
+ )} + + {data.videos.length > 0 && ( +
+ +
+ {data.videos.map((video) => ( + + ))} +
+
+ )} + + {!hasContent && ( + + )} + + {data.totalPages > 1 && ( +
+ + getPublicCollectionHref(data.collection.id, targetPage) + } + /> +
+ )} +
+ ); +} + +function SectionHeading({ title, count }: { title: string; count: number }) { + return ( +
+

{title}

+ {count} +
+ ); +} + +function CollectionStateCard({ + icon, + title, + description, +}: { + icon: typeof faVideo; + title: string; + description: string; +}) { + return ( +
+
+
+ +
+

{title}

+

{description}

+
+
+ ); +} + +function PublicFolderCard({ + folder, + layout, +}: { + folder: PublicCollectionFolder; + layout: "grid" | "list"; +}) { + const meta = `${folder.videoCount} ${ + folder.videoCount === 1 ? "video" : "videos" + }`; + + if (layout === "list") { + return ( + +
+ +
+
+

+ {folder.name} +

+

{meta}

+
+ + ); + } + + return ( + +
+ +
+
+

+ {folder.name} +

+

{meta}

+
+ + ); +} + +function CollectionFooter({ showPoweredBy }: { showPoweredBy: boolean }) { + if (!showPoweredBy) return null; + + return ( + + ); +} + +function CollectionAccessView({ + branding, + title, + description, +}: { + branding: SharePageBranding | null; + title: string; + description: ReactNode; +}) { + return ( +
+ {branding ? ( + + ) : ( + + )} +

{title}

+

{description}

+
+ ); +} From 3a8ec4cb4eebdf259963d523c8fb53b71ee0287d Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:43 +0100 Subject: [PATCH 24/37] feat(web): add public collection error boundary --- apps/web/app/c/error.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 apps/web/app/c/error.tsx diff --git a/apps/web/app/c/error.tsx b/apps/web/app/c/error.tsx new file mode 100644 index 0000000000..e762d83b3b --- /dev/null +++ b/apps/web/app/c/error.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { Button } from "@cap/ui"; +import { useEffect } from "react"; + +export default function CollectionError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error("Public collection page error:", error); + }, [error]); + + return ( +
+

Something went wrong

+

+ This collection could not be loaded right now. +

+ +
+ ); +} From a6e1b739a98512d6f9addf235fbcb0de17adc013 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:43 +0100 Subject: [PATCH 25/37] feat(web): add PublicCapCard for collection grids --- apps/web/components/PublicCapCard.tsx | 161 ++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 apps/web/components/PublicCapCard.tsx diff --git a/apps/web/components/PublicCapCard.tsx b/apps/web/components/PublicCapCard.tsx new file mode 100644 index 0000000000..68882bebfb --- /dev/null +++ b/apps/web/components/PublicCapCard.tsx @@ -0,0 +1,161 @@ +"use client"; + +import type { VideoMetadata } from "@cap/database/types"; +import type { Video } from "@cap/web-domain"; +import { faComment, faLock, faSmile } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import clsx from "clsx"; +import Link from "next/link"; +import { useState } from "react"; +import { + type ImageLoadingStatus, + VideoThumbnail, +} from "@/components/VideoThumbnail"; + +type PublicCapCardVideo = { + id: string; + name: string; + createdAt: Date | string; + metadata: Pick | undefined; + duration: number | null; + totalComments: number; + totalReactions: number; + ownerName: string; + hasPassword: boolean; + hasActiveUpload: boolean; +}; + +const relativeTimeFormatter = new Intl.RelativeTimeFormat("en", { + numeric: "auto", +}); +const relativeTimeUnits: { + unit: Intl.RelativeTimeFormatUnit; + seconds: number; +}[] = [ + { unit: "year", seconds: 31536000 }, + { unit: "month", seconds: 2592000 }, + { unit: "week", seconds: 604800 }, + { unit: "day", seconds: 86400 }, + { unit: "hour", seconds: 3600 }, + { unit: "minute", seconds: 60 }, + { unit: "second", seconds: 1 }, +]; + +function formatRelativeDate(date: Date, now: number) { + const secondsFromNow = Math.round((date.getTime() - now) / 1000); + const absoluteSeconds = Math.abs(secondsFromNow); + const unit = relativeTimeUnits.find( + ({ seconds }) => absoluteSeconds >= seconds, + ) ?? { unit: "second", seconds: 1 }; + + return relativeTimeFormatter.format( + Math.round(secondsFromNow / unit.seconds), + unit.unit, + ); +} + +export function PublicCapCard({ + cap, + now, + layout = "grid", +}: { + cap: PublicCapCardVideo; + /** + * Server render timestamp, passed as a prop so the hydrated client render + * produces the same relative-date string as the server HTML. + */ + now: number; + layout?: "grid" | "list"; +}) { + const [imageStatus, setImageStatus] = useState("loading"); + const effectiveDate = cap.metadata?.customCreatedAt + ? new Date(cap.metadata.customCreatedAt) + : new Date(cap.createdAt); + const subtitle = `${cap.ownerName || "Cap"} · ${formatRelativeDate(effectiveDate, now)}`; + + const lockBadge = cap.hasPassword && ( +
+ +
+ ); + + const stats = ( +
+ + + {cap.totalComments} + + + + {cap.totalReactions} + +
+ ); + + if (layout === "list") { + return ( + +
+ + {lockBadge} +
+
+
+

+ {cap.name} +

+

+ {subtitle} +

+
+
{stats}
+
+ + ); + } + + return ( + +
+ + {lockBadge} +
+
+
+

+ {cap.name} +

+

{subtitle}

+
+
{stats}
+
+ + ); +} From 50884261a6facc16c4df28aebf627cfda5a8f9d4 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:43 +0100 Subject: [PATCH 26/37] feat(dashboard): add public collection field component --- .../_components/PublicCollectionField.tsx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 apps/web/app/(org)/dashboard/_components/PublicCollectionField.tsx diff --git a/apps/web/app/(org)/dashboard/_components/PublicCollectionField.tsx b/apps/web/app/(org)/dashboard/_components/PublicCollectionField.tsx new file mode 100644 index 0000000000..7c29fe3b63 --- /dev/null +++ b/apps/web/app/(org)/dashboard/_components/PublicCollectionField.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { Button, Switch } from "@cap/ui"; +import { + faCheck, + faCopy, + faGlobe, + faLock, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import clsx from "clsx"; +import { useCopyCollectionLink } from "@/lib/public-collection-client"; + +interface PublicCollectionFieldProps { + kind: "folder" | "space"; + enabled: boolean; + onChange: (enabled: boolean) => void; + isPro: boolean; + onUpgrade: () => void; + collectionId?: string; + disabled?: boolean; +} + +export const PublicCollectionField = ({ + kind, + enabled, + onChange, + isPro, + onUpgrade, + collectionId, + disabled, +}: PublicCollectionFieldProps) => { + const { copied, copy } = useCopyCollectionLink(collectionId); + + return ( +
+
+
+
+ +
+
+
+

+ Public collection link +

+ {!isPro && ( + + Pro + + )} +
+

+ Anyone with the link can browse public caps in this {kind}. +

+
+
+ { + if (checked && !isPro) { + onUpgrade(); + return; + } + onChange(checked); + }} + /> +
+ {enabled && collectionId && ( +
+ +
+ )} +
+ ); +}; From 25dbddd78a8b4326ecad4ff8d92eea41099de36f Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:43 +0100 Subject: [PATCH 27/37] feat(dashboard): add collection share dialog --- .../_components/CollectionShareDialog.tsx | 470 ++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100644 apps/web/app/(org)/dashboard/_components/CollectionShareDialog.tsx diff --git a/apps/web/app/(org)/dashboard/_components/CollectionShareDialog.tsx b/apps/web/app/(org)/dashboard/_components/CollectionShareDialog.tsx new file mode 100644 index 0000000000..afa3f19711 --- /dev/null +++ b/apps/web/app/(org)/dashboard/_components/CollectionShareDialog.tsx @@ -0,0 +1,470 @@ +"use client"; + +import { + Button, + buttonVariants, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Select, + Switch, +} from "@cap/ui"; +import { PublicCollection } from "@cap/web-domain"; +import { + faArrowUpRightFromSquare, + faCheck, + faCopy, + faGlobe, + faLock, + faUpload, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import clsx from "clsx"; +import { useEffect, useId, useRef, useState } from "react"; +import { useCopyCollectionLink } from "@/lib/public-collection-client"; +import { + PUBLIC_GRID_COLUMN_OPTIONS, + PUBLIC_LAYOUT_OPTIONS, + PUBLIC_LOGO_OPTIONS, +} from "@/lib/public-collection-settings"; + +type PublicPageSettings = PublicCollection.PublicPageSettings; +type PublicPageSettingsUpdate = PublicCollection.PublicPageSettingsUpdate; + +const { + PUBLIC_PAGE_TITLE_MAX_LENGTH, + PUBLIC_PAGE_SUBTITLE_MAX_LENGTH, + PUBLIC_PAGE_CTA_LABEL_MAX_LENGTH, + PUBLIC_PAGE_CTA_URL_MAX_LENGTH, +} = PublicCollection; + +const gridColumnSelectOptions = PUBLIC_GRID_COLUMN_OPTIONS.map((option) => ({ + value: String(option.value), + label: option.label, +})); + +interface CollectionShareDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + kind: "folder" | "space"; + collectionId: string; + isPublic: boolean; + isPro: boolean; + isPending: boolean; + settings: Required; + onTogglePublic: (next: boolean) => void; + onUpdateSettings: (patch: PublicPageSettingsUpdate) => void; + onUploadLogo: (file: File) => void; + onRemoveLogo: () => void; + isUploadingLogo: boolean; +} + +export const CollectionShareDialog = ({ + open, + onOpenChange, + kind, + collectionId, + isPublic, + isPro, + isPending, + settings, + onTogglePublic, + onUpdateSettings, + onUploadLogo, + onRemoveLogo, + isUploadingLogo, +}: CollectionShareDialogProps) => { + const { url, copied, copy } = useCopyCollectionLink(collectionId); + const displayUrl = url.replace(/^https?:\/\//, ""); + + return ( + + + } + description={ + isPublic + ? `Anyone with the link can browse the public caps in this ${kind}.` + : `Publish this ${kind} as a clean, browsable page you can share with anyone.` + } + > + Share this {kind} + + +
+
+
+
+ +
+
+
+

+ Anyone with the link +

+ {!isPro && ( + + Pro + + )} +
+

+ {isPublic + ? "Public — anyone with the link can view" + : "Private — only members can view"} +

+
+
+ +
+ + {isPublic && ( + <> +
+ e.currentTarget.select()} + className="pr-11 font-mono text-xs" + /> + +
+ + + onUpdateSettings({ title: value })} + /> + onUpdateSettings({ subtitle: value })} + /> +
+ Logo + + onUpdateSettings({ + layout: value as PublicPageSettings["layout"], + }) + } + /> + + + {settings.layout === "grid" && ( + + { + const file = e.target.files?.[0]; + if (file) onUpload(file); + e.currentTarget.value = ""; + }} + /> + + {hasLogo && ( + + )} +
+

PNG, JPEG, SVG or WebP, up to 1MB.

+
+ ); +} + +function FieldGroup({ + title, + description, + children, +}: { + title: string; + description?: string; + children: React.ReactNode; +}) { + return ( +
+

+ {title} +

+ {description && ( +

{description}

+ )} +
{children}
+
+ ); +} + +function TextField({ + label, + value, + placeholder, + maxLength, + disabled, + onCommit, +}: { + label: string; + value: string; + placeholder?: string; + maxLength?: number; + disabled?: boolean; + onCommit: (value: string) => void; +}) { + const id = useId(); + const [draft, setDraft] = useState(value); + + useEffect(() => setDraft(value), [value]); + + const commit = () => { + const next = draft.trim(); + setDraft(next); + if (next === value) return; + onCommit(next); + }; + + return ( +
+ + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> +
+ ); +} + +function SettingRow({ + label, + description, + children, +}: { + label: string; + description?: string; + children: React.ReactNode; +}) { + return ( +
+
+

{label}

+ {description &&

{description}

} +
+ {children} +
+ ); +} From 9e340b2d781d5135a9c7c3656cccd2e84e8791b3 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:43 +0100 Subject: [PATCH 28/37] feat(dashboard): add collection share control --- .../_components/CollectionShareControl.tsx | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 apps/web/app/(org)/dashboard/_components/CollectionShareControl.tsx diff --git a/apps/web/app/(org)/dashboard/_components/CollectionShareControl.tsx b/apps/web/app/(org)/dashboard/_components/CollectionShareControl.tsx new file mode 100644 index 0000000000..c75ed123a7 --- /dev/null +++ b/apps/web/app/(org)/dashboard/_components/CollectionShareControl.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { Button } from "@cap/ui"; +import { type Folder, PublicCollection } from "@cap/web-domain"; +import { + faCheck, + faCopy, + faGlobe, + faSliders, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { setCollectionLogo } from "@/actions/collections/logo"; +import { setSpaceCollectionVisibility } from "@/actions/collections/visibility"; +import { Tooltip } from "@/components/Tooltip"; +import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime"; +import { useCopyCollectionLink } from "@/lib/public-collection-client"; +import { useDashboardContext } from "../Contexts"; +import { CollectionShareDialog } from "./CollectionShareDialog"; + +type PublicPageSettings = PublicCollection.PublicPageSettings; +type PublicPageSettingsUpdate = PublicCollection.PublicPageSettingsUpdate; + +interface CollectionShareControlProps { + kind: "folder" | "space"; + collectionId: string; + isPublic: boolean; + canManage: boolean; + isPro: boolean; + settings: PublicPageSettings | null; +} + +export const CollectionShareControl = ({ + kind, + collectionId, + isPublic, + canManage, + isPro, + settings, +}: CollectionShareControlProps) => { + const router = useRouter(); + const rpc = useRpcClient(); + const { setUpgradeModalOpen } = useDashboardContext(); + const { url, copied, copy } = useCopyCollectionLink(collectionId); + const displayUrl = url.replace(/^https?:\/\//, ""); + + const [pub, setPub] = useState(isPublic); + const [draft, setDraft] = useState(() => + PublicCollection.resolvePublicPageSettings(settings), + ); + const [open, setOpen] = useState(false); + + useEffect(() => setPub(isPublic), [isPublic]); + useEffect( + () => setDraft(PublicCollection.resolvePublicPageSettings(settings)), + [settings], + ); + + const onError = (error: unknown) => { + setPub(isPublic); + setDraft(PublicCollection.resolvePublicPageSettings(settings)); + toast.error( + error instanceof Error ? error.message : "Something went wrong", + ); + }; + const onSuccess = () => router.refresh(); + + const folderMutation = useEffectMutation({ + mutationFn: (data: { + public?: boolean; + settings?: PublicPageSettingsUpdate; + }) => + rpc.FolderUpdate({ + id: collectionId as Folder.FolderId, + public: data.public, + publicPage: data.settings, + }), + onSuccess, + onError, + }); + + const spaceMutation = useMutation({ + mutationFn: async (data: { + public?: boolean; + settings?: PublicPageSettingsUpdate; + }) => { + const result = await setSpaceCollectionVisibility({ + spaceId: collectionId, + public: data.public, + settings: data.settings, + }); + if (!result.success) throw new Error(result.error); + }, + onSuccess, + onError, + }); + + const persist = (data: { + public?: boolean; + settings?: PublicPageSettingsUpdate; + }) => + kind === "folder" + ? folderMutation.mutate(data) + : spaceMutation.mutate(data); + + const logoMutation = useMutation({ + mutationFn: async (file: File | null) => { + const formData = new FormData(); + formData.append("collectionId", collectionId); + formData.append("kind", kind); + if (file) formData.append("logo", file); + else formData.append("remove", "true"); + + const result = await setCollectionLogo(formData); + if (!result.success) throw new Error(result.error); + }, + onSuccess: (_data, file) => { + router.refresh(); + toast.success(file ? "Logo updated" : "Logo removed"); + }, + onError: (error) => + toast.error( + error instanceof Error ? error.message : "Failed to update logo", + ), + }); + + const isPending = + folderMutation.isPending || + spaceMutation.isPending || + logoMutation.isPending; + + const handleTogglePublic = (next: boolean) => { + if (next) { + if (!isPro) { + setOpen(false); + setUpgradeModalOpen(true); + return; + } + setPub(true); + persist({ public: true }); + return; + } + setPub(false); + persist({ public: false }); + }; + + // Optimistically merge into the local draft but persist only the patch — + // the server merges it into the stored settings, so a concurrent logo + // upload (or another in-flight patch) is never overwritten. + const updateSettings = (patch: PublicPageSettingsUpdate) => { + setDraft((prev) => ({ ...prev, ...patch })); + persist({ settings: patch }); + }; + + if (!pub && !canManage) return null; + + const dialog = canManage ? ( + logoMutation.mutate(file)} + onRemoveLogo={() => logoMutation.mutate(null)} + isUploadingLogo={logoMutation.isPending} + /> + ) : null; + + if (pub) { + return ( +
+ + + + {canManage && ( + + )} + {dialog} +
+ ); + } + + return ( + <> + + {dialog} + + ); +}; From 60bde1040c8713be4b081050d766056f2c8300bf Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:43 +0100 Subject: [PATCH 29/37] feat(dashboard): wire public collection into space dialogs --- apps/web/actions/organization/create-space.ts | 16 + apps/web/actions/organization/update-space.ts | 39 ++- .../_components/Navbar/SpaceDialog.tsx | 309 ++++++++++-------- 3 files changed, 227 insertions(+), 137 deletions(-) diff --git a/apps/web/actions/organization/create-space.ts b/apps/web/actions/organization/create-space.ts index 032e9e4c4f..40b2107e47 100644 --- a/apps/web/actions/organization/create-space.ts +++ b/apps/web/actions/organization/create-space.ts @@ -8,6 +8,7 @@ import { spaceMembers, spaces } from "@cap/database/schema"; import { userIsPro } from "@cap/utils"; import { type ImageUpload, + Organisation, Space, SpaceMemberId, type SpaceMemberRole, @@ -15,6 +16,7 @@ import { } from "@cap/web-domain"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import { isOrganizationOwnerPro } from "@/lib/org-pro"; import { getSpaceSettingsFromFormData, hasProSpaceSettingsEnabled, @@ -45,6 +47,7 @@ export async function createSpace( const name = formData.get("name") as string; const passwordEnabled = formData.get("passwordEnabled") === "true"; const password = formData.get("password") as string | null; + const publicEnabled = formData.get("public") === "true"; const settings = getSpaceSettingsFromFormData(formData); const canUseProFeatures = userIsPro(user); @@ -76,6 +79,18 @@ export async function createSpace( }; } + if ( + publicEnabled && + !(await isOrganizationOwnerPro( + Organisation.OrganisationId.make(user.activeOrganizationId), + )) + ) { + return { + success: false, + error: "Upgrade to Cap Pro to create a public collection link", + }; + } + const existingSpace = await db() .select({ id: spaces.id }) .from(spaces) @@ -110,6 +125,7 @@ export async function createSpace( iconUrl: null, settings, password: hashedPassword, + public: publicEnabled, }); const memberUserIds: string[] = []; diff --git a/apps/web/actions/organization/update-space.ts b/apps/web/actions/organization/update-space.ts index 9dd84e615e..b1caba2a6b 100644 --- a/apps/web/actions/organization/update-space.ts +++ b/apps/web/actions/organization/update-space.ts @@ -13,9 +13,10 @@ import { type SpaceMemberRole, type User, } from "@cap/web-domain"; -import { eq } from "drizzle-orm"; +import { eq, type SQL, sql } from "drizzle-orm"; import { Effect, Option } from "effect"; import { revalidatePath } from "next/cache"; +import { isOrganizationOwnerPro } from "@/lib/org-pro"; import { normalizeSpaceRole } from "@/lib/permissions/roles"; import { runPromise } from "@/lib/server"; import { requireSpaceManager } from "./space-authorization"; @@ -39,12 +40,17 @@ export async function updateSpace(formData: FormData) { | "remove" | null; const password = formData.get("password") as string | null; + // Only touch the public flag when the form actually submitted it — callers + // that omit the field must not silently un-publish the space. + const publicField = formData.get("public"); + const publicEnabled = publicField === "true"; const [space] = await db() .select({ createdById: spaces.createdById, organizationId: spaces.organizationId, settings: spaces.settings, + public: spaces.public, }) .from(spaces) .where(eq(spaces.id, id)) @@ -59,16 +65,40 @@ export async function updateSpace(formData: FormData) { return { success: false, error: "Unauthorized" }; } + // Publishing is gated on the org owner's plan, but a downgraded org can + // always un-publish — so only the false→true transition requires Pro. + if ( + publicEnabled && + !space.public && + !(await isOrganizationOwnerPro(space.organizationId)) + ) { + return { + success: false, + error: "Upgrade to Cap Pro to create a public collection link", + }; + } + const submittedSettings = getSpaceSettingsFromFormData(formData); const canUseProFeatures = userIsPro(user); - const settings = canUseProFeatures + const viewerSettings = canUseProFeatures ? submittedSettings : preserveProSpaceSettings(submittedSettings, space.settings); + // Atomic per-key merge: the form submits every viewer-settings key + // explicitly, while settings.publicPage (managed by the visibility/logo + // actions, possibly concurrently) is left untouched instead of being + // rewritten from a stale snapshot. const spaceUpdate: { name: string; - settings: ReturnType; + settings: SQL; + public?: boolean; password?: string | null; - } = { name, settings }; + } = { + name, + settings: sql`JSON_MERGE_PATCH(COALESCE(${spaces.settings}, '{}'), CAST(${JSON.stringify( + viewerSettings, + )} AS JSON))`, + }; + if (publicField !== null) spaceUpdate.public = publicEnabled; if (passwordAction === "set") { if (!canUseProFeatures) { @@ -144,5 +174,6 @@ export async function updateSpace(formData: FormData) { revalidatePath("/dashboard"); revalidatePath("/dashboard/caps"); revalidatePath(`/dashboard/spaces/${id}`); + revalidatePath(`/c/${id}`); return { success: true }; } diff --git a/apps/web/app/(org)/dashboard/_components/Navbar/SpaceDialog.tsx b/apps/web/app/(org)/dashboard/_components/Navbar/SpaceDialog.tsx index 117a6a2bcb..ed5f51edac 100644 --- a/apps/web/app/(org)/dashboard/_components/Navbar/SpaceDialog.tsx +++ b/apps/web/app/(org)/dashboard/_components/Navbar/SpaceDialog.tsx @@ -16,11 +16,7 @@ import { Switch, } from "@cap/ui"; import type { ImageUpload } from "@cap/web-domain"; -import { - faGear, - faLayerGroup, - faLock, -} from "@fortawesome/free-solid-svg-icons"; +import { faLayerGroup, faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; @@ -34,6 +30,7 @@ import { FileInput } from "@/components/FileInput"; import { useDashboardContext } from "../../Contexts"; import type { OrganizationSettings } from "../../dashboard-data"; import { MemberSelect } from "../../spaces/[spaceId]/components/MemberSelect"; +import { PublicCollectionField } from "../PublicCollectionField"; import { createSpace } from "./server"; interface SpaceDialogProps { @@ -47,6 +44,7 @@ interface SpaceDialogProps { iconUrl?: ImageUpload.ImageUrl; settings?: OrganizationSettings | null; hasPassword?: boolean; + public?: boolean; } | null; onSpaceUpdated?: () => void; } @@ -68,20 +66,20 @@ const SpaceDialog = ({ return ( !open && onClose()}> - + } description={ edit - ? "Edit your space details" - : "A new space for your team to collaborate" + ? "Manage details, sharing and viewer permissions." + : "Set up a space for your team to collaborate." } > {edit ? "Edit Space" : "Create New Space"} -
+
= (props) => { const [passwordEnabled, setPasswordEnabled] = useState( Boolean(space?.hasPassword), ); + const [publicEnabled, setPublicEnabled] = useState(Boolean(space?.public)); const [passwordValue, setPasswordValue] = useState(""); const iconInputId = useId(); useEffect(() => { setSettings({ ...defaultSettings, ...space?.settings }); setPasswordEnabled(Boolean(space?.hasPassword)); + setPublicEnabled(Boolean(space?.public)); setPasswordValue(""); }, [space]); @@ -308,6 +309,7 @@ export const NewSpaceForm: React.FC = (props) => { } formData.append("passwordEnabled", String(passwordEnabled)); + formData.append("public", String(publicEnabled)); if (passwordEnabled && passwordValue.trim()) { formData.append("password", passwordValue.trim()); @@ -376,117 +378,158 @@ export const NewSpaceForm: React.FC = (props) => { } })} > -
- ( - - { - field.onChange(e); - props.onNameChange?.(e.target.value); - }} +
+ {/* Details */} +
+ +
+
+ ( + + { + field.onChange(e); + props.onNameChange?.(e.target.value); + }} + /> + + )} /> - - )} - /> - {/* Space Members Input */} -
- - - Add team members to this space. - -
- { - return ( - - +
+ + + Custom logo or icon (max 1MB). + +
+ (field.value ?? []).includes(m.user.id)) - .map((m) => ({ - value: m.user.id, - label: m.user.name || m.user.email, - image: m.user.image ?? undefined, - }))} - onSelect={(selected) => - field.onChange(selected.map((opt) => opt.value)) - } - /> -
- ); - }} - /> - -
-
-
-
-
-
-

- Require password -

-

- All caps in this space require this password -

+
+ +
+
+ + + Add team members to this space. +
+ ( + + + (field.value ?? []).includes(m.user.id), + ) + .map((m) => ({ + value: m.user.id, + label: m.user.name || m.user.email, + image: m.user.image ?? undefined, + }))} + onSelect={(selected) => + field.onChange(selected.map((opt) => opt.value)) + } + /> + + )} + />
-
- {passwordEnabled && ( -
- setPasswordValue(e.target.value)} - placeholder={ - space?.hasPassword ? "Enter new password" : "Set a password" - } - /> - {space?.hasPassword && !passwordValue && ( -

- Leave blank to keep existing password -

+
+ + {/* Sharing */} +
+ +
+ setUpgradeModalOpen(true)} + collectionId={edit && space?.id ? space.id : undefined} + /> + +
+
+
+
+ +
+
+

+ Require password +

+

+ Protect every cap in this space +

+
+
+ +
+ {passwordEnabled && ( +
+ setPasswordValue(e.target.value)} + placeholder={ + space?.hasPassword + ? "Enter new password" + : "Set a password" + } + /> + {space?.hasPassword && !passwordValue && ( +

+ Leave blank to keep existing password +

+ )} +
)}
- )} -
- -
-
-
- -
-
-

Viewer rules

-

- These apply to every cap in this space -

-
-
+
+ + {/* Viewer permissions */} +
+ +
{settingOptions.map((option) => { const disabled = (option.pro && !user?.isPro) || @@ -497,15 +540,15 @@ export const NewSpaceForm: React.FC = (props) => { return (
-
+

{option.label}

{option.pro && ( -

+ Pro -

+ )}

@@ -521,30 +564,30 @@ export const NewSpaceForm: React.FC = (props) => { ); })}

-
- -
- - - Upload a custom logo or icon for your space (max 1MB). - -
- -
- -
+
); }; +function SectionLabel({ + title, + description, +}: { + title: string; + description?: string; +}) { + return ( +
+

+ {title} +

+ {description && ( +

{description}

+ )} +
+ ); +} + export default SpaceDialog; From ecad59c59b62531e46a2562739efbb61bc4ae993 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:43 +0100 Subject: [PATCH 30/37] feat(dashboard): support public folders in caps views --- .../caps/components/CapPagination.tsx | 16 +- .../dashboard/caps/components/Folder.tsx | 263 ++++++++++-------- .../caps/components/FoldersDropdown.tsx | 151 +++++----- .../caps/components/NewFolderDialog.tsx | 38 ++- apps/web/app/(org)/dashboard/caps/page.tsx | 1 + 5 files changed, 260 insertions(+), 209 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/CapPagination.tsx b/apps/web/app/(org)/dashboard/caps/components/CapPagination.tsx index 183f594f58..10ccca102c 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapPagination.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapPagination.tsx @@ -11,11 +11,13 @@ import { interface CapPaginationProps { currentPage: number; totalPages: number; + hrefForPage?: (page: number) => string; } export const CapPagination: React.FC = ({ currentPage, totalPages, + hrefForPage = (page) => `/dashboard/caps?page=${page}`, }) => { return ( @@ -24,14 +26,14 @@ export const CapPagination: React.FC = ({ )} 1 @@ -41,7 +43,7 @@ export const CapPagination: React.FC = ({ {currentPage} @@ -52,7 +54,7 @@ export const CapPagination: React.FC = ({ {currentPage + 1} @@ -63,9 +65,9 @@ export const CapPagination: React.FC = ({ diff --git a/apps/web/app/(org)/dashboard/caps/components/Folder.tsx b/apps/web/app/(org)/dashboard/caps/components/Folder.tsx index 6bc22a260d..03037800fe 100644 --- a/apps/web/app/(org)/dashboard/caps/components/Folder.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/Folder.tsx @@ -1,6 +1,6 @@ "use client"; import type { Folder, Space } from "@cap/web-domain"; -import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faGlobe, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Fit, Layout, useRive } from "@rive-app/react-canvas"; import clsx from "clsx"; @@ -10,6 +10,7 @@ import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { moveVideoToFolder } from "@/actions/folders/moveVideoToFolder"; import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime"; +import { useCopyCollectionLink } from "@/lib/public-collection-client"; import { ConfirmationDialog } from "../../_components/ConfirmationDialog"; import { useDashboardContext, useTheme } from "../../Contexts"; import { registerDropTarget } from "../../folder/[id]/components/ClientCapCard"; @@ -19,6 +20,7 @@ export type FolderDataType = { name: string; id: Folder.FolderId; color: "normal" | "blue" | "red" | "yellow"; + public: boolean; videoCount: number; spaceId?: Space.SpaceIdOrOrganisationId | null; parentId: Folder.FolderId | null; @@ -27,6 +29,7 @@ export type FolderDataType = { const FolderCard = ({ name, color, + public: isPublic, id, parentId, videoCount, @@ -37,19 +40,22 @@ const FolderCard = ({ const [confirmDeleteFolderOpen, setConfirmDeleteFolderOpen] = useState(false); const [isRenaming, setIsRenaming] = useState(false); const [updateName, setUpdateName] = useState(name); + const [publicEnabled, setPublicEnabled] = useState(isPublic); const nameRef = useRef(null); - const folderRef = useRef(null); + const folderRef = useRef(null); const [isDragOver, setIsDragOver] = useState(false); const [isMovingVideo, setIsMovingVideo] = useState(false); - const { activeOrganization } = useDashboardContext(); + const { activeOrganization, setUpgradeModalOpen } = useDashboardContext(); + const ownerIsPro = Boolean(activeOrganization?.ownerIsPro); + const folderHref = spaceId + ? `/dashboard/spaces/${spaceId}/folder/${id}` + : `/dashboard/folder/${id}`; - // Use a ref to track drag state to avoid re-renders during animation const dragStateRef = useRef({ isDragging: false, isAnimating: false, }); - // Add a debounce timer ref to prevent animation stuttering const animationTimerRef = useRef(null); const artboard = @@ -70,6 +76,7 @@ const FolderCard = ({ }); const rpc = useRpcClient(); + const { copy: copyPublicLink } = useCopyCollectionLink(id); const deleteFolder = useEffectMutation({ mutationFn: (id: Folder.FolderId) => rpc.FolderDelete(id), @@ -86,10 +93,13 @@ const FolderCard = ({ const updateFolder = useEffectMutation({ mutationFn: (data: Folder.FolderUpdate) => rpc.FolderUpdate(data), onSuccess: () => { - toast.success("Folder name updated successfully"); + toast.success("Folder updated successfully"); router.refresh(); }, - onError: () => toast.error("Failed to update folder name"), + onError: () => { + setPublicEnabled(isPublic); + toast.error("Failed to update folder"); + }, onSettled: () => setIsRenaming(false), }); @@ -100,13 +110,15 @@ const FolderCard = ({ } }, [isRenaming]); - // Register this folder as a drop target for mobile drag and drop + useEffect(() => { + setPublicEnabled(isPublic); + }, [isPublic]); + useEffect(() => { if (!folderRef.current) return; const unregister = registerDropTarget( folderRef.current, - // onDrop handler async (data) => { if (!data || !data.id) return; @@ -193,7 +205,7 @@ const FolderCard = ({ spaceId, ]); - const handleDragOver = (e: React.DragEvent) => { + const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -218,7 +230,7 @@ const FolderCard = ({ } }; - const handleDragLeave = (e: React.DragEvent) => { + const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -240,7 +252,7 @@ const FolderCard = ({ } }; - const handleDrop = async (e: React.DragEvent) => { + const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); @@ -277,135 +289,142 @@ const FolderCard = ({ }; return ( - -
{ - // Don't play mouse animations during drag operations - if (dragStateRef.current.isDragging) return; - if (!rive) return; - - // Clear any pending animation timer - if (animationTimerRef.current) { - clearTimeout(animationTimerRef.current); - animationTimerRef.current = null; - } +
{ + if (dragStateRef.current.isDragging) return; + if (!rive) return; - // Use a small delay to prevent stuttering when moving the mouse quickly - animationTimerRef.current = setTimeout(() => { - rive.stop(); - rive.play("folder-open"); - }, 50); - }} - onMouseLeave={() => { - // Don't play mouse animations during drag operations - if (dragStateRef.current.isDragging) return; - if (!rive) return; - - // Clear any pending animation timer - if (animationTimerRef.current) { - clearTimeout(animationTimerRef.current); - animationTimerRef.current = null; - } + if (animationTimerRef.current) { + clearTimeout(animationTimerRef.current); + animationTimerRef.current = null; + } - // Use a small delay to prevent stuttering when moving the mouse quickly - animationTimerRef.current = setTimeout(() => { - rive.stop(); - rive.play("folder-close"); - }, 50); - }} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - className={clsx( - "flex justify-between items-center px-4 py-4 w-full h-auto rounded-lg border transition-all duration-200 cursor-pointer bg-gray-3 hover:bg-gray-4 hover:border-gray-6", - isDragOver ? "border-blue-10 bg-gray-4" : "border-gray-5", - isMovingVideo && "opacity-70", - )} - > -
+ animationTimerRef.current = setTimeout(() => { + rive.stop(); + rive.play("folder-open"); + }, 50); + }} + onMouseLeave={() => { + if (dragStateRef.current.isDragging) return; + if (!rive) return; + + if (animationTimerRef.current) { + clearTimeout(animationTimerRef.current); + animationTimerRef.current = null; + } + + animationTimerRef.current = setTimeout(() => { + rive.stop(); + rive.play("folder-close"); + }, 50); + }} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + className={clsx( + "flex justify-between items-center px-4 py-4 w-full h-auto min-w-0 rounded-lg border transition-all duration-200 bg-gray-3 hover:bg-gray-4 hover:border-gray-6", + isDragOver ? "border-blue-10 bg-gray-4" : "border-gray-5", + isMovingVideo && "opacity-70", + )} + > +
+ -
{ - e.preventDefault(); - e.stopPropagation(); - }} - className="flex flex-col justify-center h-10" - > - {isRenaming ? ( -