Skip to content

Commit e78028b

Browse files
Effect: Port UpdateFolder (#1142)
* wip * wip * more wip * wip * fix error * cleanup `"use server"`'s * add policy for parent folder * improve logic * do it goodly * drop em * cleanup * introduce more branded types * drop it * format * use repo in most places * remove organisation re-export * cleanup + fix some policy problems * fix folderspolicy too * formatting --------- Co-authored-by: Brendan Allan <brendonovich@outlook.com>
1 parent 99a7912 commit e78028b

29 files changed

Lines changed: 425 additions & 240 deletions

File tree

apps/web/actions/folders/updateFolder.ts

Lines changed: 0 additions & 73 deletions
This file was deleted.

apps/web/app/(org)/dashboard/caps/components/Folder.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { useRouter } from "next/navigation";
99
import { useEffect, useRef, useState } from "react";
1010
import { toast } from "sonner";
1111
import { moveVideoToFolder } from "@/actions/folders/moveVideoToFolder";
12-
import { updateFolder } from "@/actions/folders/updateFolder";
1312
import { useEffectMutation } from "@/lib/EffectRuntime";
1413
import { withRpc } from "@/lib/Rpcs";
1514
import { ConfirmationDialog } from "../../_components/ConfirmationDialog";
@@ -83,6 +82,17 @@ const FolderCard = ({
8382
},
8483
});
8584

85+
const updateFolder = useEffectMutation({
86+
mutationFn: (data: Folder.FolderUpdate) =>
87+
withRpc((r) => r.FolderUpdate(data)),
88+
onSuccess: () => {
89+
toast.success("Folder name updated successfully");
90+
router.refresh();
91+
},
92+
onError: () => toast.error("Failed to update folder name"),
93+
onSettled: () => setIsRenaming(false),
94+
});
95+
8696
useEffect(() => {
8797
if (isRenaming && nameRef.current) {
8898
nameRef.current.focus();
@@ -176,17 +186,6 @@ const FolderCard = ({
176186
};
177187
}, [id, name, rive, isDragOver]);
178188

179-
const updateFolderNameHandler = async () => {
180-
try {
181-
await updateFolder({ folderId: id, name: updateName });
182-
toast.success("Folder name updated successfully");
183-
} catch (error) {
184-
toast.error("Failed to update folder name");
185-
} finally {
186-
setIsRenaming(false);
187-
}
188-
};
189-
190189
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
191190
e.preventDefault();
192191
e.stopPropagation();
@@ -342,18 +341,22 @@ const FolderCard = ({
342341
rows={1}
343342
value={updateName}
344343
onChange={(e) => setUpdateName(e.target.value)}
345-
onBlur={async () => {
344+
onBlur={() => {
346345
setIsRenaming(false);
347-
if (updateName.trim() !== name) {
348-
await updateFolderNameHandler();
349-
}
346+
if (updateName.trim() !== name)
347+
updateFolder.mutate({
348+
id,
349+
name: updateName.trim(),
350+
});
350351
}}
351-
onKeyDown={async (e) => {
352+
onKeyDown={(e) => {
352353
if (e.key === "Enter") {
353354
setIsRenaming(false);
354-
if (updateName.trim() !== name) {
355-
await updateFolderNameHandler();
356-
}
355+
if (updateName.trim() !== name)
356+
updateFolder.mutate({
357+
id,
358+
name: updateName.trim(),
359+
});
357360
}
358361
}}
359362
className="w-full resize-none bg-transparent border-none focus:outline-none

apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getCurrentUser } from "@cap/database/auth/session";
22
import { serverEnv } from "@cap/env";
3+
import { Spaces } from "@cap/web-backend";
34
import { CurrentUser, type Folder } from "@cap/web-domain";
45
import { Effect } from "effect";
56
import { notFound } from "next/navigation";
@@ -16,7 +17,6 @@ import {
1617
NewSubfolderButton,
1718
} from "../../../../folder/[id]/components";
1819
import FolderVideosSection from "../../../../folder/[id]/components/FolderVideosSection";
19-
import { getSpaceOrOrg } from "../../utils";
2020

2121
const FolderPage = async (props: {
2222
params: Promise<{ spaceId: string; folderId: Folder.FolderId }>;
@@ -26,7 +26,8 @@ const FolderPage = async (props: {
2626
if (!user) return notFound();
2727

2828
return await Effect.gen(function* () {
29-
const spaceOrOrg = yield* getSpaceOrOrg(params.spaceId);
29+
const spaces = yield* Spaces;
30+
const spaceOrOrg = yield* spaces.getSpaceOrOrg(params.spaceId);
3031
if (!spaceOrOrg) notFound();
3132

3233
const [childFolders, breadcrumb, videosData] = yield* Effect.all([

apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import {
1212
videoUploads,
1313
} from "@cap/database/schema";
1414
import { serverEnv } from "@cap/env";
15+
import { Spaces } from "@cap/web-backend";
1516
import { CurrentUser, Video } from "@cap/web-domain";
1617
import { and, count, desc, eq, isNull, sql } from "drizzle-orm";
1718
import { Effect } from "effect";
1819
import type { Metadata } from "next";
1920
import { notFound } from "next/navigation";
2021
import { runPromise } from "@/lib/server";
2122
import { SharedCaps } from "./SharedCaps";
22-
import { getSpaceOrOrg } from "./utils";
2323

2424
export const metadata: Metadata = {
2525
title: "Shared Caps — Cap",
@@ -92,7 +92,9 @@ export default async function SharedCapsPage(props: {
9292
const user = await getCurrentUser();
9393
if (!user) notFound();
9494

95-
const spaceOrOrg = await getSpaceOrOrg(params.spaceId).pipe(
95+
const spaceOrOrg = await Effect.flatMap(Spaces, (s) =>
96+
s.getSpaceOrOrg(params.spaceId),
97+
).pipe(
9698
Effect.catchTag("PolicyDenied", () => Effect.sync(() => notFound())),
9799
Effect.provideService(CurrentUser, user),
98100
runPromise,

apps/web/app/(org)/dashboard/spaces/[spaceId]/utils.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.

apps/web/app/Layout/devtoolsServer.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { users } from "@cap/database/schema";
66
import { eq } from "drizzle-orm";
77

88
export async function promoteToPro() {
9-
"use server";
10-
119
if (process.env.NODE_ENV !== "development")
1210
throw new Error("promoteToPro can only be used in development");
1311

@@ -24,8 +22,6 @@ export async function promoteToPro() {
2422
}
2523

2624
export async function demoteFromPro() {
27-
"use server";
28-
2925
if (process.env.NODE_ENV !== "development")
3026
throw new Error("demoteFromPro can only be used in development");
3127

apps/web/app/embed/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
"use server";
21
import { redirect } from "next/navigation";
32

43
export default async function EmbedPage() {

apps/web/app/s/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use server";
2-
31
import { redirect } from "next/navigation";
42

53
export default async function SharePage() {

apps/web/lib/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
HttpAuthMiddlewareLive,
88
OrganisationsPolicy,
99
S3Buckets,
10+
Spaces,
1011
SpacesPolicy,
1112
Videos,
1213
VideosPolicy,
@@ -47,6 +48,7 @@ export const Dependencies = Layer.mergeAll(
4748
Folders.Default,
4849
SpacesPolicy.Default,
4950
OrganisationsPolicy.Default,
51+
Spaces.Default,
5052
).pipe(
5153
Layer.provideMerge(
5254
Layer.mergeAll(Database.Default, TracingLayer, FetchHttpClient.layer),
Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,52 @@
1-
import * as Db from "@cap/database/schema";
21
import { type Folder, Policy } from "@cap/web-domain";
3-
import * as Dz from "drizzle-orm";
42
import { Effect } from "effect";
53

64
import { Database } from "../Database.ts";
5+
import { OrganisationsPolicy } from "../Organisations/OrganisationsPolicy.ts";
6+
import { Spaces } from "../Spaces/index.ts";
7+
import { SpacesPolicy } from "../Spaces/SpacesPolicy.ts";
8+
import { FoldersRepo } from "./FoldersRepo.ts";
79

810
export class FoldersPolicy extends Effect.Service<FoldersPolicy>()(
911
"FoldersPolicy",
1012
{
1113
effect: Effect.gen(function* () {
12-
const db = yield* Database;
14+
const repo = yield* FoldersRepo;
15+
const spacesPolicy = yield* SpacesPolicy;
16+
const orgsPolicy = yield* OrganisationsPolicy;
17+
const spaces = yield* Spaces;
1318

1419
const canEdit = (id: Folder.FolderId) =>
1520
Policy.policy((user) =>
1621
Effect.gen(function* () {
17-
const [folder] = yield* db.execute((db) =>
18-
db.select().from(Db.folders).where(Dz.eq(Db.folders.id, id)),
22+
const folder = yield* (yield* repo.getById(id)).pipe(
23+
Effect.catchTag(
24+
"NoSuchElementException",
25+
() => new Policy.PolicyDeniedError(),
26+
),
1927
);
2028

21-
// All space members can edit space properties
22-
if (!folder?.spaceId) {
23-
return folder?.createdById === user.id;
24-
}
25-
26-
const { spaceId } = folder;
27-
const [spaceMember] = yield* db.execute((db) =>
28-
db
29-
.select()
30-
.from(Db.spaceMembers)
31-
.where(
32-
Dz.and(
33-
Dz.eq(Db.spaceMembers.userId, user.id),
34-
Dz.eq(Db.spaceMembers.spaceId, spaceId),
35-
),
36-
),
37-
);
29+
if (folder.spaceId === null) return folder.createdById === user.id;
30+
31+
const spaceOrOrg = yield* spaces.getSpaceOrOrg(folder.spaceId);
32+
if (!spaceOrOrg) return false;
33+
34+
if (spaceOrOrg.variant === "space")
35+
yield* spacesPolicy.isMember(spaceOrOrg.space.id);
36+
else yield* orgsPolicy.isOwner(spaceOrOrg.organization.id);
3837

39-
return spaceMember !== undefined;
38+
return true;
4039
}),
4140
);
4241

4342
return { canEdit };
4443
}),
45-
dependencies: [Database.Default],
44+
dependencies: [
45+
FoldersRepo.Default,
46+
Database.Default,
47+
Spaces.Default,
48+
SpacesPolicy.Default,
49+
OrganisationsPolicy.Default,
50+
],
4651
},
4752
) {}

0 commit comments

Comments
 (0)