react-embed-docs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/client/components/Breadcrumbs.d.ts +21 -0
- package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/client/components/Breadcrumbs.js +123 -0
- package/dist/client/components/DocsLayout.d.ts +20 -0
- package/dist/client/components/DocsLayout.d.ts.map +1 -0
- package/dist/client/components/DocsLayout.js +387 -0
- package/dist/client/components/DocumentContent.d.ts +5 -0
- package/dist/client/components/DocumentContent.d.ts.map +1 -0
- package/dist/client/components/DocumentContent.js +15 -0
- package/dist/client/components/DocumentEdit.d.ts +6 -0
- package/dist/client/components/DocumentEdit.d.ts.map +1 -0
- package/dist/client/components/DocumentEdit.js +153 -0
- package/dist/client/components/DocumentList.d.ts +5 -0
- package/dist/client/components/DocumentList.d.ts.map +1 -0
- package/dist/client/components/DocumentList.js +39 -0
- package/dist/client/components/DocumentProvider.d.ts +42 -0
- package/dist/client/components/DocumentProvider.d.ts.map +1 -0
- package/dist/client/components/DocumentProvider.js +47 -0
- package/dist/client/components/DocumentView.d.ts +6 -0
- package/dist/client/components/DocumentView.d.ts.map +1 -0
- package/dist/client/components/DocumentView.js +58 -0
- package/dist/client/components/DragOverlayItem.d.ts +5 -0
- package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
- package/dist/client/components/DragOverlayItem.js +9 -0
- package/dist/client/components/EmojiPicker.d.ts +8 -0
- package/dist/client/components/EmojiPicker.d.ts.map +1 -0
- package/dist/client/components/EmojiPicker.js +48 -0
- package/dist/client/components/ExportButton.d.ts +22 -0
- package/dist/client/components/ExportButton.d.ts.map +1 -0
- package/dist/client/components/ExportButton.js +97 -0
- package/dist/client/components/Layout.d.ts +7 -0
- package/dist/client/components/Layout.d.ts.map +1 -0
- package/dist/client/components/Layout.js +172 -0
- package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
- package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
- package/dist/client/components/ReactEmbedDocs.js +8 -0
- package/dist/client/components/SearchInput.d.ts +2 -0
- package/dist/client/components/SearchInput.d.ts.map +1 -0
- package/dist/client/components/SearchInput.js +7 -0
- package/dist/client/components/Sidebar.d.ts +10 -0
- package/dist/client/components/Sidebar.d.ts.map +1 -0
- package/dist/client/components/Sidebar.js +176 -0
- package/dist/client/components/SortableTreeItem.d.ts +13 -0
- package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
- package/dist/client/components/SortableTreeItem.js +24 -0
- package/dist/client/components/VersionHistory.d.ts +14 -0
- package/dist/client/components/VersionHistory.d.ts.map +1 -0
- package/dist/client/components/VersionHistory.js +102 -0
- package/dist/client/hooks/useCollaboration.d.ts +99 -0
- package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
- package/dist/client/hooks/useCollaboration.js +180 -0
- package/dist/client/hooks/useDocsQuery.d.ts +84 -0
- package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
- package/dist/client/hooks/useDocsQuery.js +241 -0
- package/dist/client/hooks/useExport.d.ts +31 -0
- package/dist/client/hooks/useExport.d.ts.map +1 -0
- package/dist/client/hooks/useExport.js +66 -0
- package/dist/client/hooks/useFileUpload.d.ts +44 -0
- package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/client/hooks/useFileUpload.js +193 -0
- package/dist/client/hooks/useSystemTheme.d.ts +2 -0
- package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
- package/dist/client/hooks/useSystemTheme.js +19 -0
- package/dist/client/hooks/useVersions.d.ts +105 -0
- package/dist/client/hooks/useVersions.d.ts.map +1 -0
- package/dist/client/hooks/useVersions.js +129 -0
- package/dist/client/index.d.ts +23 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/lib/blocknoteTheme.d.ts +13 -0
- package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
- package/dist/client/lib/blocknoteTheme.js +76 -0
- package/dist/client/lib/path.d.ts +8 -0
- package/dist/client/lib/path.d.ts.map +1 -0
- package/dist/client/lib/path.js +30 -0
- package/dist/client/providers/DocumentProvider.d.ts +1 -0
- package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
- package/dist/client/providers/DocumentProvider.js +1 -0
- package/dist/server/CollaborationService.d.ts +134 -0
- package/dist/server/CollaborationService.d.ts.map +1 -0
- package/dist/server/CollaborationService.js +307 -0
- package/dist/server/DocsService.d.ts +115 -0
- package/dist/server/DocsService.d.ts.map +1 -0
- package/dist/server/DocsService.js +512 -0
- package/dist/server/ExportService.d.ts +106 -0
- package/dist/server/ExportService.d.ts.map +1 -0
- package/dist/server/ExportService.js +501 -0
- package/dist/server/FilesService.d.ts +44 -0
- package/dist/server/FilesService.d.ts.map +1 -0
- package/dist/server/FilesService.js +78 -0
- package/dist/server/VersioningService.d.ts +112 -0
- package/dist/server/VersioningService.d.ts.map +1 -0
- package/dist/server/VersioningService.js +264 -0
- package/dist/server/db.d.ts +7 -0
- package/dist/server/db.d.ts.map +1 -0
- package/dist/server/db.js +22 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +36 -0
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +483 -0
- package/dist/server/schema.d.ts +587 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +126 -0
- package/dist/shared/types.d.ts +314 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +48 -0
- package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
- package/drizzle/migrations/meta/0000_snapshot.json +574 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/package.json +109 -0
- package/styles/docs.css +981 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { and, asc, count, desc, eq, getTableColumns, isNull, sql, } from 'drizzle-orm';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { documentsTable } from './schema.js';
|
|
4
|
+
/**
|
|
5
|
+
* DocsService
|
|
6
|
+
* Handles all CRUD operations for documents with BlockNote content
|
|
7
|
+
*/
|
|
8
|
+
export class DocsService {
|
|
9
|
+
db;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* List documents with filtering and pagination
|
|
15
|
+
* Returns documents without content field to reduce payload size
|
|
16
|
+
*/
|
|
17
|
+
async list(filters = {}, options = {}) {
|
|
18
|
+
const { offset = 0, limit = 10, sortBy = 'createdAt', sortDir = 'desc', } = options;
|
|
19
|
+
const { search, parentId, isPublished } = filters;
|
|
20
|
+
const where = [isNull(documentsTable.deletedAt)];
|
|
21
|
+
if (search) {
|
|
22
|
+
// Search in both title and content using PostgreSQL JSONB operators
|
|
23
|
+
const searchPattern = `%${search}%`;
|
|
24
|
+
where.push(sql `${documentsTable.title} ILIKE ${searchPattern} OR
|
|
25
|
+
jsonb_path_exists(${documentsTable.content},
|
|
26
|
+
('$[*].content ? (@.type == "text" && @.text like_regex "' || ${search} || '" flag "i")')::jsonpath) OR
|
|
27
|
+
jsonb_path_exists(${documentsTable.content},
|
|
28
|
+
('$[*].content[*].text ? (@ like_regex "' || ${search} || '" flag "i")')::jsonpath)`);
|
|
29
|
+
}
|
|
30
|
+
if (parentId !== undefined) {
|
|
31
|
+
if (parentId === null) {
|
|
32
|
+
where.push(isNull(documentsTable.parentId));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
where.push(eq(documentsTable.parentId, parentId));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (isPublished !== undefined) {
|
|
39
|
+
where.push(eq(documentsTable.isPublished, isPublished));
|
|
40
|
+
}
|
|
41
|
+
const totalQuery = this.db
|
|
42
|
+
.select({ count: count() })
|
|
43
|
+
.from(documentsTable)
|
|
44
|
+
.where(and(...where));
|
|
45
|
+
const columns = getTableColumns(documentsTable);
|
|
46
|
+
const sortColumn = columns[sortBy] ?? columns.createdAt;
|
|
47
|
+
const orderByClause = sortDir === 'asc' ? asc(sortColumn) : desc(sortColumn);
|
|
48
|
+
const documents = await this.db.query.documentsTable.findMany({
|
|
49
|
+
where: and(...where),
|
|
50
|
+
orderBy: orderByClause,
|
|
51
|
+
limit,
|
|
52
|
+
offset,
|
|
53
|
+
columns: {
|
|
54
|
+
id: true,
|
|
55
|
+
title: true,
|
|
56
|
+
slug: true,
|
|
57
|
+
emoji: true,
|
|
58
|
+
cover: true,
|
|
59
|
+
isPublished: true,
|
|
60
|
+
parentId: true,
|
|
61
|
+
order: true,
|
|
62
|
+
authorId: true,
|
|
63
|
+
createdAt: true,
|
|
64
|
+
updatedAt: true,
|
|
65
|
+
deletedAt: true,
|
|
66
|
+
// Exclude: content, searchIndex
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const totalResult = await totalQuery;
|
|
70
|
+
return {
|
|
71
|
+
documents: documents,
|
|
72
|
+
total: totalResult[0]?.count ?? 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Full-text search with ranking using searchIndex column
|
|
77
|
+
* More efficient than JSONB search for large datasets
|
|
78
|
+
*/
|
|
79
|
+
async searchText(query, options = {}) {
|
|
80
|
+
const { limit = 20, offset = 0 } = options;
|
|
81
|
+
const searchPattern = `%${query}%`;
|
|
82
|
+
// Use ILIKE on searchIndex column and also check title separately for ranking
|
|
83
|
+
const results = await this.db.execute(sql `
|
|
84
|
+
SELECT
|
|
85
|
+
d.*,
|
|
86
|
+
CASE
|
|
87
|
+
WHEN d.title ILIKE ${searchPattern} THEN 2
|
|
88
|
+
WHEN d.search_index ILIKE ${searchPattern} THEN 1
|
|
89
|
+
ELSE 0
|
|
90
|
+
END as rank
|
|
91
|
+
FROM docs.documents d
|
|
92
|
+
WHERE d.deleted_at IS NULL
|
|
93
|
+
AND d.search_index ILIKE ${searchPattern}
|
|
94
|
+
ORDER BY rank DESC, d.updated_at DESC
|
|
95
|
+
LIMIT ${limit}
|
|
96
|
+
OFFSET ${offset}
|
|
97
|
+
`);
|
|
98
|
+
// Get total count
|
|
99
|
+
const countResult = await this.db.execute(sql `
|
|
100
|
+
SELECT COUNT(*) as total
|
|
101
|
+
FROM docs.documents d
|
|
102
|
+
WHERE d.deleted_at IS NULL
|
|
103
|
+
AND d.search_index ILIKE ${searchPattern}
|
|
104
|
+
`);
|
|
105
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
106
|
+
// Map results
|
|
107
|
+
const mappedResults = results.map((row) => ({
|
|
108
|
+
document: row,
|
|
109
|
+
rank: Number(row.rank),
|
|
110
|
+
}));
|
|
111
|
+
return { results: mappedResults, total };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Advanced search with highlighting
|
|
115
|
+
* Searches JSONB content directly for more precise matching
|
|
116
|
+
*/
|
|
117
|
+
async search(query, options = {}) {
|
|
118
|
+
const { limit = 20, offset = 0 } = options;
|
|
119
|
+
const searchPattern = `%${query}%`;
|
|
120
|
+
// Use raw SQL for complex search with ranking
|
|
121
|
+
const results = await this.db.execute(sql `
|
|
122
|
+
SELECT
|
|
123
|
+
d.*,
|
|
124
|
+
CASE
|
|
125
|
+
WHEN d.title ILIKE ${searchPattern} THEN 3
|
|
126
|
+
WHEN jsonb_path_exists(d.content, ('$[*].content ? (@.type == "text" && @.text like_regex "' || ${query} || '" flag "i")')::jsonpath) THEN 2
|
|
127
|
+
WHEN jsonb_path_exists(d.content, ('$[*].content[*].text ? (@ like_regex "' || ${query} || '" flag "i")')::jsonpath) THEN 1
|
|
128
|
+
ELSE 0
|
|
129
|
+
END as rank
|
|
130
|
+
FROM docs.documents d
|
|
131
|
+
WHERE d.deleted_at IS NULL
|
|
132
|
+
AND (
|
|
133
|
+
d.title ILIKE ${searchPattern}
|
|
134
|
+
OR jsonb_path_exists(d.content, ('$[*].content ? (@.type == "text" && @.text like_regex "' || ${query} || '" flag "i")')::jsonpath)
|
|
135
|
+
OR jsonb_path_exists(d.content, ('$[*].content[*].text ? (@ like_regex "' || ${query} || '" flag "i")')::jsonpath)
|
|
136
|
+
)
|
|
137
|
+
ORDER BY rank DESC, d.updated_at DESC
|
|
138
|
+
LIMIT ${limit}
|
|
139
|
+
OFFSET ${offset}
|
|
140
|
+
`);
|
|
141
|
+
// Get total count
|
|
142
|
+
const countResult = await this.db.execute(sql `
|
|
143
|
+
SELECT COUNT(*) as total
|
|
144
|
+
FROM docs.documents d
|
|
145
|
+
WHERE d.deleted_at IS NULL
|
|
146
|
+
AND (
|
|
147
|
+
d.title ILIKE ${searchPattern}
|
|
148
|
+
OR jsonb_path_exists(d.content, ('$[*].content ? (@.type == "text" && @.text like_regex "' || ${query} || '" flag "i")')::jsonpath)
|
|
149
|
+
OR jsonb_path_exists(d.content, ('$[*].content[*].text ? (@ like_regex "' || ${query} || '" flag "i")')::jsonpath)
|
|
150
|
+
)
|
|
151
|
+
`);
|
|
152
|
+
const total = Number(countResult[0]?.total ?? 0);
|
|
153
|
+
// Map results to SearchResult format
|
|
154
|
+
const mappedResults = results.map((row) => ({
|
|
155
|
+
document: row,
|
|
156
|
+
rank: Number(row.rank),
|
|
157
|
+
highlights: this.extractHighlights(row.content, query),
|
|
158
|
+
}));
|
|
159
|
+
return { results: mappedResults, total };
|
|
160
|
+
}
|
|
161
|
+
extractFromContentItem(queryLower, item) {
|
|
162
|
+
const highlights = [];
|
|
163
|
+
if (item.text && item.text.toLowerCase().includes(queryLower)) {
|
|
164
|
+
// Extract surrounding context (50 chars before and after)
|
|
165
|
+
const text = item.text;
|
|
166
|
+
const index = text.toLowerCase().indexOf(queryLower);
|
|
167
|
+
const start = Math.max(0, index - 50);
|
|
168
|
+
const end = Math.min(text.length, index + queryLower.length + 50);
|
|
169
|
+
const context = text.substring(start, end);
|
|
170
|
+
highlights.push(context);
|
|
171
|
+
// Limit to 3 highlights
|
|
172
|
+
if (highlights.length >= 3)
|
|
173
|
+
return highlights;
|
|
174
|
+
}
|
|
175
|
+
return highlights;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Extract text highlights from BlockNote content
|
|
179
|
+
* Returns surrounding context around matched text
|
|
180
|
+
*/
|
|
181
|
+
extractHighlights(content, query) {
|
|
182
|
+
const highlights = [];
|
|
183
|
+
const queryLower = query.toLowerCase();
|
|
184
|
+
if (!Array.isArray(content))
|
|
185
|
+
return highlights;
|
|
186
|
+
for (const block of content) {
|
|
187
|
+
if (block.content && Array.isArray(block.content)) {
|
|
188
|
+
for (const item of block.content) {
|
|
189
|
+
if (Array.isArray(item)) {
|
|
190
|
+
// Nested content arrays
|
|
191
|
+
for (const nestedItem of item) {
|
|
192
|
+
const results = this.extractFromContentItem(queryLower, nestedItem);
|
|
193
|
+
if (results.length > 0) {
|
|
194
|
+
highlights.push(...results);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const results = this.extractFromContentItem(queryLower, item);
|
|
200
|
+
if (results.length > 0) {
|
|
201
|
+
highlights.push(...results);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (highlights.length >= 3)
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
return highlights;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Extract plain text from BlockNote content
|
|
212
|
+
* Used for building search index
|
|
213
|
+
*/
|
|
214
|
+
extractTextFromContent(content) {
|
|
215
|
+
const textParts = [];
|
|
216
|
+
function extractFromContentItem(item) {
|
|
217
|
+
if (item.text && typeof item.text === 'string') {
|
|
218
|
+
textParts.push(item.text);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function extractFromBlock(block) {
|
|
222
|
+
if (!block.content || !Array.isArray(block.content))
|
|
223
|
+
return;
|
|
224
|
+
for (const contentItem of block.content) {
|
|
225
|
+
if (Array.isArray(contentItem)) {
|
|
226
|
+
// Nested content arrays
|
|
227
|
+
for (const nestedItem of contentItem) {
|
|
228
|
+
extractFromContentItem(nestedItem);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
extractFromContentItem(contentItem);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!Array.isArray(content)) {
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
for (const block of content) {
|
|
240
|
+
if (block && typeof block === 'object') {
|
|
241
|
+
extractFromBlock(block);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return textParts.join(' ');
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build search index from title and content
|
|
248
|
+
* Concatenates title and extracted text for full-text search
|
|
249
|
+
*/
|
|
250
|
+
buildSearchIndex(title, content) {
|
|
251
|
+
const contentText = this.extractTextFromContent(content);
|
|
252
|
+
return `${title} ${contentText}`.trim();
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get document by ID
|
|
256
|
+
*/
|
|
257
|
+
async getById(id) {
|
|
258
|
+
const document = await this.db.query.documentsTable.findFirst({
|
|
259
|
+
where: and(eq(documentsTable.id, id), isNull(documentsTable.deletedAt)),
|
|
260
|
+
});
|
|
261
|
+
return document;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get document by slug
|
|
265
|
+
*/
|
|
266
|
+
async getBySlug(slug) {
|
|
267
|
+
const document = await this.db.query.documentsTable.findFirst({
|
|
268
|
+
where: and(eq(documentsTable.slug, slug), isNull(documentsTable.deletedAt)),
|
|
269
|
+
});
|
|
270
|
+
return document;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Create new document
|
|
274
|
+
* Automatically generates search index from title and content
|
|
275
|
+
*/
|
|
276
|
+
async create(data) {
|
|
277
|
+
const id = nanoid();
|
|
278
|
+
const searchIndex = this.buildSearchIndex(data.title, data.content);
|
|
279
|
+
let parentId = null;
|
|
280
|
+
if (data.parentSlugs) {
|
|
281
|
+
for (const parentSlug of data.parentSlugs) {
|
|
282
|
+
const parent = await this.getBySlug(parentSlug);
|
|
283
|
+
if (!parent) {
|
|
284
|
+
throw new Error(`Parent document not found: ${parentSlug}`);
|
|
285
|
+
}
|
|
286
|
+
parentId = parent.id;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const [result] = await this.db
|
|
290
|
+
.insert(documentsTable)
|
|
291
|
+
.values({
|
|
292
|
+
...data,
|
|
293
|
+
parentId,
|
|
294
|
+
id,
|
|
295
|
+
searchIndex,
|
|
296
|
+
createdAt: new Date(),
|
|
297
|
+
updatedAt: new Date(),
|
|
298
|
+
})
|
|
299
|
+
.returning();
|
|
300
|
+
if (!result) {
|
|
301
|
+
throw new Error('Failed to create document');
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Update existing document
|
|
307
|
+
* Rebuilds search index if title or content changes
|
|
308
|
+
*/
|
|
309
|
+
async update(id, data) {
|
|
310
|
+
// If title or content is being updated, we need to rebuild the searchIndex
|
|
311
|
+
let searchIndex;
|
|
312
|
+
if (data.title !== undefined || data.content !== undefined) {
|
|
313
|
+
// Get current document to have both values
|
|
314
|
+
const currentDoc = await this.getById(id);
|
|
315
|
+
if (!currentDoc) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const title = data.title ?? currentDoc.title;
|
|
319
|
+
const content = data.content ?? currentDoc.content;
|
|
320
|
+
searchIndex = this.buildSearchIndex(title, content);
|
|
321
|
+
}
|
|
322
|
+
const [result] = await this.db
|
|
323
|
+
.update(documentsTable)
|
|
324
|
+
.set({
|
|
325
|
+
...data,
|
|
326
|
+
...(searchIndex !== undefined && { searchIndex }),
|
|
327
|
+
updatedAt: new Date(),
|
|
328
|
+
})
|
|
329
|
+
.where(and(eq(documentsTable.id, id), isNull(documentsTable.deletedAt)))
|
|
330
|
+
.returning();
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Soft delete document
|
|
335
|
+
* Sets deletedAt timestamp instead of removing from database
|
|
336
|
+
*/
|
|
337
|
+
async delete(id) {
|
|
338
|
+
const [result] = await this.db
|
|
339
|
+
.update(documentsTable)
|
|
340
|
+
.set({
|
|
341
|
+
deletedAt: new Date(),
|
|
342
|
+
})
|
|
343
|
+
.where(and(eq(documentsTable.id, id), isNull(documentsTable.deletedAt)))
|
|
344
|
+
.returning();
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get children documents by parent ID
|
|
349
|
+
* Returns documents without content field
|
|
350
|
+
*/
|
|
351
|
+
async getChildren(parentId) {
|
|
352
|
+
const children = await this.db.query.documentsTable.findMany({
|
|
353
|
+
where: and(eq(documentsTable.parentId, parentId), isNull(documentsTable.deletedAt)),
|
|
354
|
+
orderBy: asc(documentsTable.order),
|
|
355
|
+
columns: {
|
|
356
|
+
id: true,
|
|
357
|
+
title: true,
|
|
358
|
+
slug: true,
|
|
359
|
+
emoji: true,
|
|
360
|
+
cover: true,
|
|
361
|
+
isPublished: true,
|
|
362
|
+
parentId: true,
|
|
363
|
+
order: true,
|
|
364
|
+
authorId: true,
|
|
365
|
+
createdAt: true,
|
|
366
|
+
updatedAt: true,
|
|
367
|
+
deletedAt: true,
|
|
368
|
+
// Exclude: content, searchIndex
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
return children;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get all documents as flat tree structure
|
|
375
|
+
* Useful for building document tree UI
|
|
376
|
+
* Returns documents without content field
|
|
377
|
+
*/
|
|
378
|
+
async getTree() {
|
|
379
|
+
const allDocs = await this.db.query.documentsTable.findMany({
|
|
380
|
+
where: isNull(documentsTable.deletedAt),
|
|
381
|
+
orderBy: [asc(documentsTable.parentId), asc(documentsTable.order)],
|
|
382
|
+
columns: {
|
|
383
|
+
id: true,
|
|
384
|
+
title: true,
|
|
385
|
+
slug: true,
|
|
386
|
+
emoji: true,
|
|
387
|
+
cover: true,
|
|
388
|
+
isPublished: true,
|
|
389
|
+
parentId: true,
|
|
390
|
+
order: true,
|
|
391
|
+
authorId: true,
|
|
392
|
+
createdAt: true,
|
|
393
|
+
updatedAt: true,
|
|
394
|
+
deletedAt: true,
|
|
395
|
+
// Exclude: content, searchIndex
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
return allDocs;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Reorder document - move to new parent and/or position
|
|
402
|
+
* Automatically reorders siblings to maintain consistent ordering
|
|
403
|
+
*/
|
|
404
|
+
async reorder(id, newParentId, newOrder) {
|
|
405
|
+
// Get the current document
|
|
406
|
+
const doc = await this.getById(id);
|
|
407
|
+
if (!doc) {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
const oldParentId = doc.parentId;
|
|
411
|
+
// Update the moved document
|
|
412
|
+
const [updated] = await this.db
|
|
413
|
+
.update(documentsTable)
|
|
414
|
+
.set({
|
|
415
|
+
parentId: newParentId,
|
|
416
|
+
order: newOrder,
|
|
417
|
+
updatedAt: new Date(),
|
|
418
|
+
})
|
|
419
|
+
.where(and(eq(documentsTable.id, id), isNull(documentsTable.deletedAt)))
|
|
420
|
+
.returning();
|
|
421
|
+
// Reorder other documents in the old parent if parent changed
|
|
422
|
+
if (oldParentId !== undefined && oldParentId !== newParentId) {
|
|
423
|
+
const siblings = await this.db.query.documentsTable.findMany({
|
|
424
|
+
where: and(oldParentId === null
|
|
425
|
+
? isNull(documentsTable.parentId)
|
|
426
|
+
: eq(documentsTable.parentId, oldParentId), isNull(documentsTable.deletedAt)),
|
|
427
|
+
orderBy: asc(documentsTable.order),
|
|
428
|
+
});
|
|
429
|
+
// Reassign orders to siblings with spacing for future insertions
|
|
430
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
431
|
+
const sibling = siblings[i];
|
|
432
|
+
if (sibling) {
|
|
433
|
+
await this.db
|
|
434
|
+
.update(documentsTable)
|
|
435
|
+
.set({ order: i * 10 })
|
|
436
|
+
.where(eq(documentsTable.id, sibling.id));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Reorder documents in the new parent
|
|
441
|
+
const newSiblings = await this.db.query.documentsTable.findMany({
|
|
442
|
+
where: and(newParentId === null
|
|
443
|
+
? isNull(documentsTable.parentId)
|
|
444
|
+
: eq(documentsTable.parentId, newParentId), isNull(documentsTable.deletedAt)),
|
|
445
|
+
orderBy: asc(documentsTable.order),
|
|
446
|
+
});
|
|
447
|
+
// Reassign orders to ensure proper spacing
|
|
448
|
+
for (let i = 0; i < newSiblings.length; i++) {
|
|
449
|
+
const sibling = newSiblings[i];
|
|
450
|
+
if (sibling) {
|
|
451
|
+
await this.db
|
|
452
|
+
.update(documentsTable)
|
|
453
|
+
.set({ order: i * 10 })
|
|
454
|
+
.where(eq(documentsTable.id, sibling.id));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return updated;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Update just the order field of a document
|
|
461
|
+
*/
|
|
462
|
+
async updateOrder(id, order) {
|
|
463
|
+
const [result] = await this.db
|
|
464
|
+
.update(documentsTable)
|
|
465
|
+
.set({
|
|
466
|
+
order,
|
|
467
|
+
updatedAt: new Date(),
|
|
468
|
+
})
|
|
469
|
+
.where(and(eq(documentsTable.id, id), isNull(documentsTable.deletedAt)))
|
|
470
|
+
.returning();
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get breadcrumb path for a document
|
|
475
|
+
* Returns array of ancestors from root to parent, plus the document itself
|
|
476
|
+
* Returns documents without content field
|
|
477
|
+
*/
|
|
478
|
+
async getBreadcrumbs(id) {
|
|
479
|
+
const breadcrumbs = [];
|
|
480
|
+
const visited = new Set();
|
|
481
|
+
let currentId = id;
|
|
482
|
+
while (currentId) {
|
|
483
|
+
// Prevent infinite loops from circular references
|
|
484
|
+
if (visited.has(currentId))
|
|
485
|
+
break;
|
|
486
|
+
visited.add(currentId);
|
|
487
|
+
const doc = await this.db.query.documentsTable.findFirst({
|
|
488
|
+
where: and(eq(documentsTable.id, currentId), isNull(documentsTable.deletedAt)),
|
|
489
|
+
columns: {
|
|
490
|
+
id: true,
|
|
491
|
+
title: true,
|
|
492
|
+
slug: true,
|
|
493
|
+
emoji: true,
|
|
494
|
+
cover: true,
|
|
495
|
+
isPublished: true,
|
|
496
|
+
parentId: true,
|
|
497
|
+
order: true,
|
|
498
|
+
authorId: true,
|
|
499
|
+
createdAt: true,
|
|
500
|
+
updatedAt: true,
|
|
501
|
+
deletedAt: true,
|
|
502
|
+
// Exclude: content, searchIndex
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
if (!doc)
|
|
506
|
+
break;
|
|
507
|
+
breadcrumbs.unshift(doc);
|
|
508
|
+
currentId = doc.parentId ?? null;
|
|
509
|
+
}
|
|
510
|
+
return breadcrumbs;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Document } from '../shared/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for DOCX export
|
|
4
|
+
*/
|
|
5
|
+
export interface DocxExportOptions {
|
|
6
|
+
includeTitle?: boolean;
|
|
7
|
+
includeEmoji?: boolean;
|
|
8
|
+
author?: string;
|
|
9
|
+
company?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Options for PDF export
|
|
13
|
+
*/
|
|
14
|
+
export interface PdfExportOptions {
|
|
15
|
+
includeTitle?: boolean;
|
|
16
|
+
includeEmoji?: boolean;
|
|
17
|
+
format?: 'A4' | 'Letter' | 'Legal';
|
|
18
|
+
margin?: {
|
|
19
|
+
top?: string;
|
|
20
|
+
right?: string;
|
|
21
|
+
bottom?: string;
|
|
22
|
+
left?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Export result
|
|
27
|
+
*/
|
|
28
|
+
export interface ExportResult {
|
|
29
|
+
buffer: Buffer;
|
|
30
|
+
filename: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Service for exporting documents to various formats
|
|
35
|
+
*/
|
|
36
|
+
export declare class ExportService {
|
|
37
|
+
/**
|
|
38
|
+
* Export document to DOCX format
|
|
39
|
+
* Requires 'docx' package to be installed
|
|
40
|
+
*/
|
|
41
|
+
exportToDocx(document: Document, options?: DocxExportOptions): Promise<ExportResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Export document to PDF format
|
|
44
|
+
* Requires 'puppeteer' or 'playwright' package to be installed
|
|
45
|
+
*/
|
|
46
|
+
exportToPdf(document: Document, options?: PdfExportOptions): Promise<ExportResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Export document to HTML format
|
|
49
|
+
*/
|
|
50
|
+
exportToHtml(document: Document, options?: {
|
|
51
|
+
includeTitle?: boolean;
|
|
52
|
+
includeEmoji?: boolean;
|
|
53
|
+
}): Promise<ExportResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Export document to Markdown format
|
|
56
|
+
*/
|
|
57
|
+
exportToMarkdown(document: Document, options?: {
|
|
58
|
+
includeTitle?: boolean;
|
|
59
|
+
includeEmoji?: boolean;
|
|
60
|
+
}): Promise<ExportResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Convert a BlockNote block to DOCX Paragraph
|
|
63
|
+
*/
|
|
64
|
+
private blockToDocxParagraph;
|
|
65
|
+
/**
|
|
66
|
+
* Convert BlockNote content items to TextRun array
|
|
67
|
+
*/
|
|
68
|
+
private convertContentToTextRuns;
|
|
69
|
+
/**
|
|
70
|
+
* Extract plain text from content
|
|
71
|
+
*/
|
|
72
|
+
private extractTextFromContent;
|
|
73
|
+
/**
|
|
74
|
+
* Generate HTML from document
|
|
75
|
+
*/
|
|
76
|
+
private generateHtml;
|
|
77
|
+
/**
|
|
78
|
+
* Convert a BlockNote block to HTML
|
|
79
|
+
*/
|
|
80
|
+
private blockToHtml;
|
|
81
|
+
/**
|
|
82
|
+
* Render content items to HTML
|
|
83
|
+
*/
|
|
84
|
+
private renderContentToHtml;
|
|
85
|
+
/**
|
|
86
|
+
* Convert a single content item to HTML
|
|
87
|
+
*/
|
|
88
|
+
private contentItemToHtml;
|
|
89
|
+
/**
|
|
90
|
+
* Convert a table block to HTML
|
|
91
|
+
*/
|
|
92
|
+
private renderTableToHtml;
|
|
93
|
+
/**
|
|
94
|
+
* Convert a block to Markdown
|
|
95
|
+
*/
|
|
96
|
+
private blockToMarkdown;
|
|
97
|
+
/**
|
|
98
|
+
* Escape HTML special characters
|
|
99
|
+
*/
|
|
100
|
+
private escapeHtml;
|
|
101
|
+
/**
|
|
102
|
+
* Sanitize filename for filesystem
|
|
103
|
+
*/
|
|
104
|
+
private sanitizeFilename;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=ExportService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExportService.d.ts","sourceRoot":"","sources":["../../src/server/ExportService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,oBAAoB,CAAA;AAExF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,MAAM,CAAC,EAAE,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAA;IAClC,MAAM,CAAC,EAAE;QACP,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB;;;OAGG;IACG,YAAY,CAChB,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,YAAY,CAAC;IAgExB;;;OAGG;IACG,WAAW,CACf,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,YAAY,CAAC;IA4DxB;;OAEG;IACG,YAAY,CAChB,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAO,GAC/D,OAAO,CAAC,YAAY,CAAC;IAYxB;;OAEG;IACG,gBAAgB,CACpB,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAO,GAC/D,OAAO,CAAC,YAAY,CAAC;IA4BxB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAsF5B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAyChC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;OAEG;IACH,OAAO,CAAC,YAAY;IAqEpB;;OAEG;IACH,OAAO,CAAC,WAAW;IA+CnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAU3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAiBzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAsBzB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2CvB;;OAEG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAOzB"}
|