intuitive memory movement, avoid duplicates in home

This commit is contained in:
Dhravya Shah 2025-02-14 15:53:44 -08:00
parent c825043b84
commit af1093d4aa
6 changed files with 183 additions and 13 deletions

View file

@ -47,6 +47,10 @@ export const app = new Hono<{ Variables: Variables; Bindings: Env }>()
.use("/v1/*", auth)
.use("/v1/*", (c, next) => {
const user = c.get("user");
if (c.env.NODE_ENV === "development") {
return next();
}
// RATELIMITS
const rateLimitConfig = {

View file

@ -8,7 +8,7 @@ import {
spaceAccess,
contentToSpace,
} from "@supermemory/db/schema";
import { and, database, desc, eq, or, sql } from "@supermemory/db";
import { and, database, desc, eq, or, sql, isNull } from "@supermemory/db";
const memories = new Hono<{ Variables: Variables; Bindings: Env }>()
.get(
@ -124,9 +124,20 @@ const memories = new Hono<{ Variables: Variables; Bindings: Env }>()
const [items, [{ total }]] = await Promise.all([
db
.select()
.select({
documents: documents,
})
.from(documents)
.where(eq(documents.userId, user.id))
.leftJoin(
contentToSpace,
eq(documents.id, contentToSpace.contentId)
)
.where(
and(
eq(documents.userId, user.id),
isNull(contentToSpace.contentId)
)
)
.orderBy(desc(documents.createdAt))
.limit(count)
.offset(start),
@ -135,13 +146,22 @@ const memories = new Hono<{ Variables: Variables; Bindings: Env }>()
total: sql<number>`count(*)`.as("total"),
})
.from(documents)
.where(eq(documents.userId, user.id)),
.leftJoin(
contentToSpace,
eq(documents.id, contentToSpace.contentId)
)
.where(
and(
eq(documents.userId, user.id),
isNull(contentToSpace.contentId)
)
),
]);
return c.json({
items: items.map((item) => ({
...item,
id: item.uuid,
...item.documents,
id: item.documents.uuid,
})),
total,
});

View file

@ -99,6 +99,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
const spaceId = c.req.param("spaceId");
const db = database(c.env.HYPERDRIVE.connectionString);
const space = await db
.select()
.from(spaces)
@ -178,6 +179,10 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
return c.json({ error: "Unauthorized" }, 401);
}
if (body.spaceName.trim() === "<HOME>") {
return c.json({ error: "Cannot create space with name <HOME>" }, 400);
}
const db = database(c.env.HYPERDRIVE.connectionString);
const uuid = randomId();
@ -263,6 +268,99 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
}
}
)
.post(
"/moveContent",
zValidator(
"json",
z.object({
spaceId: z.string(),
documentId: z.string(),
})
),
async (c) => {
const body = c.req.valid("json");
const user = c.get("user");
const { spaceId, documentId } = body;
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
const db = database(c.env.HYPERDRIVE.connectionString);
try {
await db.transaction(async (tx) => {
// If moving to <HOME>, just remove all space connections
if (spaceId === "<HOME>") {
const doc = await tx
.select()
.from(documents)
.where(eq(documents.uuid, documentId))
.limit(1);
if (!doc[0]) {
return c.json({ error: "Document not found" }, 404);
}
await tx
.delete(contentToSpace)
.where(eq(contentToSpace.contentId, doc[0].id));
return;
}
// Get space and document, verify space ownership
const results = (
await tx
.select({
spaceId: spaces.id,
documentId: documents.id,
ownerId: spaces.ownerId,
spaceName: spaces.name,
})
.from(spaces)
.innerJoin(
documents,
and(eq(spaces.uuid, spaceId), eq(documents.uuid, documentId))
)
.limit(1)
)[0];
if (!results) {
return c.json({ error: "Space or document not found" }, 404);
}
if (results.ownerId !== user.id) {
return c.json(
{ error: "Not authorized to modify this space" },
403
);
}
// Delete existing space relations for this document
await tx
.delete(contentToSpace)
.where(eq(contentToSpace.contentId, results.documentId));
// Add new space relation
await tx.insert(contentToSpace).values({
contentId: results.documentId,
spaceId: results.spaceId,
});
});
return c.json({ success: true, spaceId });
} catch (e) {
console.error("Failed to move content to space:", e);
return c.json(
{
error: "Failed to move content to space",
details: e instanceof Error ? e.message : "Unknown error",
},
500
);
}
}
)
.post(
"/addContent",
zValidator(
@ -285,6 +383,24 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
try {
await db.transaction(async (tx) => {
// If adding to <HOME>, just remove all space connections
if (spaceId === "<HOME>") {
const doc = await tx
.select()
.from(documents)
.where(eq(documents.uuid, documentId))
.limit(1);
if (!doc[0]) {
return c.json({ error: "Document not found" }, 404);
}
await tx
.delete(contentToSpace)
.where(eq(contentToSpace.contentId, doc[0].id));
return;
}
// Get space and document, verify space ownership
const results = (
await tx

View file

@ -41,6 +41,7 @@ import { ExtraSpaceMetaData, fetchSpaces } from "~/lib/hooks/use-spaces";
import { useTextOverflow } from "~/lib/hooks/use-text-overflow";
import { Memory, WebsiteMetadata } from "~/lib/types/memory";
import { cn } from "~/lib/utils";
import { useNavigate } from "@remix-run/react";
const { useTweet } = ReactTweet;
@ -616,6 +617,7 @@ export function FetchAndRenderContent({ content }: { content: string }) {
function SharedCard({ data }: { data: Memory }) {
const queryClient = useQueryClient();
const navigate = useNavigate();
// Delete mutation
const deleteMutation = useMutation({
@ -657,7 +659,7 @@ function SharedCard({ data }: { data: Memory }) {
// Move to space mutation
const moveToSpaceMutation = useMutation({
mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
const response = await fetch("/backend/v1/spaces/addContent", {
const response = await fetch("/backend/v1/spaces/moveContent", {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -668,15 +670,19 @@ function SharedCard({ data }: { data: Memory }) {
if (!response.ok) {
throw new Error("Failed to move memory");
}
return response.json();
return response.json() as Promise<{ spaceId: string }>;
},
onError: (err) => {
toast.error("Failed to move memory to space");
},
onSuccess: () => {
onSuccess: ({ spaceId }: { spaceId: string }) => {
toast.success("Memory moved successfully");
queryClient.invalidateQueries({ queryKey: ["memories"] });
queryClient.invalidateQueries({ queryKey: ["spaces"] });
if (spaceId === "<HOME>") {
return navigate("/");
}
return navigate(`/space/${spaceId}`);
},
});

View file

@ -44,7 +44,27 @@ export async function fetchSpaces(): Promise<SpaceResponse> {
throw new Error("Failed to fetch spaces");
}
return response.json();
const resp = (await response.json()) as SpaceResponse;
resp.spaces.push({
id: 0,
uuid: "<HOME>",
name: "Home",
createdAt: new Date(),
updatedAt: new Date(),
ownerId: 0,
isPublic: false,
permissions: {
canRead: false,
canEdit: false,
isOwner: false,
isPublic: false,
},
owner: null,
favorited: false,
});
return resp;
}
async function createSpace(data: {

View file

@ -94,7 +94,7 @@ export default function Content() {
// Move to space mutation
const moveToSpaceMutation = useMutation({
mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
const response = await fetch("/backend/v1/spaces/addContent", {
const response = await fetch("/backend/v1/spaces/moveContent", {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -105,12 +105,16 @@ export default function Content() {
if (!response.ok) {
throw new Error("Failed to move memory");
}
return response.json();
return response.json() as Promise<{ spaceId: string }>;
},
onSuccess: () => {
onSuccess: ({ spaceId }: {spaceId: string}) => {
toast.success("Memory moved successfully");
queryClient.invalidateQueries({ queryKey: ["memories"] });
queryClient.invalidateQueries({ queryKey: ["spaces"] });
if (spaceId === "<HOME>") {
return navigate("/");
}
return navigate(`/space/${spaceId}`);
},
onError: () => {
toast.error("Failed to move memory to space");