mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
fix: postgres error
This commit is contained in:
parent
a6e6c06120
commit
0c0ea871c3
5 changed files with 283 additions and 16 deletions
|
|
@ -164,3 +164,5 @@ Thanks to all the awesome people who have contributed to supermemory.
|
|||
<a href="https://github.com/Dhravya/SuperMemory/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Dhravya/SuperMemory" />
|
||||
</a>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,19 @@ import {
|
|||
chunk,
|
||||
spaces,
|
||||
spaceAccess,
|
||||
type Space,
|
||||
} from "@supermemory/db/schema";
|
||||
import { google, openai } from "../providers";
|
||||
import { randomId } from "@supermemory/shared";
|
||||
import { and, cosineDistance, database, desc, eq, or, sql } from "@supermemory/db";
|
||||
import {
|
||||
and,
|
||||
cosineDistance,
|
||||
database,
|
||||
desc,
|
||||
eq,
|
||||
or,
|
||||
sql,
|
||||
} from "@supermemory/db";
|
||||
import { typeDecider } from "../utils/typeDecider";
|
||||
import { isErr, Ok } from "../errors/results";
|
||||
|
||||
|
|
@ -527,7 +536,7 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
const cached = await c.env.MD_CACHE.get(cacheKey);
|
||||
if (cached) {
|
||||
return c.json({
|
||||
suggestedLearnings: JSON.parse(cached) as {[x: string]: string},
|
||||
suggestedLearnings: JSON.parse(cached) as { [x: string]: string },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -714,7 +723,9 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
const data = encoder.encode(content);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const documentHash = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
const documentHash = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
// Check for duplicates using hash
|
||||
const existingDocs = await db
|
||||
|
|
@ -727,10 +738,7 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
eq(documents.contentHash, documentHash),
|
||||
and(
|
||||
eq(documents.type, type.value),
|
||||
or(
|
||||
eq(documents.url, body.content),
|
||||
eq(documents.raw, content)
|
||||
)
|
||||
or(eq(documents.url, body.content), eq(documents.raw, content))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -757,13 +765,15 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
return { spaceId, allowed: false, error: "Space not found" };
|
||||
}
|
||||
|
||||
const spaceData = space[0] as Space;
|
||||
|
||||
// If public space, only owner can add content
|
||||
if (space[0].isPublic) {
|
||||
if (spaceData.isPublic) {
|
||||
return {
|
||||
spaceId,
|
||||
allowed: space[0].ownerId === user.id,
|
||||
allowed: spaceData.ownerId === user.id,
|
||||
error:
|
||||
space[0].ownerId !== user.id
|
||||
spaceData.ownerId !== user.id
|
||||
? "Only space owner can add to public spaces"
|
||||
: null,
|
||||
};
|
||||
|
|
@ -775,7 +785,7 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
.from(spaceAccess)
|
||||
.where(
|
||||
and(
|
||||
eq(spaceAccess.spaceId, space[0].id),
|
||||
eq(spaceAccess.spaceId, spaceData.id),
|
||||
eq(spaceAccess.userEmail, user.email),
|
||||
eq(spaceAccess.status, "accepted")
|
||||
)
|
||||
|
|
@ -785,9 +795,9 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
return {
|
||||
spaceId,
|
||||
allowed:
|
||||
space[0].ownerId === user.id || spaceAccessCheck.length > 0,
|
||||
spaceData.ownerId === user.id || spaceAccessCheck.length > 0,
|
||||
error:
|
||||
space[0].ownerId !== user.id && !spaceAccessCheck.length
|
||||
spaceData.ownerId !== user.id && !spaceAccessCheck.length
|
||||
? "Not authorized to add to this space"
|
||||
: null,
|
||||
};
|
||||
|
|
@ -856,6 +866,239 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
return c.json({ error: "Failed to process content" }, 500);
|
||||
}
|
||||
}
|
||||
)
|
||||
.post(
|
||||
"/batch-add",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
urls: z.array(z.string()).min(1, "At least one URL is required"),
|
||||
spaces: z.array(z.string()).max(5).optional(),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const user = c.get("user");
|
||||
if (!user) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const { urls, spaces } = await c.req.valid("json");
|
||||
|
||||
// Check space permissions if spaces are specified
|
||||
if (spaces && spaces.length > 0) {
|
||||
const db = database(c.env.HYPERDRIVE.connectionString);
|
||||
const spacePermissions = await Promise.all(
|
||||
spaces.map(async (spaceId) => {
|
||||
const space = await db
|
||||
.select()
|
||||
.from(spaces)
|
||||
.where(eq(spaces.uuid, spaceId))
|
||||
.limit(1);
|
||||
|
||||
if (!space[0]) {
|
||||
return { spaceId, allowed: false, error: "Space not found" };
|
||||
}
|
||||
|
||||
const spaceData = space[0] as Space;
|
||||
|
||||
// If public space, only owner can add content
|
||||
if (spaceData.isPublic) {
|
||||
return {
|
||||
spaceId,
|
||||
allowed: spaceData.ownerId === user.id,
|
||||
error:
|
||||
spaceData.ownerId !== user.id
|
||||
? "Only space owner can add to public spaces"
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
// For private spaces, check if user is owner or in allowlist
|
||||
const spaceAccessCheck = await db
|
||||
.select()
|
||||
.from(spaceAccess)
|
||||
.where(
|
||||
and(
|
||||
eq(spaceAccess.spaceId, spaceData.id),
|
||||
eq(spaceAccess.userEmail, user.email),
|
||||
eq(spaceAccess.status, "accepted")
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return {
|
||||
spaceId,
|
||||
allowed:
|
||||
spaceData.ownerId === user.id || spaceAccessCheck.length > 0,
|
||||
error:
|
||||
spaceData.ownerId !== user.id && !spaceAccessCheck.length
|
||||
? "Not authorized to add to this space"
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const unauthorized = spacePermissions.filter((p) => !p.allowed);
|
||||
if (unauthorized.length > 0) {
|
||||
return c.json(
|
||||
{
|
||||
error: "Space permission denied",
|
||||
details: unauthorized
|
||||
.map((u) => `${u.spaceId}: ${u.error}`)
|
||||
.join(", "),
|
||||
},
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new ReadableStream for progress updates
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const db = database(c.env.HYPERDRIVE.connectionString);
|
||||
const total = urls.length;
|
||||
let processed = 0;
|
||||
let failed = 0;
|
||||
let succeeded = 0;
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
processed++;
|
||||
|
||||
// Calculate document hash for duplicate detection
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(url);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const documentHash = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
// Check for duplicates
|
||||
const existingDocs = await db
|
||||
.select()
|
||||
.from(documents)
|
||||
.where(
|
||||
and(
|
||||
eq(documents.userId, user.id),
|
||||
or(
|
||||
eq(documents.contentHash, documentHash),
|
||||
eq(documents.url, url)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (existingDocs.length > 0) {
|
||||
failed++;
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
progress: Math.round((processed / total) * 100),
|
||||
status: "duplicate",
|
||||
url,
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
})}\n\n`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentId = `add-${user.id}-${randomId()}`;
|
||||
const type = typeDecider(url);
|
||||
|
||||
if (isErr(type)) {
|
||||
failed++;
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
progress: Math.round((processed / total) * 100),
|
||||
status: "error",
|
||||
url,
|
||||
error: type.error.message,
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
})}\n\n`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert into documents table
|
||||
await db.insert(documents).values({
|
||||
uuid: contentId,
|
||||
userId: user.id,
|
||||
type: type.value,
|
||||
url,
|
||||
contentHash: documentHash,
|
||||
raw: url + "\n\n" + spaces?.join(" "),
|
||||
});
|
||||
|
||||
// Create workflow for processing
|
||||
await c.env.CONTENT_WORKFLOW.create({
|
||||
params: {
|
||||
userId: user.id,
|
||||
content: url,
|
||||
spaces,
|
||||
type: type.value,
|
||||
uuid: contentId,
|
||||
url,
|
||||
},
|
||||
id: contentId,
|
||||
});
|
||||
|
||||
succeeded++;
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
progress: Math.round((processed / total) * 100),
|
||||
status: "success",
|
||||
url,
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
})}\n\n`
|
||||
);
|
||||
} catch (error) {
|
||||
failed++;
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
progress: Math.round((processed / total) * 100),
|
||||
status: "error",
|
||||
url,
|
||||
error:
|
||||
error instanceof Error ? error.message : "Unknown error",
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
})}\n\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
`data: ${JSON.stringify({
|
||||
progress: 100,
|
||||
status: "complete",
|
||||
processed,
|
||||
total,
|
||||
succeeded,
|
||||
failed,
|
||||
})}\n\n`
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default actions;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import { Card } from "../ui/card";
|
|||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { AlertCircle, CheckCircle, Clipboard, ClipboardCheckIcon, X } from "lucide-react";
|
||||
import { AlertCircle, CheckCircle, Clipboard, ClipboardCheckIcon, X, FileUpIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { type IntegrationConfig, getIntegrations } from "~/config/integrations";
|
||||
import { getChromeExtensionId } from "~/config/util";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { loader } from "~/root";
|
||||
import { CSVUploadModal } from "./CSVUploadModal";
|
||||
|
||||
function IntegrationButton({
|
||||
integration,
|
||||
|
|
@ -150,6 +151,7 @@ function Integrations() {
|
|||
const [loadingIntegration, setLoadingIntegration] = useState<IntegrationConfig | null>(null);
|
||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isCSVModalOpen, setIsCSVModalOpen] = useState(false);
|
||||
|
||||
const handleIntegrationClick = (integration: IntegrationConfig) => {
|
||||
setLoadingIntegration(integration);
|
||||
|
|
@ -254,6 +256,24 @@ function Integrations() {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 overflow-x-auto">
|
||||
<Card
|
||||
className="group relative overflow-hidden transition-all hover:shadow-lg flex-1 basis-[calc(33.333%-1rem)]"
|
||||
onClick={() => setIsCSVModalOpen(true)}
|
||||
>
|
||||
<div className="absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100 bg-gradient-to-r from-blue-500/10 to-blue-600/10" />
|
||||
<div className="relative z-10 flex flex-col items-center gap-4 p-6">
|
||||
<div className="rounded-full bg-white/10 p-3">
|
||||
<FileUpIcon className="transition-transform group-hover:scale-110 text-blue-500" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="font-semibold text-neutral-900 dark:text-white">CSV Upload</h3>
|
||||
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400 hidden md:block">
|
||||
Bulk import URLs from a CSV file
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{Object.entries(integrations).map(([key, integration]) => (
|
||||
<IntegrationButton
|
||||
key={key}
|
||||
|
|
@ -270,6 +290,8 @@ function Integrations() {
|
|||
/>
|
||||
)}
|
||||
|
||||
<CSVUploadModal isOpen={isCSVModalOpen} onClose={() => setIsCSVModalOpen(false)} />
|
||||
|
||||
<div className="mt-8 md:mt-12 text-center">
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
More integrations coming soon.{" "}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@
|
|||
"isbot": "^5.1.17",
|
||||
"lucide-react": "^0.456.0",
|
||||
"masonic": "^4.0.1",
|
||||
"postgres": "^3.4.4",
|
||||
"postgres": "^3.4.5",
|
||||
"prettier-eslint": "^16.3.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default defineConfig((mode) => {
|
|||
resolve: {
|
||||
alias: {
|
||||
...(mode.mode === "development" && {
|
||||
postgres: path.resolve(__dirname, "node_modules/postgres/src/index.js"),
|
||||
postgres: path.resolve(__dirname, "../../node_modules/postgres/src/index.js"),
|
||||
}),
|
||||
},
|
||||
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".css"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue