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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/dist/client/components/Breadcrumbs.d.ts +21 -0
  4. package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
  5. package/dist/client/components/Breadcrumbs.js +123 -0
  6. package/dist/client/components/DocsLayout.d.ts +20 -0
  7. package/dist/client/components/DocsLayout.d.ts.map +1 -0
  8. package/dist/client/components/DocsLayout.js +387 -0
  9. package/dist/client/components/DocumentContent.d.ts +5 -0
  10. package/dist/client/components/DocumentContent.d.ts.map +1 -0
  11. package/dist/client/components/DocumentContent.js +15 -0
  12. package/dist/client/components/DocumentEdit.d.ts +6 -0
  13. package/dist/client/components/DocumentEdit.d.ts.map +1 -0
  14. package/dist/client/components/DocumentEdit.js +153 -0
  15. package/dist/client/components/DocumentList.d.ts +5 -0
  16. package/dist/client/components/DocumentList.d.ts.map +1 -0
  17. package/dist/client/components/DocumentList.js +39 -0
  18. package/dist/client/components/DocumentProvider.d.ts +42 -0
  19. package/dist/client/components/DocumentProvider.d.ts.map +1 -0
  20. package/dist/client/components/DocumentProvider.js +47 -0
  21. package/dist/client/components/DocumentView.d.ts +6 -0
  22. package/dist/client/components/DocumentView.d.ts.map +1 -0
  23. package/dist/client/components/DocumentView.js +58 -0
  24. package/dist/client/components/DragOverlayItem.d.ts +5 -0
  25. package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
  26. package/dist/client/components/DragOverlayItem.js +9 -0
  27. package/dist/client/components/EmojiPicker.d.ts +8 -0
  28. package/dist/client/components/EmojiPicker.d.ts.map +1 -0
  29. package/dist/client/components/EmojiPicker.js +48 -0
  30. package/dist/client/components/ExportButton.d.ts +22 -0
  31. package/dist/client/components/ExportButton.d.ts.map +1 -0
  32. package/dist/client/components/ExportButton.js +97 -0
  33. package/dist/client/components/Layout.d.ts +7 -0
  34. package/dist/client/components/Layout.d.ts.map +1 -0
  35. package/dist/client/components/Layout.js +172 -0
  36. package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
  37. package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
  38. package/dist/client/components/ReactEmbedDocs.js +8 -0
  39. package/dist/client/components/SearchInput.d.ts +2 -0
  40. package/dist/client/components/SearchInput.d.ts.map +1 -0
  41. package/dist/client/components/SearchInput.js +7 -0
  42. package/dist/client/components/Sidebar.d.ts +10 -0
  43. package/dist/client/components/Sidebar.d.ts.map +1 -0
  44. package/dist/client/components/Sidebar.js +176 -0
  45. package/dist/client/components/SortableTreeItem.d.ts +13 -0
  46. package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
  47. package/dist/client/components/SortableTreeItem.js +24 -0
  48. package/dist/client/components/VersionHistory.d.ts +14 -0
  49. package/dist/client/components/VersionHistory.d.ts.map +1 -0
  50. package/dist/client/components/VersionHistory.js +102 -0
  51. package/dist/client/hooks/useCollaboration.d.ts +99 -0
  52. package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
  53. package/dist/client/hooks/useCollaboration.js +180 -0
  54. package/dist/client/hooks/useDocsQuery.d.ts +84 -0
  55. package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
  56. package/dist/client/hooks/useDocsQuery.js +241 -0
  57. package/dist/client/hooks/useExport.d.ts +31 -0
  58. package/dist/client/hooks/useExport.d.ts.map +1 -0
  59. package/dist/client/hooks/useExport.js +66 -0
  60. package/dist/client/hooks/useFileUpload.d.ts +44 -0
  61. package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
  62. package/dist/client/hooks/useFileUpload.js +193 -0
  63. package/dist/client/hooks/useSystemTheme.d.ts +2 -0
  64. package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
  65. package/dist/client/hooks/useSystemTheme.js +19 -0
  66. package/dist/client/hooks/useVersions.d.ts +105 -0
  67. package/dist/client/hooks/useVersions.d.ts.map +1 -0
  68. package/dist/client/hooks/useVersions.js +129 -0
  69. package/dist/client/index.d.ts +23 -0
  70. package/dist/client/index.d.ts.map +1 -0
  71. package/dist/client/index.js +18 -0
  72. package/dist/client/lib/blocknoteTheme.d.ts +13 -0
  73. package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
  74. package/dist/client/lib/blocknoteTheme.js +76 -0
  75. package/dist/client/lib/path.d.ts +8 -0
  76. package/dist/client/lib/path.d.ts.map +1 -0
  77. package/dist/client/lib/path.js +30 -0
  78. package/dist/client/providers/DocumentProvider.d.ts +1 -0
  79. package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
  80. package/dist/client/providers/DocumentProvider.js +1 -0
  81. package/dist/server/CollaborationService.d.ts +134 -0
  82. package/dist/server/CollaborationService.d.ts.map +1 -0
  83. package/dist/server/CollaborationService.js +307 -0
  84. package/dist/server/DocsService.d.ts +115 -0
  85. package/dist/server/DocsService.d.ts.map +1 -0
  86. package/dist/server/DocsService.js +512 -0
  87. package/dist/server/ExportService.d.ts +106 -0
  88. package/dist/server/ExportService.d.ts.map +1 -0
  89. package/dist/server/ExportService.js +501 -0
  90. package/dist/server/FilesService.d.ts +44 -0
  91. package/dist/server/FilesService.d.ts.map +1 -0
  92. package/dist/server/FilesService.js +78 -0
  93. package/dist/server/VersioningService.d.ts +112 -0
  94. package/dist/server/VersioningService.d.ts.map +1 -0
  95. package/dist/server/VersioningService.js +264 -0
  96. package/dist/server/db.d.ts +7 -0
  97. package/dist/server/db.d.ts.map +1 -0
  98. package/dist/server/db.js +22 -0
  99. package/dist/server/index.d.ts +55 -0
  100. package/dist/server/index.d.ts.map +1 -0
  101. package/dist/server/index.js +36 -0
  102. package/dist/server/routes.d.ts +9 -0
  103. package/dist/server/routes.d.ts.map +1 -0
  104. package/dist/server/routes.js +483 -0
  105. package/dist/server/schema.d.ts +587 -0
  106. package/dist/server/schema.d.ts.map +1 -0
  107. package/dist/server/schema.js +126 -0
  108. package/dist/shared/types.d.ts +314 -0
  109. package/dist/shared/types.d.ts.map +1 -0
  110. package/dist/shared/types.js +48 -0
  111. package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
  112. package/drizzle/migrations/meta/0000_snapshot.json +574 -0
  113. package/drizzle/migrations/meta/_journal.json +13 -0
  114. package/package.json +109 -0
  115. 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"}