mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
intuitive memory movement, avoid duplicates in home
This commit is contained in:
parent
c825043b84
commit
af1093d4aa
6 changed files with 183 additions and 13 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue