@mindbase/express-knowledge 1.0.1 → 1.0.8

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.
@@ -0,0 +1,515 @@
1
+ import { knowledge } from "../orm/Knowledge.schema";
2
+ import { eq, and, or, like, sql, desc, inArray } from "drizzle-orm";
3
+ import { buildPath, getLevel, validatePath, isDescendant } from "../utils/path-builder";
4
+ import type { Knowledge, KnowledgeSafe, InsertKnowledge } from "../types";
5
+
6
+ let _db: any;
7
+
8
+ /**
9
+ * 初始化 Service 的数据库实例
10
+ */
11
+ export function initKnowledgeService(db: any) {
12
+ _db = db;
13
+ }
14
+
15
+ function getDB() {
16
+ if (!_db) throw new Error("KnowledgeService not initialized with DB");
17
+ return _db;
18
+ }
19
+
20
+ // ==================== CRUD 操作 ====================
21
+
22
+ /**
23
+ * 创建知识库内容
24
+ */
25
+ export async function createKnowledge(data: InsertKnowledge & { userId: number }) {
26
+ const db = getDB();
27
+ const now = new Date();
28
+
29
+ // 如果有父节点,获取父节点信息
30
+ let parentPath = "/";
31
+ let level = 0;
32
+
33
+ if (data.parentId) {
34
+ const [parent] = await db
35
+ .select()
36
+ .from(knowledge)
37
+ .where(and(eq(knowledge.id, data.parentId), eq(knowledge.isDelete, 0)))
38
+ .limit(1);
39
+
40
+ if (!parent) {
41
+ throw new Error("父节点不存在");
42
+ }
43
+
44
+ parentPath = parent.path;
45
+ level = parent.level + 1;
46
+ }
47
+
48
+ // 先插入基础数据(不含 path 和 level)
49
+ const result = await db
50
+ .insert(knowledge)
51
+ .values({
52
+ name: data.name,
53
+ type: data.type ?? 0,
54
+ safe: data.safe ?? 0,
55
+ userId: data.userId,
56
+ description: data.description,
57
+ content: data.content,
58
+ contentText: data.contentText,
59
+ viewType: data.viewType,
60
+ coverImage: data.coverImage,
61
+ author: data.author,
62
+ isbn: data.isbn,
63
+ created: now,
64
+ updated: now,
65
+ isDelete: 0,
66
+ isTop: 0,
67
+ path: "/", // 临时值
68
+ level: 0, // 临时值
69
+ })
70
+ .execute();
71
+
72
+ // 获取插入的 ID
73
+ const lastId = result.lastInsertRowid as number;
74
+
75
+ // 更新 path 和 level
76
+ const finalPath = buildPath(parentPath, lastId);
77
+ await db
78
+ .update(knowledge)
79
+ .set({ path: finalPath, level })
80
+ .where(eq(knowledge.id, lastId))
81
+ .execute();
82
+
83
+ return getKnowledgeById(lastId);
84
+ }
85
+
86
+ /**
87
+ * 批量创建知识库内容
88
+ */
89
+ export async function createKnowledgeBatch(items: Array<{ name: string; type: number }>, userId: number, parentId?: number) {
90
+ const results = [];
91
+
92
+ for (const item of items) {
93
+ try {
94
+ const result = await createKnowledge({ ...item, userId, parentId });
95
+ results.push({ success: true, data: result });
96
+ } catch (error) {
97
+ results.push({ success: false, error: error instanceof Error ? error.message : String(error) });
98
+ }
99
+ }
100
+
101
+ return results;
102
+ }
103
+
104
+ /**
105
+ * 分页获取知识库列表
106
+ */
107
+ export async function listKnowledge(options: {
108
+ pageIndex: number;
109
+ pageSize: number;
110
+ level?: number;
111
+ type?: number;
112
+ safe?: number[];
113
+ userId?: number;
114
+ isAdmin?: boolean;
115
+ }) {
116
+ const db = getDB();
117
+ const { pageIndex, pageSize, level, type, safe = [0], userId, isAdmin } = options;
118
+
119
+ let conditions = [eq(knowledge.isDelete, 0)];
120
+
121
+ // 层级筛选
122
+ if (level !== undefined) {
123
+ conditions.push(eq(knowledge.level, level));
124
+ }
125
+
126
+ // 类型筛选
127
+ if (type !== undefined) {
128
+ conditions.push(eq(knowledge.type, type));
129
+ }
130
+
131
+ // 权限筛选
132
+ const safeConditions = [];
133
+ for (const s of safe) {
134
+ switch (s) {
135
+ case 0: // 公开
136
+ safeConditions.push(eq(knowledge.safe, s));
137
+ break;
138
+ case 1: // 登录共享
139
+ if (userId) {
140
+ safeConditions.push(eq(knowledge.safe, s));
141
+ }
142
+ break;
143
+ case 2: // 个人私密
144
+ if (userId) {
145
+ safeConditions.push(
146
+ and(eq(knowledge.safe, s), isAdmin ? sql`1=1` : eq(knowledge.userId, userId))
147
+ );
148
+ }
149
+ break;
150
+ }
151
+ }
152
+
153
+ if (safeConditions.length > 0) {
154
+ conditions.push(or(...safeConditions));
155
+ }
156
+
157
+ const offset = (pageIndex - 1) * pageSize;
158
+
159
+ const list = await db
160
+ .select()
161
+ .from(knowledge)
162
+ .where(and(...conditions))
163
+ .limit(pageSize)
164
+ .offset(offset)
165
+ .orderBy(desc(knowledge.isTop), desc(knowledge.created))
166
+ .execute();
167
+
168
+ const [totalResult] = await db
169
+ .select({ count: sql<number>`count(*)` })
170
+ .from(knowledge)
171
+ .where(and(...conditions))
172
+ .execute();
173
+
174
+ return {
175
+ list,
176
+ total: totalResult.count,
177
+ pageIndex,
178
+ pageSize,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * 获取知识库详情
184
+ */
185
+ export async function getKnowledgeById(id: number): Promise<Knowledge | null> {
186
+ const db = getDB();
187
+ const [item] = await db
188
+ .select()
189
+ .from(knowledge)
190
+ .where(and(eq(knowledge.id, id), eq(knowledge.isDelete, 0)))
191
+ .limit(1);
192
+
193
+ return item || null;
194
+ }
195
+
196
+ /**
197
+ * 批量获取知识库详情
198
+ */
199
+ export async function getKnowledgeByIds(ids: number[]): Promise<Knowledge[]> {
200
+ const db = getDB();
201
+ if (ids.length === 0) return [];
202
+
203
+ return await db
204
+ .select()
205
+ .from(knowledge)
206
+ .where(and(inArray(knowledge.id, ids), eq(knowledge.isDelete, 0)))
207
+ .execute();
208
+ }
209
+
210
+ /**
211
+ * 获取知识库目录树
212
+ */
213
+ export async function getKnowledgeTree(options: {
214
+ parentId?: number | null;
215
+ safe?: number[];
216
+ userId?: number;
217
+ isAdmin?: boolean;
218
+ }) {
219
+ const db = getDB();
220
+ const { parentId = null, safe = [0], userId, isAdmin } = options;
221
+
222
+ let conditions = [eq(knowledge.isDelete, 0)];
223
+
224
+ // 权限筛选
225
+ const safeConditions = [];
226
+ for (const s of safe) {
227
+ switch (s) {
228
+ case 0: // 公开
229
+ safeConditions.push(eq(knowledge.safe, s));
230
+ break;
231
+ case 1: // 登录共享
232
+ if (userId) {
233
+ safeConditions.push(eq(knowledge.safe, s));
234
+ }
235
+ break;
236
+ case 2: // 个人私密
237
+ if (userId) {
238
+ safeConditions.push(
239
+ and(eq(knowledge.safe, s), isAdmin ? sql`1=1` : eq(knowledge.userId, userId))
240
+ );
241
+ }
242
+ break;
243
+ }
244
+ }
245
+
246
+ if (safeConditions.length > 0) {
247
+ conditions.push(or(...safeConditions));
248
+ }
249
+
250
+ // 如果指定了 parentId,只获取该节点下的子树
251
+ if (parentId !== null) {
252
+ const [parent] = await db
253
+ .select()
254
+ .from(knowledge)
255
+ .where(and(eq(knowledge.id, parentId), eq(knowledge.isDelete, 0)))
256
+ .limit(1);
257
+
258
+ if (!parent) return [];
259
+
260
+ conditions.push(like(knowledge.path, `${parent.path}%`));
261
+ }
262
+
263
+ const allNodes = await db
264
+ .select()
265
+ .from(knowledge)
266
+ .where(and(...conditions))
267
+ .orderBy(desc(knowledge.isTop), knowledge.name)
268
+ .execute();
269
+
270
+ // 使用 tree-builder 构建树形结构
271
+ const { buildTree } = await import("../utils/tree-builder");
272
+ return buildTree(allNodes, parentId);
273
+ }
274
+
275
+ /**
276
+ * 获取子节点列表
277
+ */
278
+ export async function getChildren(parentId: number) {
279
+ const db = getDB();
280
+
281
+ const [parent] = await db
282
+ .select()
283
+ .from(knowledge)
284
+ .where(and(eq(knowledge.id, parentId), eq(knowledge.isDelete, 0)))
285
+ .limit(1);
286
+
287
+ if (!parent) {
288
+ throw new Error("父节点不存在");
289
+ }
290
+
291
+ return await db
292
+ .select()
293
+ .from(knowledge)
294
+ .where(
295
+ and(
296
+ eq(knowledge.isDelete, 0),
297
+ like(knowledge.path, `${parent.path}%`),
298
+ eq(knowledge.level, parent.level + 1)
299
+ )
300
+ )
301
+ .orderBy(desc(knowledge.isTop), knowledge.name)
302
+ .execute();
303
+ }
304
+
305
+ /**
306
+ * 更新知识库内容
307
+ */
308
+ export async function updateKnowledge(id: number, data: Partial<InsertKnowledge>, userId: number) {
309
+ const db = getDB();
310
+ const now = new Date();
311
+
312
+ const result = await db
313
+ .update(knowledge)
314
+ .set({
315
+ ...(data.name !== undefined && { name: data.name }),
316
+ ...(data.description !== undefined && { description: data.description }),
317
+ ...(data.content !== undefined && { content: data.content }),
318
+ ...(data.contentText !== undefined && { contentText: data.contentText }),
319
+ ...(data.viewType !== undefined && { viewType: data.viewType }),
320
+ ...(data.coverImage !== undefined && { coverImage: data.coverImage }),
321
+ ...(data.author !== undefined && { author: data.author }),
322
+ ...(data.isbn !== undefined && { isbn: data.isbn }),
323
+ updated: now,
324
+ })
325
+ .where(and(eq(knowledge.id, id), eq(knowledge.userId, userId)))
326
+ .execute();
327
+
328
+ return result;
329
+ }
330
+
331
+ /**
332
+ * 移动节点(更新整个子树的 path 和 level)
333
+ */
334
+ export async function moveKnowledge(nodeId: number, newParentId: number, userId: number) {
335
+ const db = getDB();
336
+
337
+ // 1. 获取节点和新父节点信息
338
+ const [node] = await db
339
+ .select()
340
+ .from(knowledge)
341
+ .where(and(eq(knowledge.id, nodeId), eq(knowledge.isDelete, 0)))
342
+ .limit(1);
343
+
344
+ const [newParent] = await db
345
+ .select()
346
+ .from(knowledge)
347
+ .where(and(eq(knowledge.id, newParentId), eq(knowledge.isDelete, 0)))
348
+ .limit(1);
349
+
350
+ if (!node || !newParent) {
351
+ throw new Error("节点不存在");
352
+ }
353
+
354
+ // 权限检查
355
+ if (node.userId !== userId) {
356
+ throw new Error("无权操作此节点");
357
+ }
358
+
359
+ // 2. 防止循环:不能移动到自己的后代节点
360
+ if (isDescendant(node.path, newParent.path)) {
361
+ throw new Error("不能移动到自己的后代节点");
362
+ }
363
+
364
+ const oldPath = node.path;
365
+ const newPath = buildPath(newParent.path, nodeId);
366
+ const levelDiff = newParent.level + 1 - node.level;
367
+
368
+ // 3. 批量更新整个子树
369
+ await db
370
+ .update(knowledge)
371
+ .set({
372
+ path: sql`REPLACE(${knowledge.path}, ${oldPath}, ${newPath})`,
373
+ level: sql`${knowledge.level} + ${levelDiff}`,
374
+ })
375
+ .where(or(like(knowledge.path, `${oldPath}%`), eq(knowledge.path, oldPath)))
376
+ .execute();
377
+
378
+ return getKnowledgeById(nodeId);
379
+ }
380
+
381
+ /**
382
+ * 更新置顶状态
383
+ */
384
+ export async function updateIsTop(id: number, isTop: number, userId: number) {
385
+ const db = getDB();
386
+ const now = new Date();
387
+
388
+ await db
389
+ .update(knowledge)
390
+ .set({ isTop, updated: now })
391
+ .where(and(eq(knowledge.id, id), eq(knowledge.userId, userId)))
392
+ .execute();
393
+
394
+ return getKnowledgeById(id);
395
+ }
396
+
397
+ /**
398
+ * 更新安全等级
399
+ */
400
+ export async function updateSafe(id: number, safe: KnowledgeSafe, userId: number) {
401
+ const db = getDB();
402
+ const now = new Date();
403
+
404
+ await db
405
+ .update(knowledge)
406
+ .set({ safe, updated: now })
407
+ .where(and(eq(knowledge.id, id), eq(knowledge.userId, userId)))
408
+ .execute();
409
+
410
+ return getKnowledgeById(id);
411
+ }
412
+
413
+ /**
414
+ * 逻辑删除
415
+ */
416
+ export async function deleteKnowledge(id: number, userId: number) {
417
+ const db = getDB();
418
+ const now = new Date();
419
+
420
+ await db
421
+ .update(knowledge)
422
+ .set({ isDelete: 1, updated: now })
423
+ .where(and(eq(knowledge.id, id), eq(knowledge.userId, userId)))
424
+ .execute();
425
+
426
+ return { success: true };
427
+ }
428
+
429
+ /**
430
+ * 真实删除(管理员)
431
+ */
432
+ export async function deleteKnowledgeReal(id: number) {
433
+ const db = getDB();
434
+
435
+ await db
436
+ .delete(knowledge)
437
+ .where(eq(knowledge.id, id))
438
+ .execute();
439
+
440
+ return { success: true };
441
+ }
442
+
443
+ /**
444
+ * 搜索知识库内容
445
+ */
446
+ export async function searchKnowledge(options: {
447
+ keyword: string;
448
+ pageIndex?: number;
449
+ pageSize?: number;
450
+ safe?: number[];
451
+ userId?: number;
452
+ isAdmin?: boolean;
453
+ }) {
454
+ const db = getDB();
455
+ const { keyword, pageIndex = 1, pageSize = 10, safe = [0], userId, isAdmin } = options;
456
+
457
+ let conditions = [
458
+ eq(knowledge.isDelete, 0),
459
+ or(
460
+ like(knowledge.name, `%${keyword}%`),
461
+ like(knowledge.description, `%${keyword}%`),
462
+ like(knowledge.contentText, `%${keyword}%`)
463
+ ),
464
+ ];
465
+
466
+ // 权限筛选
467
+ const safeConditions = [];
468
+ for (const s of safe) {
469
+ switch (s) {
470
+ case 0: // 公开
471
+ safeConditions.push(eq(knowledge.safe, s));
472
+ break;
473
+ case 1: // 登录共享
474
+ if (userId) {
475
+ safeConditions.push(eq(knowledge.safe, s));
476
+ }
477
+ break;
478
+ case 2: // 个人私密
479
+ if (userId) {
480
+ safeConditions.push(
481
+ and(eq(knowledge.safe, s), isAdmin ? sql`1=1` : eq(knowledge.userId, userId))
482
+ );
483
+ }
484
+ break;
485
+ }
486
+ }
487
+
488
+ if (safeConditions.length > 0) {
489
+ conditions.push(or(...safeConditions));
490
+ }
491
+
492
+ const offset = (pageIndex - 1) * pageSize;
493
+
494
+ const list = await db
495
+ .select()
496
+ .from(knowledge)
497
+ .where(and(...conditions))
498
+ .limit(pageSize)
499
+ .offset(offset)
500
+ .orderBy(desc(knowledge.created))
501
+ .execute();
502
+
503
+ const [totalResult] = await db
504
+ .select({ count: sql<number>`count(*)` })
505
+ .from(knowledge)
506
+ .where(and(...conditions))
507
+ .execute();
508
+
509
+ return {
510
+ list,
511
+ total: totalResult.count,
512
+ pageIndex,
513
+ pageSize,
514
+ };
515
+ }