Repository navigation
Expand file tree
/
Copy pathblockRepository.ts
More file actions
230 lines (204 loc) · 6.17 KB
/
Copy pathblockRepository.ts
File metadata and controls
230 lines (204 loc) · 6.17 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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
/**
* 内容块仓储。
*
* 这里统一管理文本块和文件块的基础数据结构,
* 但不负责文件系统读写;文件删除、上传目录维护仍由服务层和路由层控制。
*/
import type { BlockRecord } from "../types";
import { db, now, withTransaction } from "./shared";
/**
* 为新插入的块预留较大的 sort 间隔,便于前端频繁拖拽后仍能重排。
*/
function getNextSortOrder(workspaceId: number): number {
const row = db
.prepare("SELECT COALESCE(MAX(sort_order), 0) AS maxSort FROM blocks WHERE workspace_id = ?")
.get(workspaceId) as { maxSort: number };
return Number(row.maxSort ?? 0) + 100;
}
/**
* 获取工作区下的全部内容块。
*/
export function listBlocksByWorkspaceId(workspaceId: number): BlockRecord[] {
const rows = db
.prepare("SELECT * FROM blocks WHERE workspace_id = ? ORDER BY sort_order, id")
.all(workspaceId) as unknown[];
return rows as BlockRecord[];
}
/**
* 创建文本块。
*/
export function createTextBlock(
workspaceId: number,
input: { title: string; content: string; language?: string }
): BlockRecord {
const timestamp = now();
const sortOrder = getNextSortOrder(workspaceId);
const language = input.language || "markdown";
const result = db
.prepare(
"INSERT INTO blocks (workspace_id, type, sort_order, title, content, language, created_at, updated_at) VALUES (?, 'text', ?, ?, ?, ?, ?, ?)"
)
.run(workspaceId, sortOrder, input.title, input.content, language, timestamp, timestamp) as {
lastInsertRowid: number | bigint;
};
return getBlockById(Number(result.lastInsertRowid));
}
/**
* 创建文件块。
*
* 注意:这里需要把默认标题一并写入数据库,避免前端刚创建完成时出现空标题。
*/
export function createFileBlockGroup(
workspaceId: number,
input: {
title: string;
files: Array<{
storageName: string;
originalName: string;
mimeType: string | null;
sizeBytes: number | null;
}>;
}
): BlockRecord {
const timestamp = now();
const sortOrder = getNextSortOrder(workspaceId);
return withTransaction(() => {
const blockResult = db
.prepare(
"INSERT INTO blocks (workspace_id, type, sort_order, title, created_at, updated_at) VALUES (?, 'file', ?, ?, ?, ?)"
)
.run(workspaceId, sortOrder, input.title || null, timestamp, timestamp) as {
lastInsertRowid: number | bigint;
};
const blockId = Number(blockResult.lastInsertRowid);
const insertAttachment = db.prepare(
"INSERT INTO attachments (block_id, storage_name, original_name, mime_type, size_bytes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
);
for (const file of input.files) {
insertAttachment.run(
blockId,
file.storageName,
file.originalName,
file.mimeType,
file.sizeBytes,
timestamp,
timestamp
);
}
return getBlockById(blockId);
});
}
/**
* 根据 ID 获取内容块。
*/
export function getBlockById(id: number): BlockRecord {
const block = db.prepare("SELECT * FROM blocks WHERE id = ?").get(id) as
| BlockRecord
| undefined;
if (!block) {
throw new Error("Block not found");
}
return block;
}
/**
* 根据 ID 获取内容块,不校验工作区归属。
*/
export function findBlockById(id: number): BlockRecord | null {
const block = db.prepare("SELECT * FROM blocks WHERE id = ?").get(id) as
| BlockRecord
| undefined;
return block ?? null;
}
/**
* 更新文本块。
*/
export function updateTextBlock(
id: number,
input: { title: string; content: string; language?: string }
): BlockRecord | null {
const exists = db.prepare("SELECT 1 FROM blocks WHERE id = ?").get(id) as
| { 1: number }
| undefined;
if (!exists) {
return null;
}
db.prepare("UPDATE blocks SET title = ?, content = ?, language = ?, updated_at = ? WHERE id = ?").run(
input.title,
input.content,
input.language || "markdown",
now(),
id
);
return getBlockById(id);
}
/**
* 更新文件块标题。
*/
export function updateFileBlockTitle(
workspaceId: number,
id: number,
title: string | null
): BlockRecord | null {
const exists = db.prepare("SELECT 1 FROM blocks WHERE id = ? AND workspace_id = ?").get(id, workspaceId) as
| { 1: number }
| undefined;
if (!exists) {
return null;
}
db.prepare("UPDATE blocks SET title = ?, updated_at = ? WHERE id = ?").run(title, now(), id);
return getBlockById(id);
}
/**
* 删除内容块。
*/
export function deleteBlock(workspaceId: number, id: number): BlockRecord | null {
const block = db
.prepare("SELECT * FROM blocks WHERE id = ? AND workspace_id = ?")
.get(id, workspaceId) as BlockRecord | undefined;
if (!block) {
return null;
}
db.prepare("DELETE FROM blocks WHERE id = ?").run(id);
return block;
}
/**
* 重排内容块顺序。
*
* 这里要求前端提交完整且无重复的 ID 列表,
* 这样能避免“只更新局部顺序”带来的隐式状态错乱。
*/
export function reorderBlocks(workspaceId: number, orderedIds: number[]): BlockRecord[] | null {
const existingBlocks = listBlocksByWorkspaceId(workspaceId);
const existingIds = existingBlocks.map((block) => block.id);
const existingIdSet = new Set(existingIds);
if (orderedIds.length !== existingIds.length) {
return null;
}
const seenIds = new Set<number>();
for (const id of orderedIds) {
if (!existingIdSet.has(id) || seenIds.has(id)) {
return null;
}
seenIds.add(id);
}
const isSameOrder = existingIds.every((id, index) => id === orderedIds[index]);
if (isSameOrder) {
return existingBlocks;
}
const timestamp = now();
const updateSort = db.prepare(
"UPDATE blocks SET sort_order = ?, updated_at = ? WHERE id = ? AND workspace_id = ?"
);
withTransaction(() => {
orderedIds.forEach((id, index) => {
updateSort.run((index + 1) * 100, timestamp, id, workspaceId);
});
});
return listBlocksByWorkspaceId(workspaceId);
}
/**
* 更新内容块更新时间。
*/
export function updateBlockTimestamp(blockId: number): void {
db.prepare("UPDATE blocks SET updated_at = ? WHERE id = ?").run(now(), blockId);
}