fix: postgres error

This commit is contained in:
Dhravya Shah 2025-01-22 17:23:22 -07:00
parent a6e6c06120
commit 0c0ea871c3
5 changed files with 283 additions and 16 deletions

View file

@ -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>

View file

@ -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;

View file

@ -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.{" "}

View file

@ -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",

View file

@ -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"],