From b668f2ccf5847a155528c5acd77f82398b91e300 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:40:12 +0100 Subject: [PATCH 1/4] perf(videos): decide view and owner policies on the loaded row --- apps/web/__tests__/unit/videos-policy.test.ts | 169 ++++++++++++- .../web-backend/src/Videos/VideosPolicy.ts | 230 ++++++++++++------ packages/web-backend/src/Videos/index.ts | 52 ++-- packages/web-backend/src/index.ts | 3 + packages/web-domain/src/Video.ts | 2 +- 5 files changed, 340 insertions(+), 116 deletions(-) diff --git a/apps/web/__tests__/unit/videos-policy.test.ts b/apps/web/__tests__/unit/videos-policy.test.ts index 8929c3ab5f9..7c218ac4696 100644 --- a/apps/web/__tests__/unit/videos-policy.test.ts +++ b/apps/web/__tests__/unit/videos-policy.test.ts @@ -1,4 +1,8 @@ -import { buildCanView, type VideosPolicyDeps } from "@cap/web-backend"; +import { + buildCanView, + buildCanViewLoaded, + type VideosPolicyDeps, +} from "@cap/web-backend"; import { CurrentUser, type Organisation, @@ -573,3 +577,166 @@ describe("VideosPolicy.canView", () => { }); }); }); + +function runCanViewLoaded( + deps: VideosPolicyDeps, + video: Video.Video, + password: Option.Option, + user: Option.Option, + attachedPasswords: ReadonlyArray = [], +): Promise<"allowed" | "denied" | "password"> { + const policy = buildCanViewLoaded(deps, video, password); + + const program = Effect.zipRight( + policy, + Effect.succeed("allowed" as const), + ).pipe( + Effect.catchTag("PolicyDenied", () => Effect.succeed("denied" as const)), + Effect.catchTag("VerifyVideoPasswordError", () => + Effect.succeed("password" as const), + ), + ); + + const withPassword = + attachedPasswords.length === 0 + ? program + : Effect.provideService(program, Video.VideoPasswordAttachment, { + passwords: attachedPasswords, + }); + + const withUser = user.pipe( + Option.match({ + onNone: () => withPassword, + onSome: (u) => Effect.provideService(withPassword, CurrentUser, u), + }), + ); + + return Effect.runPromise(withUser); +} + +describe("VideosPolicy.canViewLoaded", () => { + const scenarios: Array<{ + name: string; + config: Parameters[0] & { video: Video.Video }; + user: Option.Option; + attachedPasswords?: string[]; + }> = [ + { + name: "owner on a private restricted video", + config: { + video: makeVideo({ public: false }), + allowedEmailDomain: Option.some("restricted.com"), + }, + user: makeUser("owner@anything.com", TEST_OWNER_ID), + }, + { + name: "anonymous viewer on a public video", + config: { video: makeVideo() }, + user: noUser, + }, + { + name: "anonymous viewer on a private video", + config: { video: makeVideo({ public: false }) }, + user: noUser, + }, + { + name: "org member on a private video", + config: { video: makeVideo({ public: false }), orgMembership: true }, + user: makeUser("member@company.com"), + }, + { + name: "space member with an email restriction that does not match", + config: { + video: makeVideo({ public: false }), + spaceMembership: true, + allowedEmailDomain: Option.some("company.com"), + }, + user: makeUser("bob@gmail.com"), + }, + { + name: "anonymous viewer with a video password and no attachment", + config: { video: makeVideo(), password: Option.some("video-hash") }, + user: noUser, + }, + { + name: "anonymous viewer with a matching video password", + config: { video: makeVideo(), password: Option.some("video-hash") }, + user: noUser, + attachedPasswords: ["video-hash"], + }, + { + name: "anonymous viewer with a matching inherited space password", + config: { + video: makeVideo(), + spacePasswords: ["space-one-hash", "space-two-hash"], + }, + user: noUser, + attachedPasswords: ["space-two-hash"], + }, + { + name: "logged-in viewer outside the allowed email domain", + config: { + video: makeVideo(), + allowedEmailDomain: Option.some("company.com"), + }, + user: makeUser("outsider@gmail.com"), + }, + { + name: "anonymous viewer with an email restriction", + config: { + video: makeVideo(), + allowedEmailDomain: Option.some("company.com"), + }, + user: noUser, + }, + { + name: "logged-in viewer inside the allowed email domain", + config: { + video: makeVideo(), + allowedEmailDomain: Option.some("company.com"), + }, + user: makeUser("employee@company.com"), + }, + ]; + + for (const scenario of scenarios) { + it(`matches canView for ${scenario.name}`, async () => { + const deps = makeDeps(scenario.config); + const password = scenario.config.password ?? Option.none(); + + expect( + await runCanViewLoaded( + deps, + scenario.config.video, + password, + scenario.user, + scenario.attachedPasswords, + ), + ).toBe(await runCanView(deps, scenario.user, scenario.attachedPasswords)); + }); + } + + it("never reads the video row again", async () => { + let getByIdCalls = 0; + const deps = makeDeps({ video: makeVideo({ public: false }) }); + const countingDeps: VideosPolicyDeps = { + ...deps, + repo: { + getById: (id) => { + getByIdCalls += 1; + return deps.repo.getById(id); + }, + }, + }; + + expect( + await runCanViewLoaded( + countingDeps, + makeVideo({ public: false }), + Option.none(), + makeUser("member@company.com"), + ), + ).toBe("denied"); + expect(getByIdCalls).toBe(0); + }); +}); diff --git a/packages/web-backend/src/Videos/VideosPolicy.ts b/packages/web-backend/src/Videos/VideosPolicy.ts index 858483b61db..d99f0c74108 100644 --- a/packages/web-backend/src/Videos/VideosPolicy.ts +++ b/packages/web-backend/src/Videos/VideosPolicy.ts @@ -1,5 +1,6 @@ import { isEmailAllowedByRestriction } from "@cap/utils"; import { + type CurrentUser, type DatabaseError, type Organisation, Policy, @@ -14,14 +15,18 @@ import { SpacesRepo } from "../Spaces/SpacesRepo.ts"; import { collectPasswordHashes } from "./EffectiveVideoRules.ts"; import { VideosRepo } from "./VideosRepo.ts"; +export type LoadedVideo = readonly [Video.Video, Option.Option]; + +export type ViewableVideo = Pick< + Video.Video, + "id" | "ownerId" | "orgId" | "public" +>; + export type VideosPolicyDeps = { repo: { getById: ( id: Video.VideoId, - ) => Effect.Effect< - Option.Option]>, - DatabaseError - >; + ) => Effect.Effect, DatabaseError>; }; orgsRepo: { membershipForVideo: ( @@ -43,97 +48,121 @@ export type VideosPolicyDeps = { }; }; -export function buildCanView( - { repo, orgsRepo, spacesRepo }: VideosPolicyDeps, - videoId: Video.VideoId, -) { - return Policy.publicPolicy( - Effect.fn(function* (user) { - const res = yield* repo.getById(videoId); - - if (Option.isNone(res)) { - yield* Effect.log("Video not found. Access granted."); - return true; - } - - const [video, password] = res.value; - - if (Option.isSome(user)) { - const userId = user.value.id; - if (userId === video.ownerId) return true; - } - - const spacePasswords = yield* spacesRepo.passwordsForVideo(video.id); - const passwordHashes = collectPasswordHashes({ - videoPassword: Option.getOrNull(password), - spacePasswords: [...spacePasswords], - }); - - if (Option.isSome(user)) { - const userId = user.value.id; - const [videoOrgShareMembership, videoSpaceShareMembership] = - yield* Effect.all([ +export type ViewDecisionDeps = Pick< + VideosPolicyDeps, + "orgsRepo" | "spacesRepo" +>; + +const decideCanView = ( + { orgsRepo, spacesRepo }: ViewDecisionDeps, + user: Option.Option, + video: ViewableVideo, + password: Option.Option, +) => + Effect.gen(function* () { + if (Option.isSome(user)) { + const userId = user.value.id; + if (userId === video.ownerId) return true; + } + + const spacePasswords = yield* spacesRepo.passwordsForVideo(video.id); + const passwordHashes = collectPasswordHashes({ + videoPassword: Option.getOrNull(password), + spacePasswords: [...spacePasswords], + }); + + if (Option.isSome(user)) { + const userId = user.value.id; + const [videoOrgShareMembership, videoSpaceShareMembership] = + yield* Effect.all( + [ orgsRepo .membershipForVideo(userId, video.id) .pipe(Effect.map(Array.get(0))), spacesRepo.membershipForVideo(userId, video.id), - ]); - - if ( - Option.isSome(videoOrgShareMembership) || - Option.isSome(videoSpaceShareMembership) - ) { - yield* Effect.log( - "Explicit org/space membership found. Access granted.", - ); - yield* Video.verifyPasswordCandidates(video, passwordHashes); - return true; - } - } + ], + { concurrency: "unbounded" }, + ); - if (!video.public) { + if ( + Option.isSome(videoOrgShareMembership) || + Option.isSome(videoSpaceShareMembership) + ) { yield* Effect.log( - "Video is private and user has no explicit access. Access denied.", + "Explicit org/space membership found. Access granted.", ); - return false; + yield* Video.verifyPasswordCandidates(video, passwordHashes); + return true; } - - const allowedEmails = yield* orgsRepo.allowedEmailDomain(video.orgId); - const restriction = Option.isSome(allowedEmails) - ? allowedEmails.value.trim() - : ""; - - if (restriction.length > 0) { - if (Option.isNone(user)) { - yield* Effect.log( - "Email access restriction active and user not logged in. Access denied.", - ); - yield* Effect.fail( - new Policy.PolicyDeniedError({ - reason: "email_restriction_login_required", - }), - ); - } - if ( - Option.isSome(user) && - !isEmailAllowedByRestriction(user.value.email, restriction) - ) { - yield* Effect.log("Email access restriction active. Access denied."); - yield* Effect.fail( - new Policy.PolicyDeniedError({ - reason: "email_restriction_denied", - }), - ); - } + } + + if (!video.public) { + yield* Effect.log( + "Video is private and user has no explicit access. Access denied.", + ); + return false; + } + + const allowedEmails = yield* orgsRepo.allowedEmailDomain(video.orgId); + const restriction = Option.isSome(allowedEmails) + ? allowedEmails.value.trim() + : ""; + + if (restriction.length > 0) { + if (Option.isNone(user)) { + yield* Effect.log( + "Email access restriction active and user not logged in. Access denied.", + ); + yield* Effect.fail( + new Policy.PolicyDeniedError({ + reason: "email_restriction_login_required", + }), + ); } + if ( + Option.isSome(user) && + !isEmailAllowedByRestriction(user.value.email, restriction) + ) { + yield* Effect.log("Email access restriction active. Access denied."); + yield* Effect.fail( + new Policy.PolicyDeniedError({ + reason: "email_restriction_denied", + }), + ); + } + } - yield* Video.verifyPasswordCandidates(video, passwordHashes); + yield* Video.verifyPasswordCandidates(video, passwordHashes); - return true; + return true; + }); + +export function buildCanView(deps: VideosPolicyDeps, videoId: Video.VideoId) { + return Policy.publicPolicy( + Effect.fn(function* (user) { + const res = yield* deps.repo.getById(videoId); + + if (Option.isNone(res)) { + yield* Effect.log("Video not found. Access granted."); + return true; + } + + const [video, password] = res.value; + return yield* decideCanView(deps, user, video, password); }), ); } +export function buildCanViewLoaded( + deps: ViewDecisionDeps, + video: ViewableVideo, + password: Option.Option, +) { + return Policy.publicPolicy((user) => + decideCanView(deps, user, video, password), + ); +} + export class VideosPolicy extends Effect.Service()( "VideosPolicy", { @@ -146,6 +175,11 @@ export class VideosPolicy extends Effect.Service()( const canView = (videoId: Video.VideoId) => buildCanView(deps, videoId); + const canViewLoaded = ( + video: ViewableVideo, + password: Option.Option, + ) => buildCanViewLoaded(deps, video, password); + const isOwner = (videoId: Video.VideoId) => Policy.policy((user) => repo.getById(videoId).pipe( @@ -158,7 +192,41 @@ export class VideosPolicy extends Effect.Service()( ), ); - return { canView, isOwner }; + const isOwnerLoaded = (video: Pick) => + Policy.policy((user) => Effect.succeed(video.ownerId === user.id)); + + const getViewableById = (videoId: Video.VideoId) => + repo.getById(videoId).pipe( + Effect.flatMap( + Option.match({ + onNone: () => Effect.succeed(Option.none()), + onSome: (loaded) => + canViewLoaded(loaded[0], loaded[1]).pipe( + Effect.as(Option.some(loaded)), + ), + }), + ), + ); + + const getOwnedById = (videoId: Video.VideoId) => + repo.getById(videoId).pipe( + Effect.flatMap( + Option.match({ + onNone: () => Effect.succeed(Option.none()), + onSome: (loaded) => + isOwnerLoaded(loaded[0]).pipe(Effect.as(Option.some(loaded))), + }), + ), + ); + + return { + canView, + canViewLoaded, + isOwner, + isOwnerLoaded, + getViewableById, + getOwnedById, + }; }), dependencies: [ VideosRepo.Default, diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index 2ac200dd8df..c5e8351a4d8 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -130,12 +130,7 @@ export class Videos extends Effect.Service()("Videos", { const tinybird = yield* Tinybird; const getByIdForViewing = (id: Video.VideoId) => - repo - .getById(id) - .pipe( - Policy.withPublicPolicy(policy.canView(id)), - Effect.withSpan("Videos.getById"), - ); + policy.getViewableById(id).pipe(Effect.withSpan("Videos.getById")); const getAnalyticsCounts = Effect.fn("Videos.getAnalyticsCounts")( function* ( @@ -302,9 +297,7 @@ export class Videos extends Effect.Service()("Videos", { * Delete a video. Will fail if the user does not have access. */ delete: Effect.fn("Videos.delete")(function* (videoId: Video.VideoId) { - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const maybeVideo = yield* policy.getOwnedById(videoId); if (Option.isNone(maybeVideo)) return yield* Effect.fail(new Video.NotFoundError()); const [video] = maybeVideo.value; @@ -353,9 +346,7 @@ export class Videos extends Effect.Service()("Videos", { duplicate: Effect.fn("Videos.duplicate")(function* ( videoId: Video.VideoId, ) { - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const maybeVideo = yield* policy.getOwnedById(videoId); if (Option.isNone(maybeVideo)) return yield* Effect.fail(new Video.NotFoundError()); const [video] = maybeVideo.value; @@ -528,23 +519,22 @@ export class Videos extends Effect.Service()("Videos", { const updatedAt = input.updatedAt; const videoId = input.videoId; - const [record] = yield* db - .use((db) => - db - .select({ - video: Db.videos, - upload: Db.videoUploads, - }) - .from(Db.videos) - .leftJoin( - Db.videoUploads, - Dz.eq(Db.videos.id, Db.videoUploads.videoId), - ) - .where(Dz.eq(Db.videos.id, videoId)), - ) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const [record] = yield* db.use((db) => + db + .select({ + video: Db.videos, + upload: Db.videoUploads, + }) + .from(Db.videos) + .leftJoin( + Db.videoUploads, + Dz.eq(Db.videos.id, Db.videoUploads.videoId), + ) + .where(Dz.eq(Db.videos.id, videoId)), + ); if (!record) return yield* Effect.fail(new Video.NotFoundError()); + yield* policy.isOwnerLoaded(record.video); yield* db.use((db) => db.transaction(async (tx) => { @@ -705,9 +695,7 @@ export class Videos extends Effect.Service()("Videos", { getDownloadInfo: Effect.fn("Videos.getDownloadInfo")(function* ( videoId: Video.VideoId, ) { - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPublicPolicy(policy.canView(videoId))); + const maybeVideo = yield* policy.getViewableById(videoId); if (Option.isNone(maybeVideo)) return yield* Effect.fail(new Video.NotFoundError()); const [video] = maybeVideo.value; @@ -788,9 +776,7 @@ export class Videos extends Effect.Service()("Videos", { getThumbnailURL: Effect.fn("Videos.getThumbnailURL")(function* ( videoId: Video.VideoId, ) { - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPublicPolicy(policy.canView(videoId))); + const maybeVideo = yield* policy.getViewableById(videoId); if (Option.isNone(maybeVideo)) return Option.none(); const [video] = maybeVideo.value; diff --git a/packages/web-backend/src/index.ts b/packages/web-backend/src/index.ts index 4ded8a58b37..cdb5819fb57 100644 --- a/packages/web-backend/src/index.ts +++ b/packages/web-backend/src/index.ts @@ -41,8 +41,11 @@ export { export { findScreenshotObjectKey, Videos } from "./Videos/index.ts"; export { buildCanView, + buildCanViewLoaded, VideosPolicy, type VideosPolicyDeps, + type ViewableVideo, + type ViewDecisionDeps, } from "./Videos/VideosPolicy.ts"; export { VideosRepo } from "./Videos/VideosRepo.ts"; export * as Workflows from "./Workflows.ts"; diff --git a/packages/web-domain/src/Video.ts b/packages/web-domain/src/Video.ts index 3c0138fcb30..e6f6e0fb8c6 100644 --- a/packages/web-domain/src/Video.ts +++ b/packages/web-domain/src/Video.ts @@ -269,7 +269,7 @@ export const verifyPassword = (video: Video, password: Option.Option) => ); export const verifyPasswordCandidates = ( - video: Video, + video: Pick, passwords: ReadonlyArray, ) => Effect.gen(function* () { From d7ff80f74fe9a068affa3b2de1b8b55a510737ff Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:40:12 +0100 Subject: [PATCH 2/4] perf(upload): drop the duplicate video read from multipart handlers --- .../app/api/upload/[...route]/multipart.ts | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index b1b90eda9ef..3a893d8b16a 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -8,9 +8,8 @@ import { provideOptionalAuth, Storage, VideosPolicy, - VideosRepo, } from "@cap/web-backend"; -import { Policy, Video } from "@cap/web-domain"; +import { Video } from "@cap/web-domain"; import { zValidator } from "@hono/zod-validator"; import { and, eq } from "drizzle-orm"; import { Effect, Option, Schedule } from "effect"; @@ -91,13 +90,10 @@ app.post( const videoId = Video.VideoId.make(videoIdRaw); const resp = await Effect.gen(function* () { - const repo = yield* VideosRepo; const policy = yield* VideosPolicy; const db = yield* Database; - const video = yield* repo - .getById(videoId) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const video = yield* policy.getOwnedById(videoId); if (Option.isNone(video)) return yield* new Video.NotFoundError(); yield* db.use((db) => @@ -133,11 +129,8 @@ app.post( try { try { const uploadId = await Effect.gen(function* () { - const repo = yield* VideosRepo; const policy = yield* VideosPolicy; - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const maybeVideo = yield* policy.getOwnedById(videoId); if (Option.isNone(maybeVideo)) { return yield* new Video.NotFoundError(); } @@ -231,11 +224,8 @@ app.post( "videoId" in body ? body.videoId : videoIdFromFileKey; if (!videoIdRaw) throw new Error("Video id not found"); const videoId = Video.VideoId.make(videoIdRaw); - const repo = yield* VideosRepo; const policy = yield* VideosPolicy; - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const maybeVideo = yield* policy.getOwnedById(videoId); if (Option.isNone(maybeVideo)) { return yield* new Video.NotFoundError(); } @@ -315,7 +305,6 @@ app.post( const user = c.get("user"); return Effect.gen(function* () { - const repo = yield* VideosRepo; const policy = yield* VideosPolicy; const db = yield* Database; @@ -327,9 +316,7 @@ app.post( if (!videoIdRaw) return c.text("Video id not found", 400); const videoId = Video.VideoId.make(videoIdRaw); - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const maybeVideo = yield* policy.getOwnedById(videoId); if (Option.isNone(maybeVideo)) { c.status(404); return c.text(`Video '${encodeURIComponent(videoId)}' not found`); @@ -766,13 +753,10 @@ app.post("/abort", abortRequestValidator, (c) => { const videoId = Video.VideoId.make(videoIdRaw); return Effect.gen(function* () { - const repo = yield* VideosRepo; const policy = yield* VideosPolicy; const db = yield* Database; - const maybeVideo = yield* repo - .getById(videoId) - .pipe(Policy.withPolicy(policy.isOwner(videoId))); + const maybeVideo = yield* policy.getOwnedById(videoId); if (Option.isNone(maybeVideo)) { c.status(404); return c.text(`Video '${encodeURIComponent(videoId)}' not found`); From 403b88070738e5902a9ea35aa1aaada137536d11 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:40:12 +0100 Subject: [PATCH 3/4] perf(dashboard): run independent dashboard queries concurrently --- apps/web/app/(org)/dashboard/caps/page.tsx | 101 +-- .../web/app/(org)/dashboard/dashboard-data.ts | 606 ++++++++++-------- 2 files changed, 392 insertions(+), 315 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/page.tsx b/apps/web/app/(org)/dashboard/caps/page.tsx index 964da696d15..6200ac48b0f 100644 --- a/apps/web/app/(org)/dashboard/caps/page.tsx +++ b/apps/web/app/(org)/dashboard/caps/page.tsx @@ -37,40 +37,42 @@ const getSharedSpacesForVideos = Effect.fn(function* ( const db = yield* Database; - // Fetch space-level sharing - const spaceSharing = yield* db.use((db) => - db - .select({ - videoId: spaceVideos.videoId, - id: spaces.id, - name: spaces.name, - organizationId: spaces.organizationId, - iconUrl: spaces.iconUrl, - settings: spaces.settings, - hasPassword: sql`${spaces.password} IS NOT NULL`.mapWith(Boolean), - }) - .from(spaceVideos) - .innerJoin(spaces, eq(spaceVideos.spaceId, spaces.id)) - .innerJoin(organizations, eq(spaces.organizationId, organizations.id)) - .where(inArray(spaceVideos.videoId, videoIds)), - ); - - // Fetch organization-level sharing - const orgSharing = yield* db.use((db) => - db - .select({ - videoId: sharedVideos.videoId, - id: organizations.id, - name: organizations.name, - organizationId: organizations.id, - iconUrl: organizations.iconUrl, - }) - .from(sharedVideos) - .innerJoin( - organizations, - eq(sharedVideos.organizationId, organizations.id), - ) - .where(inArray(sharedVideos.videoId, videoIds)), + const [spaceSharing, orgSharing] = yield* Effect.all( + [ + db.use((db) => + db + .select({ + videoId: spaceVideos.videoId, + id: spaces.id, + name: spaces.name, + organizationId: spaces.organizationId, + iconUrl: spaces.iconUrl, + settings: spaces.settings, + hasPassword: sql`${spaces.password} IS NOT NULL`.mapWith(Boolean), + }) + .from(spaceVideos) + .innerJoin(spaces, eq(spaceVideos.spaceId, spaces.id)) + .innerJoin(organizations, eq(spaces.organizationId, organizations.id)) + .where(inArray(spaceVideos.videoId, videoIds)), + ), + db.use((db) => + db + .select({ + videoId: sharedVideos.videoId, + id: organizations.id, + name: organizations.name, + organizationId: organizations.id, + iconUrl: organizations.iconUrl, + }) + .from(sharedVideos) + .innerJoin( + organizations, + eq(sharedVideos.organizationId, organizations.id), + ) + .where(inArray(sharedVideos.videoId, videoIds)), + ), + ], + { concurrency: "unbounded" }, ); // Combine and group by videoId @@ -136,7 +138,7 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) { const userId = user.id; const offset = (page - 1) * limit; - const totalCountResult = await db() + const totalCountPromise = db() .select({ count: count() }) .from(videos) .leftJoin(organizations, eq(videos.orgId, organizations.id)) @@ -148,9 +150,7 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) { ), ); - const totalCount = totalCountResult[0]?.count || 0; - - const videoData = await db() + const videoDataPromise = db() .select({ id: videos.id, ownerId: videos.ownerId, @@ -222,7 +222,7 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) { .limit(limit) .offset(offset); - const foldersData = await db() + const foldersDataPromise = db() .select({ id: folders.id, name: folders.name, @@ -244,19 +244,28 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) { ), ); - // Fetch shared spaces data for all videos - const videoIds = videoData.map((video) => video.id); - const sharedSpacesMap = - await getSharedSpacesForVideos(videoIds).pipe(runPromise); - const [organizationSettingsRow] = user.activeOrganizationId - ? await db() + const organizationSettingsPromise = user.activeOrganizationId + ? db() .select({ settings: organizations.settings }) .from(organizations) .where(eq(organizations.id, user.activeOrganizationId)) .limit(1) - : []; + : Promise.resolve([]); + + const [totalCountResult, videoData, foldersData, [organizationSettingsRow]] = + await Promise.all([ + totalCountPromise, + videoDataPromise, + foldersDataPromise, + organizationSettingsPromise, + ]); + const totalCount = totalCountResult[0]?.count || 0; const organizationSettings = organizationSettingsRow?.settings ?? null; + const videoIds = videoData.map((video) => video.id); + const sharedSpacesMap = + await getSharedSpacesForVideos(videoIds).pipe(runPromise); + const processedVideoData = await Effect.all( videoData.map( Effect.fn(function* (video) { diff --git a/apps/web/app/(org)/dashboard/dashboard-data.ts b/apps/web/app/(org)/dashboard/dashboard-data.ts index db913c12dd1..a5a88583dd2 100644 --- a/apps/web/app/(org)/dashboard/dashboard-data.ts +++ b/apps/web/app/(org)/dashboard/dashboard-data.ts @@ -87,197 +87,263 @@ function mergeUserOrganizations( return Array.from(organizationsById.values()); } -export async function getDashboardData(user: typeof userSelectProps) { - try { - const [ownedOrganizations, memberOrganizations] = await Promise.all([ - db() - .select() - .from(organizations) - .where( - and( - isNull(organizations.tombstoneAt), - eq(organizations.ownerId, user.id), - ), +type OrganizationRow = typeof organizations.$inferSelect; + +async function loadUserOrganizations(user: typeof userSelectProps) { + const [ownedOrganizations, memberOrganizations] = await Promise.all([ + db() + .select() + .from(organizations) + .where( + and( + isNull(organizations.tombstoneAt), + eq(organizations.ownerId, user.id), ), - db() - .select({ organization: organizations }) - .from(organizationMembers) - .innerJoin( - organizations, - eq(organizations.id, organizationMembers.organizationId), - ) - .where( - and( - eq(organizationMembers.userId, user.id), - isNull(organizations.tombstoneAt), - ), + ), + db() + .select({ organization: organizations }) + .from(organizationMembers) + .innerJoin( + organizations, + eq(organizations.id, organizationMembers.organizationId), + ) + .where( + and( + eq(organizationMembers.userId, user.id), + isNull(organizations.tombstoneAt), ), - ]); + ), + ]); - const userOrganizations = mergeUserOrganizations( - ownedOrganizations, - memberOrganizations, - ); + return mergeUserOrganizations(ownedOrganizations, memberOrganizations); +} - const organizationIds = userOrganizations.map((org) => org.id); +function resolveActiveOrganization( + user: typeof userSelectProps, + userOrganizations: OrganizationRow[], +) { + const organizationIds = userOrganizations.map((org) => org.id); - let organizationInvitesData: (typeof organizationInvites.$inferSelect)[] = - []; - if (organizationIds.length > 0) { - organizationInvitesData = await db() - .select() - .from(organizationInvites) - .where(inArray(organizationInvites.organizationId, organizationIds)); - } - - let anyNewNotifications = false; - let spacesData: Spaces[] = []; - let organizationSettings: OrganizationSettings | null = null; - let userCapsCount = 0; - let currentOrganizationRole: OrganizationRole | null = null; - - let activeOrganizationId = organizationIds.find( - (orgId) => orgId === user.activeOrganizationId, - ); - - if (!activeOrganizationId && organizationIds.length > 0) { - activeOrganizationId = organizationIds[0]; - } - - if (activeOrganizationId) { - const activeOrgInfo = userOrganizations.find( - (org) => org.id === activeOrganizationId, - ); - const [activeOrgMembership] = await db() - .select({ role: organizationMembers.role }) - .from(organizationMembers) - .where( - and( - eq(organizationMembers.organizationId, activeOrganizationId), - eq(organizationMembers.userId, user.id), - ), - ) - .limit(1); - currentOrganizationRole = getEffectiveOrganizationRole({ - userId: user.id, - ownerId: activeOrgInfo?.ownerId, - memberRole: activeOrgMembership?.role, - }); - - const [notification] = await db() - .select({ id: notifications.id }) - .from(notifications) - .where( - and( - eq(notifications.recipientId, user.id), - eq(notifications.orgId, activeOrganizationId), - isNull(notifications.readAt), - ), - ) - .limit(1); - - anyNewNotifications = !!notification; - - const [organizationSetting] = await db() - .select({ settings: organizations.settings }) - .from(organizations) - .where(eq(organizations.id, activeOrganizationId)); - organizationSettings = organizationSetting?.settings || null; - - spacesData = await Effect.gen(function* () { - const db = yield* Database; - const imageUploads = yield* ImageUploads; - - return yield* db - .use((db) => - db - .select({ - id: spaces.id, - primary: spaces.primary, - privacy: spaces.privacy, - public: spaces.public, - name: spaces.name, - description: spaces.description, - organizationId: spaces.organizationId, - createdById: spaces.createdById, - iconUrl: spaces.iconUrl, - settings: spaces.settings, - currentUserSpaceRole: sql`( + let activeOrganizationId = organizationIds.find( + (orgId) => orgId === user.activeOrganizationId, + ); + + if (!activeOrganizationId && organizationIds.length > 0) { + activeOrganizationId = organizationIds[0]; + } + + if (!activeOrganizationId) return null; + + return { + activeOrganizationId, + activeOrgInfo: userOrganizations.find( + (org) => org.id === activeOrganizationId, + ), + }; +} + +async function loadActiveOrganizationRole( + user: typeof userSelectProps, + activeOrganizationId: OrganizationRow["id"], + activeOrgInfo: OrganizationRow | undefined, +) { + const [activeOrgMembership] = await db() + .select({ role: organizationMembers.role }) + .from(organizationMembers) + .where( + and( + eq(organizationMembers.organizationId, activeOrganizationId), + eq(organizationMembers.userId, user.id), + ), + ) + .limit(1); + + return getEffectiveOrganizationRole({ + userId: user.id, + ownerId: activeOrgInfo?.ownerId, + memberRole: activeOrgMembership?.role, + }); +} + +function loadSpaces( + user: typeof userSelectProps, + activeOrganizationId: OrganizationRow["id"], + currentOrganizationRole: OrganizationRole | null, +): Promise { + return Effect.gen(function* () { + const db = yield* Database; + const imageUploads = yield* ImageUploads; + + return yield* db + .use((db) => + db + .select({ + id: spaces.id, + primary: spaces.primary, + privacy: spaces.privacy, + public: spaces.public, + name: spaces.name, + description: spaces.description, + organizationId: spaces.organizationId, + createdById: spaces.createdById, + iconUrl: spaces.iconUrl, + settings: spaces.settings, + currentUserSpaceRole: sql`( SELECT space_members.role FROM space_members WHERE space_members.spaceId = spaces.id AND space_members.userId = ${user.id} LIMIT 1 )`, - hasPassword: sql`${spaces.password} IS NOT NULL`.mapWith( - Boolean, - ), - memberCount: sql`( + hasPassword: sql`${spaces.password} IS NOT NULL`.mapWith(Boolean), + memberCount: sql`( SELECT COUNT(*) FROM space_members WHERE space_members.spaceId = spaces.id )`, - videoCount: sql`( + videoCount: sql`( SELECT COUNT(*) FROM space_videos WHERE space_videos.spaceId = spaces.id )`, - }) - .from(spaces) - .where( - and( - eq(spaces.organizationId, activeOrganizationId), - or( - eq(spaces.createdById, user.id), - eq(spaces.privacy, "Public"), - sql`EXISTS ( + }) + .from(spaces) + .where( + and( + eq(spaces.organizationId, activeOrganizationId), + or( + eq(spaces.createdById, user.id), + eq(spaces.privacy, "Public"), + sql`EXISTS ( SELECT 1 FROM space_members WHERE space_members.spaceId = spaces.id AND space_members.userId = ${user.id} )`, - ), - ), - ), - ) - .pipe( - Effect.map((rows) => - rows.map( - Effect.fn(function* (row) { - const { currentUserSpaceRole, ...spaceRow } = row; - const currentUserRole = getEffectiveSpaceRole({ - userId: user.id, - createdById: row.createdById, - memberRole: currentUserSpaceRole, - }); - return { - ...spaceRow, - iconUrl: row.iconUrl - ? yield* imageUploads.resolveImageUrl(row.iconUrl) - : null, - currentUserRole, - currentUserCanManage: canManageSpace({ - organizationRole: currentOrganizationRole, - spaceRole: currentUserRole, - }), - }; - }), ), ), - Effect.flatMap(Effect.all), - ); - }).pipe(runPromise); + ), + ) + .pipe( + Effect.map((rows) => + rows.map( + Effect.fn(function* (row) { + const { currentUserSpaceRole, ...spaceRow } = row; + const currentUserRole = getEffectiveSpaceRole({ + userId: user.id, + createdById: row.createdById, + memberRole: currentUserSpaceRole, + }); + return { + ...spaceRow, + iconUrl: row.iconUrl + ? yield* imageUploads.resolveImageUrl(row.iconUrl) + : null, + currentUserRole, + currentUserCanManage: canManageSpace({ + organizationRole: currentOrganizationRole, + spaceRole: currentUserRole, + }), + }; + }), + ), + ), + Effect.flatMap(Effect.all), + ); + }).pipe(runPromise); +} - if (activeOrgInfo) { - const orgMemberCountResult = await db() - .select({ value: sql`COUNT(*)` }) - .from(organizationMembers) - .where(eq(organizationMembers.organizationId, activeOrgInfo.id)); - const orgMemberCount = orgMemberCountResult[0]?.value || 0; +async function loadAllSpacesEntry( + activeOrgInfo: OrganizationRow, + currentOrganizationRole: OrganizationRole | null, +): Promise { + const [orgMemberCountResult, orgVideoCountResult] = await Promise.all([ + db() + .select({ value: sql`COUNT(*)` }) + .from(organizationMembers) + .where(eq(organizationMembers.organizationId, activeOrgInfo.id)), + db() + .select({ + value: sql`COUNT(DISTINCT ${sharedVideos.videoId})`, + }) + .from(sharedVideos) + .where(eq(sharedVideos.organizationId, activeOrgInfo.id)), + ]); + const orgMemberCount = orgMemberCountResult[0]?.value || 0; + const orgVideoCount = orgVideoCountResult[0]?.value || 0; - const orgVideoCountResult = await db() - .select({ - value: sql`COUNT(DISTINCT ${sharedVideos.videoId})`, - }) - .from(sharedVideos) - .where(eq(sharedVideos.organizationId, activeOrgInfo.id)); - const orgVideoCount = orgVideoCountResult[0]?.value || 0; + return Effect.gen(function* () { + const imageUploads = yield* ImageUploads; + + const iconUrl = activeOrgInfo.iconUrl; + + return { + id: activeOrgInfo.id, + primary: true, + privacy: "Public", + name: `All ${activeOrgInfo.name}`, + description: `View all content in ${activeOrgInfo.name}`, + organizationId: activeOrgInfo.id, + iconUrl: iconUrl ? yield* imageUploads.resolveImageUrl(iconUrl) : null, + memberCount: orgMemberCount, + createdById: activeOrgInfo.ownerId, + videoCount: orgVideoCount, + settings: null, + hasPassword: false, + public: false, + currentUserRole: currentOrganizationRole, + currentUserCanManage: canManageOrganizationMembers( + currentOrganizationRole, + ), + } as const; + }).pipe(runPromise); +} - const userCapsCountResult = await db() +export async function getDashboardSpacesData( + user: typeof userSelectProps, +): Promise { + const userOrganizations = await loadUserOrganizations(user); + const active = resolveActiveOrganization(user, userOrganizations); + if (!active) return []; + + const currentOrganizationRole = await loadActiveOrganizationRole( + user, + active.activeOrganizationId, + active.activeOrgInfo, + ); + const [spacesRows, allSpacesEntry] = await Promise.all([ + loadSpaces(user, active.activeOrganizationId, currentOrganizationRole), + active.activeOrgInfo + ? loadAllSpacesEntry(active.activeOrgInfo, currentOrganizationRole) + : Promise.resolve(null), + ]); + + return allSpacesEntry ? [allSpacesEntry, ...spacesRows] : spacesRows; +} + +async function loadActiveOrganizationData( + user: typeof userSelectProps, + { + activeOrganizationId, + activeOrgInfo, + }: NonNullable>, +) { + const [currentOrganizationRole, [notification]] = await Promise.all([ + loadActiveOrganizationRole(user, activeOrganizationId, activeOrgInfo), + db() + .select({ id: notifications.id }) + .from(notifications) + .where( + and( + eq(notifications.recipientId, user.id), + eq(notifications.orgId, activeOrganizationId), + isNull(notifications.readAt), + ), + ) + .limit(1), + ]); + + const [spacesRows, allSpacesEntry, userCapsCountResult] = await Promise.all([ + loadSpaces(user, activeOrganizationId, currentOrganizationRole), + activeOrgInfo + ? loadAllSpacesEntry(activeOrgInfo, currentOrganizationRole) + : Promise.resolve(null), + activeOrgInfo + ? db() .select({ value: sql`COUNT(DISTINCT ${videos.id})`, }) @@ -287,49 +353,40 @@ export async function getDashboardData(user: typeof userSelectProps) { eq(videos.orgId, activeOrgInfo.id), eq(videos.ownerId, user.id), ), - ); - - userCapsCount = userCapsCountResult[0]?.value || 0; - - const allSpacesEntry = await Effect.gen(function* () { - const imageUploads = yield* ImageUploads; - - const iconUrl = activeOrgInfo.iconUrl; - - return { - id: activeOrgInfo.id, - primary: true, - privacy: "Public", - name: `All ${activeOrgInfo.name}`, - description: `View all content in ${activeOrgInfo.name}`, - organizationId: activeOrgInfo.id, - iconUrl: iconUrl - ? yield* imageUploads.resolveImageUrl(iconUrl) - : null, - memberCount: orgMemberCount, - createdById: activeOrgInfo.ownerId, - videoCount: orgVideoCount, - settings: null, - hasPassword: false, - public: false, - currentUserRole: currentOrganizationRole, - currentUserCanManage: canManageOrganizationMembers( - currentOrganizationRole, - ), - } as const; - }).pipe(runPromise); + ) + : Promise.resolve<{ value: number }[]>([]), + ]); + + return { + anyNewNotifications: !!notification, + organizationSettings: activeOrgInfo?.settings || null, + spacesData: allSpacesEntry ? [allSpacesEntry, ...spacesRows] : spacesRows, + userCapsCount: userCapsCountResult[0]?.value || 0, + }; +} - spacesData = [allSpacesEntry, ...spacesData]; - } - } +export async function getDashboardData(user: typeof userSelectProps) { + try { + const userOrganizations = await loadUserOrganizations(user); + const organizationIds = userOrganizations.map((org) => org.id); + const active = resolveActiveOrganization(user, userOrganizations); + + const [organizationInvitesData, activeData] = await Promise.all([ + organizationIds.length > 0 + ? db() + .select() + .from(organizationInvites) + .where(inArray(organizationInvites.organizationId, organizationIds)) + : Promise.resolve<(typeof organizationInvites.$inferSelect)[]>([]), + active ? loadActiveOrganizationData(user, active) : Promise.resolve(null), + ]); - const [userPreferences] = await db() - .select({ - preferences: users.preferences, - }) - .from(users) - .where(eq(users.id, user.id)) - .limit(1); + const spacesData: Spaces[] = activeData?.spacesData ?? []; + const organizationSettings: OrganizationSettings | null = + activeData?.organizationSettings ?? null; + const anyNewNotifications = activeData?.anyNewNotifications ?? false; + const userCapsCount = activeData?.userCapsCount ?? 0; + const userPreferences = { preferences: user.preferences }; const organizationSelect: Organization[] = await Effect.all( userOrganizations.map( @@ -337,38 +394,56 @@ export async function getDashboardData(user: typeof userSelectProps) { const db = yield* Database; const iconImages = yield* ImageUploads; - const allMembers = yield* db.use((db) => - db - .select({ - member: organizationMembers, - user: { - id: users.id, - name: users.name, - lastName: users.lastName, - email: users.email, - image: users.image, - }, - }) - .from(organizationMembers) - .leftJoin(users, eq(organizationMembers.userId, users.id)) - .where(eq(organizationMembers.organizationId, organization.id)), - ); - const managerIds = Array.from( new Set([organization.ownerId, user.id]), ); - const managers = yield* db.use((db) => - db - .select({ - id: users.id, - inviteQuota: users.inviteQuota, - stripeSubscriptionId: users.stripeSubscriptionId, - stripeSubscriptionStatus: users.stripeSubscriptionStatus, - thirdPartyStripeSubscriptionId: - users.thirdPartyStripeSubscriptionId, - }) - .from(users) - .where(inArray(users.id, managerIds)), + const [allMembers, managers, ownedIds] = yield* Effect.all( + [ + db.use((db) => + db + .select({ + member: organizationMembers, + user: { + id: users.id, + name: users.name, + lastName: users.lastName, + email: users.email, + image: users.image, + }, + }) + .from(organizationMembers) + .leftJoin(users, eq(organizationMembers.userId, users.id)) + .where( + eq(organizationMembers.organizationId, organization.id), + ), + ), + db.use((db) => + db + .select({ + id: users.id, + inviteQuota: users.inviteQuota, + stripeSubscriptionId: users.stripeSubscriptionId, + stripeSubscriptionStatus: users.stripeSubscriptionStatus, + thirdPartyStripeSubscriptionId: + users.thirdPartyStripeSubscriptionId, + }) + .from(users) + .where(inArray(users.id, managerIds)), + ), + db.use((db) => + db + .select({ id: organizations.id }) + .from(organizations) + .where( + and( + eq(organizations.ownerId, organization.ownerId), + isNull(organizations.tombstoneAt), + ), + ) + .then((rows) => rows.map((r) => r.id)), + ), + ], + { concurrency: "unbounded" }, ); const owner = managers.find( (manager) => manager.id === organization.ownerId, @@ -390,37 +465,30 @@ export async function getDashboardData(user: typeof userSelectProps) { actorCanManageProSeats: canManageOrganizationProSeats(currentRole), }); - const ownedOrgIds = db.use((db) => - db - .select({ id: organizations.id }) - .from(organizations) - .where( - and( - eq(organizations.ownerId, organization.ownerId), - isNull(organizations.tombstoneAt), - ), - ) - .then((rows) => rows.map((r) => r.id)), - ); - - const ownedIds = yield* ownedOrgIds; - - const memberCountResult = yield* db.use((db) => - ownedIds.length > 0 - ? db - .select({ value: count() }) - .from(organizationMembers) - .where(inArray(organizationMembers.organizationId, ownedIds)) - : Promise.resolve([{ value: 0 }]), - ); - - const inviteCountResult = yield* db.use((db) => - ownedIds.length > 0 - ? db - .select({ value: count() }) - .from(organizationInvites) - .where(inArray(organizationInvites.organizationId, ownedIds)) - : Promise.resolve([{ value: 0 }]), + const [memberCountResult, inviteCountResult] = yield* Effect.all( + [ + db.use((db) => + ownedIds.length > 0 + ? db + .select({ value: count() }) + .from(organizationMembers) + .where( + inArray(organizationMembers.organizationId, ownedIds), + ) + : Promise.resolve([{ value: 0 }]), + ), + db.use((db) => + ownedIds.length > 0 + ? db + .select({ value: count() }) + .from(organizationInvites) + .where( + inArray(organizationInvites.organizationId, ownedIds), + ) + : Promise.resolve([{ value: 0 }]), + ), + ], + { concurrency: "unbounded" }, ); const totalInvites = From e11583962392b01aa7cf7c6c338d5fb211e1828a Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:40:12 +0100 Subject: [PATCH 4/4] perf(share): load the share page with one video read and a spaces-only query --- apps/web/app/s/[videoId]/page.tsx | 73 ++++++++++++++++++------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index b1ac888798b..c803a6e83f6 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -29,7 +29,7 @@ import { Comment, type ImageUpload, type Organisation, - Policy, + type Policy, type Video, } from "@cap/web-domain"; import { and, eq, type InferSelectModel, isNull, sql } from "drizzle-orm"; @@ -40,8 +40,9 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { getVideoAnalytics } from "@/actions/videos/get-analytics"; import { - getDashboardData, + getDashboardSpacesData, type OrganizationSettings, + type Spaces, } from "@/app/(org)/dashboard/dashboard-data"; import { isAiConfigured } from "@/lib/ai/provider"; import { completeDesktopSegmentsManifestAndQueue } from "@/lib/desktop-segments-recovery"; @@ -98,6 +99,22 @@ const hasRecordingStoppedParam = (searchParams: ShareVideoSearchParams) => { return recordingStoppedParam === "1" || recordingStoppedParam === "true"; }; +function toShareVideo< + T extends { + password: unknown; + ownerId: unknown; + organizationTombstoneAt: Date | null; + }, +>(row: T) { + const { + password: _password, + ownerId: _ownerId, + organizationTombstoneAt: _organizationTombstoneAt, + ...video + } = row; + return video; +} + // Helper function to fetch shared spaces data for a video async function getSharedSpacesForVideo(videoId: Video.VideoId) { // Space-level and organization-level sharing are independent queries. @@ -316,7 +333,7 @@ export default async function ShareVideoPage(props: PageProps<"/s/[videoId]">) { return Effect.gen(function* () { const videosPolicy = yield* VideosPolicy; - const [video] = yield* Effect.promise(() => + const [row] = yield* Effect.promise(() => db() .select({ id: videos.id, @@ -360,16 +377,29 @@ export default async function ShareVideoPage(props: PageProps<"/s/[videoId]">) { ), activeUploadRawFileKey: videoUploads.rawFileKey, owner: users, + ownerId: videos.ownerId, + password: videos.password, + organizationTombstoneAt: organizations.tombstoneAt, }) .from(videos) .leftJoin(sharedVideos, eq(videos.id, sharedVideos.videoId)) .innerJoin(users, eq(videos.ownerId, users.id)) .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) .leftJoin(organizations, eq(videos.orgId, organizations.id)) - .where(and(eq(videos.id, videoId), isNull(organizations.tombstoneAt))), - ).pipe(Policy.withPublicPolicy(videosPolicy.canView(videoId))); + .where(eq(videos.id, videoId)), + ); + + // The access decision runs on the row already loaded above instead of + // re-reading it, and stays ahead of the tombstone check so a denied or + // password-gated video on a deleted org still resolves the way it did + // when the policy ran before the select. + if (row) { + yield* videosPolicy.canViewLoaded(row, Option.fromNullable(row.password)); + } - return Option.fromNullable(video); + return Option.fromNullable( + row && row.organizationTombstoneAt === null ? toShareVideo(row) : null, + ); }).pipe( Effect.flatten, Effect.map((video) => ({ needsPassword: false, video }) as const), @@ -480,19 +510,11 @@ async function AuthorizedContent({ // Everything below is an independent round trip (DB or storage); each is // started here and awaited together further down, so the page pays for the // slowest one instead of the sum of all of them. - const spacesDataPromise: Promise< - Awaited>["spacesData"] | null - > = user - ? getDashboardData(user).then( - (dashboardData) => dashboardData.spacesData, - (error) => { - console.error( - "Failed to fetch spaces data for sharing dialog:", - error, - ); - return []; - }, - ) + const spacesDataPromise: Promise = user + ? getDashboardSpacesData(user).catch((error) => { + console.error("Failed to fetch spaces data for sharing dialog:", error); + return []; + }) : Promise.resolve(null); const sharedSpacesPromise = getSharedSpacesForVideo(videoId); @@ -515,18 +537,7 @@ async function AuthorizedContent({ return false; }); - const aiGenerationEnabledPromise = db() - .select({ - email: users.email, - stripeSubscriptionStatus: users.stripeSubscriptionStatus, - thirdPartyStripeSubscriptionId: users.thirdPartyStripeSubscriptionId, - }) - .from(users) - .where(eq(users.id, video.owner.id)) - .limit(1) - .then((videoOwnerQuery) => - videoOwnerQuery[0] ? isAiGenerationEnabled(videoOwnerQuery[0]) : false, - ); + const aiGenerationEnabledPromise = isAiGenerationEnabled(video.owner); const screenshotImageUrlPromise = video.isScreenshot ? Effect.flatMap(Videos, (videos) => videos.getThumbnailURL(videoId)).pipe(