Skip to content

Commit bb6575b

Browse files
committed
feat(document): embedding task table
1 parent 3784771 commit bb6575b

File tree

13 files changed

+131
-42
lines changed

13 files changed

+131
-42
lines changed

apps/drizzle-studio/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@acme/drizzle-studio",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "NODE_ENV=development pnpm with-env pnpm --filter=db studio",
8+
"with-env": "dotenv -e ../../.env --"
9+
},
10+
"devDependencies": {
11+
"@acme/db": "workspace:*",
12+
"dotenv-cli": "^8.0.0"
13+
}
14+
}

apps/drizzle-studio/turbo.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://turborepo.org/schema.json",
3+
"extends": ["//"],
4+
"tasks": {
5+
"dev": {
6+
"persistent": true
7+
}
8+
}
9+
}

apps/localtunnel/src/middleware/use-nextjs-proxy.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,42 +22,18 @@ import { signPayload } from "../lib/sign-payload.ts";
2222
export function useNextjsProxy(app: express.Application) {
2323
app.use(async (req, res) => {
2424
try {
25-
const { table } = req.body;
2625
const payload = JSON.stringify(req.body);
2726
const signature = signPayload(payload, env.SUPABASE_SECRET);
28-
let webhookEndpoint = `${env.NEXT_JS_URL}/api/webhooks/journal-entries`;
29-
switch (table) {
30-
case "journal_entry": {
31-
webhookEndpoint = `${env.NEXT_JS_URL}/api/webhooks/journal-entries`;
32-
break;
33-
}
34-
case "page": {
35-
webhookEndpoint = `${env.NEXT_JS_URL}/api/webhooks/page`;
36-
break;
37-
}
38-
default:
39-
break;
40-
}
41-
// Forward the request to Next.js server
42-
const response = await fetch(webhookEndpoint, {
27+
const webhookEndpoint = new URL(req.url, env.NEXT_JS_URL);
28+
// Proxy the request to Next.js server
29+
void fetch(webhookEndpoint, {
4330
body: payload,
4431
headers: {
4532
"Content-Type": "application/json",
4633
"x-supabase-signature": signature,
4734
},
4835
method: req.method,
4936
});
50-
51-
// Forward the response back to the client
52-
const responseBody = await response.text();
53-
res.status(response.status);
54-
55-
// Forward response headers
56-
response.headers.forEach((value, key) => {
57-
res.setHeader(key, value);
58-
});
59-
60-
return res.send(responseBody);
6137
} catch (error) {
6238
console.error("Error proxying webhook:", error);
6339
res.status(500).json({ error: "Failed to proxy webhook" });

apps/web/src/app/(app)/pages/_components/page-editor.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export function PageEditor({
2525

2626
const { mutate, isPending } = useMutation({
2727
...trpc.blocks.saveTransactions.mutationOptions({}),
28+
// ! TODO: When the mutation fails we need to revert the changes to the editor just like Notion does.
29+
// ! To do this we can use `onError` and `editor.undo()`, without calling the transactions. We might have to get creative.
30+
// ! Maybe we can refetch the blocks after an error instead of `undo`?
2831
onSuccess: () => {
2932
if (pendingChangesRef.current.length > 0) {
3033
debouncedMutate();

packages/api/src/router/blocks.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { and, eq } from "@acme/db";
1+
import { and, eq, sql } from "@acme/db";
22
import {
33
BlockEdge,
44
BlockNode,
5+
DocumentEmbeddingTask,
56
zInsertBlockEdge,
67
zInsertBlockNode,
78
} from "@acme/db/schema";
@@ -95,6 +96,21 @@ export const blocksRouter = {
9596
});
9697
}
9798
}
99+
100+
// Anytime we save a transaction we need to update the document embedding task status to debounced.
101+
await tx
102+
.insert(DocumentEmbeddingTask)
103+
.values({
104+
document_id: input.document_id,
105+
user_id: ctx.session.user.id,
106+
})
107+
.onConflictDoUpdate({
108+
set: {
109+
status: "debounced",
110+
},
111+
target: [DocumentEmbeddingTask.document_id],
112+
targetWhere: sql`${DocumentEmbeddingTask.status} != 'completed'`,
113+
});
98114
});
99115
}),
100116
};

packages/db/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"check": "biome check",
2323
"clean": "git clean -xdf .cache .turbo dist node_modules",
2424
"dev": "pnpm build --watch",
25+
"prepush": "pnpm build",
2526
"push": "pnpm with-env drizzle-kit push",
2627
"studio": "pnpm with-env drizzle-kit studio",
2728
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { sql } from "drizzle-orm";
2+
import { pgEnum, pgTable, text, uniqueIndex } from "drizzle-orm/pg-core";
3+
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
4+
import { z } from "zod/v4";
5+
import { user } from "../auth/user.schema.js";
6+
import { Document } from "./document.schema.js";
7+
8+
export const DocumentEmbeddingTaskStatus = pgEnum(
9+
"document_embedding_task_status",
10+
["debounced", "ready", "running", "completed", "failed"],
11+
);
12+
13+
export const DocumentEmbeddingTask = pgTable(
14+
"document_embedding_task",
15+
(t) => ({
16+
id: t.uuid().notNull().primaryKey().defaultRandom(),
17+
user_id: text()
18+
.notNull()
19+
.references(() => user.id, { onDelete: "cascade" }),
20+
document_id: t
21+
.uuid()
22+
.notNull()
23+
.references(() => Document.id, { onDelete: "cascade" }),
24+
status: DocumentEmbeddingTaskStatus().notNull().default("debounced"),
25+
created_at: t
26+
.timestamp({ mode: "string", withTimezone: true })
27+
.defaultNow()
28+
.notNull(),
29+
updated_at: t
30+
.timestamp({ mode: "string", withTimezone: true })
31+
.defaultNow()
32+
.notNull()
33+
.$onUpdateFn(() => sql`now()`),
34+
}),
35+
(t) => [
36+
// Unique constraint: only one non-completed task per page
37+
// This allows multiple completed tasks but prevents multiple active tasks
38+
uniqueIndex("unique_non_completed_task_per_page")
39+
.on(t.document_id)
40+
.where(sql`${t.status} != 'completed'`),
41+
],
42+
);
43+
44+
export type DocumentEmbeddingTask = typeof DocumentEmbeddingTask.$inferSelect;
45+
46+
export const zInsertDocumentEmbeddingTask = createInsertSchema(
47+
DocumentEmbeddingTask,
48+
{
49+
document_id: z.uuid(),
50+
},
51+
).omit({
52+
created_at: true,
53+
id: true,
54+
updated_at: true,
55+
});
56+
57+
export const zUpdateDocumentEmbeddingTask = createInsertSchema(
58+
DocumentEmbeddingTask,
59+
{
60+
document_id: z.uuid(),
61+
},
62+
).omit({
63+
created_at: true,
64+
id: true,
65+
updated_at: true,
66+
user_id: true,
67+
});
68+
69+
export const zDocumentEmbeddingTask = createSelectSchema(DocumentEmbeddingTask);

packages/db/src/core/page-embedding.schema.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { createSelectSchema } from "drizzle-zod";
44
import { user } from "../auth/user.schema.js";
55
import { Page } from "./page.schema.js";
66

7+
const EMBEDDING_DIMENSIONS = 1536;
8+
79
export const PageEmbedding = pgTable(
810
"page_embedding",
911
(t) => ({
@@ -23,7 +25,7 @@ export const PageEmbedding = pgTable(
2325
// Hash of the entire page's text content for change detection
2426
page_text_hash: t.text().notNull(),
2527
// The actual embedding vector for this chunk
26-
embedding: vector({ dimensions: 1536 }).notNull(),
28+
embedding: vector({ dimensions: EMBEDDING_DIMENSIONS }).notNull(),
2729
created_at: t
2830
.timestamp({ mode: "string", withTimezone: true })
2931
.defaultNow()

packages/db/src/core/page.schema.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ export const Page = pgTable("page", (t) => ({
2525
.defaultNow()
2626
.notNull()
2727
.$onUpdateFn(() => sql`now()`),
28-
// Separate timestamp for embedding triggers (debounced longer)
29-
embed_updated_at: t
30-
.timestamp({ mode: "string", withTimezone: true })
31-
.defaultNow()
32-
.notNull(),
3328
}));
3429

3530
export type Page = typeof Page.$inferSelect;

0 commit comments

Comments
 (0)