Repository navigation
Expand file tree
/
Copy pathexample.ts
More file actions
153 lines (147 loc) · 5.18 KB
/
Copy pathexample.ts
File metadata and controls
153 lines (147 loc) · 5.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* eslint-disable @typescript-eslint/no-unused-vars */
import { v } from "convex/values";
import { components, internal } from "./_generated/api";
import { action, internalMutation, mutation } from "./_generated/server";
import { ProsemirrorSync } from "@convex-dev/prosemirror-sync";
import { getSchema } from "@tiptap/core";
import { Transform, Step } from "@tiptap/pm/transform";
import { EditorState } from "@tiptap/pm/state";
import { extensions } from "../src/extensions";
import { Schema } from "@tiptap/pm/model";
import { BlockNoteEditor } from "@blocknote/core";
const prosemirrorSync = new ProsemirrorSync(components.prosemirrorSync);
export const {
getSnapshot,
submitSnapshot,
latestVersion,
getSteps,
submitSteps,
} = prosemirrorSync.syncApi({
checkRead(ctx, id) {
// const user = await userFromAuth(ctx);
// ...validate that the user can read this document
},
checkWrite(ctx, id) {
// const user = await userFromAuth(ctx);
// ...validate that the user can write to this document
},
async onSnapshot(ctx, id, snapshot, version) {
// ...do something with the snapshot, like store a copy in another table,
// save a text version of the document for text search, or generate
// embeddings for vector search.
const snapshotJSON = JSON.parse(snapshot);
let schema: Schema;
// A hack for our demo which uses both BlockNote and Tiptap.
if (id.endsWith("-blocknote")) {
// Parse the snapshot from BlockNote in the Convex runtime.
// ServerBlockNoteEditor only works in "use node" in the Convex runtime.
const editor = BlockNoteEditor.create();
schema = editor.pmSchema;
} else {
// Fetching the content from Tiptap, using your extensions.
schema = getSchema(extensions);
}
const node = schema.nodeFromJSON(snapshotJSON);
const content = node.textContent;
await ctx.scheduler.runAfter(0, internal.example.updateDocSearchIndex, {
id,
content,
});
},
});
async function generateContent(doc: string) {
return `Overall length: ${doc.length}`;
}
// We keep a text search index on the documents table for easy search.
// But we don't update that full version all the time, for efficiency.
export const updateDocSearchIndex = internalMutation({
args: { id: v.string(), content: v.string() },
handler: async (ctx, { id, content }) => {
const existing = await ctx.db
.query("documents")
.withIndex("docId", (q) => q.eq("docId", id))
.unique();
if (!existing) {
await ctx.db.insert("documents", { docId: id, content });
} else {
await ctx.db.patch("documents", existing._id, { content });
}
},
});
// This is an example of how to modify a document using the transform fn.
export const transformExample = action({
args: { id: v.string() },
handler: async (ctx, { id }) => {
const schema = getSchema(extensions);
const { doc, version } = await prosemirrorSync.getDoc(ctx, id, schema);
const newContent = await generateContent(doc.textContent);
const node = await prosemirrorSync.transform(ctx, id, schema, (doc, v) => {
if (v !== version) {
// If we wanted to avoid making changes, we could return null here.
// Or we could rebase our changes on top of the new document.
}
const tr = EditorState.create({ doc }).tr;
return tr.insertText(newContent, 0);
});
await ctx.scheduler.runAfter(0, internal.example.updateDocSearchIndex, {
id,
content: node.textContent,
});
},
});
// This is an example of how to manually transform the document.
export const manualTransform = mutation({
args: {
id: v.string(),
text: v.string(),
},
handler: async (ctx, { id, text }) => {
const snapshot = await ctx.runQuery(
components.prosemirrorSync.lib.getSnapshot,
{
id,
},
);
if (!snapshot.content) {
throw new Error("Document not found");
}
const content = JSON.parse(snapshot.content);
const schema = getSchema(extensions);
const serverVersion = new Transform(schema.nodeFromJSON(content));
const stepsResult = await ctx.runQuery(
components.prosemirrorSync.lib.getSteps,
{ id, version: snapshot.version },
);
if (stepsResult.steps.length > 0) {
for (const step of stepsResult.steps) {
serverVersion.step(Step.fromJSON(schema, JSON.parse(step)));
}
}
let version = stepsResult.version;
while (true) {
const tr = new Transform(serverVersion.doc);
tr.insert(0, schema.text(text));
const result = await ctx.runMutation(
components.prosemirrorSync.lib.submitSteps,
{
id,
clientId: "server function",
version,
steps: tr.steps.map((step) => JSON.stringify(step.toJSON())),
},
);
if (result.status === "synced") {
await ctx.runMutation(components.prosemirrorSync.lib.submitSnapshot, {
id,
version: version + tr.steps.length,
content: JSON.stringify(tr.doc.toJSON()),
});
return tr.doc.toJSON();
}
for (const step of result.steps) {
serverVersion.step(Step.fromJSON(schema, JSON.parse(step)));
}
version += result.steps.length;
}
},
});