@plandesk/api 0.7.0 → 0.9.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/dist/events.d.ts +20 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/routes/agent-runs.d.ts +1 -1
- package/dist/routes/agent-runs.js +20 -0
- package/dist/routes/documents.js +2 -0
- package/dist/routes/folders.d.ts +4 -0
- package/dist/routes/folders.js +69 -0
- package/dist/routes/goals.d.ts +4 -0
- package/dist/routes/goals.js +138 -0
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.js +23 -3
- package/dist/routes/submissions.d.ts +5 -0
- package/dist/routes/submissions.js +58 -0
- package/dist/routes/tags.d.ts +4 -0
- package/dist/routes/tags.js +62 -0
- package/dist/routes/tasks.d.ts +1 -0
- package/dist/routes/tasks.js +9 -1
- package/dist/serialize.d.ts +51 -2
- package/dist/serialize.js +101 -1
- package/dist/server.js +9 -1
- package/dist/services/canvas.d.ts +4 -0
- package/dist/services/canvas.js +3 -2
- package/dist/services/documents.d.ts +5 -1
- package/dist/services/documents.js +31 -2
- package/dist/services/folders.d.ts +27 -0
- package/dist/services/folders.js +107 -0
- package/dist/services/goals.d.ts +239 -0
- package/dist/services/goals.js +323 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +9 -0
- package/dist/services/projects.js +8 -2
- package/dist/services/sync.d.ts +5 -3
- package/dist/services/sync.js +45 -2
- package/dist/services/tags.d.ts +27 -0
- package/dist/services/tags.js +79 -0
- package/dist/services/tasks.d.ts +20 -2
- package/dist/services/tasks.js +98 -26
- package/package.json +3 -3
- package/web/assets/index-DcD7-VGi.css +1 -0
- package/web/assets/index-Dcaq3wR-.js +318 -0
- package/web/index.html +2 -2
- package/web/assets/index-CUxSE_WZ.css +0 -1
- package/web/assets/index-cyyf9-Uw.js +0 -234
package/dist/serialize.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentRun, AgentRunEvent, Document, DocumentComment, Edge, Note, Project, Task, TaskStatus } from '@plandesk/db';
|
|
1
|
+
import type { AgentRun, AgentRunEvent, Document, DocumentComment, Edge, Folder, Goal, Note, Project, Tag, Task, TaskStatus } from '@plandesk/db';
|
|
2
2
|
export type PaginationParams = {
|
|
3
3
|
limit?: number;
|
|
4
4
|
offset?: number;
|
|
@@ -21,9 +21,39 @@ export declare function serializeProjectDetail(project: Project, summary: TaskSt
|
|
|
21
21
|
created_at: string;
|
|
22
22
|
updated_at: string;
|
|
23
23
|
};
|
|
24
|
-
export
|
|
24
|
+
export type SerializedTag = {
|
|
25
25
|
id: string;
|
|
26
26
|
project_id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
color: string | null;
|
|
29
|
+
created_at: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function serializeTag(tag: Tag): SerializedTag;
|
|
32
|
+
export declare function serializeGoal(goal: Goal): {
|
|
33
|
+
id: string;
|
|
34
|
+
project_id: string;
|
|
35
|
+
objective: string;
|
|
36
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
37
|
+
verification_surface: string | null;
|
|
38
|
+
constraints: string | null;
|
|
39
|
+
boundaries: string | null;
|
|
40
|
+
iteration_policy: string | null;
|
|
41
|
+
stop_condition: string | null;
|
|
42
|
+
budget: string | null;
|
|
43
|
+
last_verification: {
|
|
44
|
+
at: string;
|
|
45
|
+
green: boolean;
|
|
46
|
+
kind: string | null;
|
|
47
|
+
detail?: string;
|
|
48
|
+
} | null;
|
|
49
|
+
created_at: string;
|
|
50
|
+
updated_at: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function serializeTask(task: Task, tags?: Tag[]): {
|
|
53
|
+
tags?: SerializedTag[] | undefined;
|
|
54
|
+
id: string;
|
|
55
|
+
project_id: string;
|
|
56
|
+
goal_id: string;
|
|
27
57
|
label: string;
|
|
28
58
|
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
29
59
|
description: string | null;
|
|
@@ -41,6 +71,7 @@ export type SerializedDocument = {
|
|
|
41
71
|
body: string | null;
|
|
42
72
|
status_line: string | null;
|
|
43
73
|
parent_id: string | null;
|
|
74
|
+
folder_id: string | null;
|
|
44
75
|
linked_task_id: string | null;
|
|
45
76
|
created_at: string;
|
|
46
77
|
updated_at: string;
|
|
@@ -68,6 +99,24 @@ export type SerializedComment = {
|
|
|
68
99
|
};
|
|
69
100
|
export declare function serializeComment(comment: DocumentComment): SerializedComment;
|
|
70
101
|
export declare function buildDocumentTree(documents: Document[]): SerializedDocumentTree[];
|
|
102
|
+
export type SerializedFolder = {
|
|
103
|
+
id: string;
|
|
104
|
+
project_id: string;
|
|
105
|
+
name: string;
|
|
106
|
+
parent_folder_id: string | null;
|
|
107
|
+
created_at: string;
|
|
108
|
+
updated_at: string;
|
|
109
|
+
};
|
|
110
|
+
export declare function serializeFolder(folder: Folder): SerializedFolder;
|
|
111
|
+
export type SerializedFolderTree = SerializedFolder & {
|
|
112
|
+
folders: SerializedFolderTree[];
|
|
113
|
+
documents: SerializedDocumentTree[];
|
|
114
|
+
};
|
|
115
|
+
export type SerializedDocumentFolderTree = {
|
|
116
|
+
folders: SerializedFolderTree[];
|
|
117
|
+
documents: SerializedDocumentTree[];
|
|
118
|
+
};
|
|
119
|
+
export declare function buildFolderTree(folders: Folder[], documents: Document[]): SerializedDocumentFolderTree;
|
|
71
120
|
export declare function serializeEdge(edge: Edge): {
|
|
72
121
|
id: string;
|
|
73
122
|
project_id: string;
|
package/dist/serialize.js
CHANGED
|
@@ -36,10 +36,54 @@ export function serializeProjectDetail(project, summary) {
|
|
|
36
36
|
summary,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
export function
|
|
39
|
+
export function serializeTag(tag) {
|
|
40
|
+
return {
|
|
41
|
+
id: tag.id,
|
|
42
|
+
project_id: tag.projectId,
|
|
43
|
+
name: tag.name,
|
|
44
|
+
color: tag.color,
|
|
45
|
+
created_at: tag.createdAt.toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parseLastVerification(raw) {
|
|
49
|
+
if (raw === null) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (typeof parsed.at === 'string' &&
|
|
55
|
+
typeof parsed.green === 'boolean' &&
|
|
56
|
+
(parsed.kind === null || typeof parsed.kind === 'string')) {
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function serializeGoal(goal) {
|
|
66
|
+
return {
|
|
67
|
+
id: goal.id,
|
|
68
|
+
project_id: goal.projectId,
|
|
69
|
+
objective: goal.objective,
|
|
70
|
+
status: goal.status,
|
|
71
|
+
verification_surface: goal.verificationSurface,
|
|
72
|
+
constraints: goal.constraints,
|
|
73
|
+
boundaries: goal.boundaries,
|
|
74
|
+
iteration_policy: goal.iterationPolicy,
|
|
75
|
+
stop_condition: goal.stopCondition,
|
|
76
|
+
budget: goal.budget,
|
|
77
|
+
last_verification: parseLastVerification(goal.lastVerification),
|
|
78
|
+
created_at: goal.createdAt.toISOString(),
|
|
79
|
+
updated_at: goal.updatedAt.toISOString(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function serializeTask(task, tags) {
|
|
40
83
|
return {
|
|
41
84
|
id: task.id,
|
|
42
85
|
project_id: task.projectId,
|
|
86
|
+
goal_id: task.goalId,
|
|
43
87
|
label: task.label,
|
|
44
88
|
status: task.status,
|
|
45
89
|
description: task.description,
|
|
@@ -49,6 +93,7 @@ export function serializeTask(task) {
|
|
|
49
93
|
due_date: task.dueDate?.toISOString() ?? null,
|
|
50
94
|
created_at: task.createdAt.toISOString(),
|
|
51
95
|
updated_at: task.updatedAt.toISOString(),
|
|
96
|
+
...(tags !== undefined ? { tags: tags.map(serializeTag) } : {}),
|
|
52
97
|
};
|
|
53
98
|
}
|
|
54
99
|
export function serializeDocument(document) {
|
|
@@ -59,6 +104,7 @@ export function serializeDocument(document) {
|
|
|
59
104
|
body: document.body,
|
|
60
105
|
status_line: document.statusLine,
|
|
61
106
|
parent_id: document.parentId,
|
|
107
|
+
folder_id: document.folderId,
|
|
62
108
|
linked_task_id: document.linkedTaskId,
|
|
63
109
|
created_at: document.createdAt.toISOString(),
|
|
64
110
|
updated_at: document.updatedAt.toISOString(),
|
|
@@ -109,6 +155,60 @@ export function buildDocumentTree(documents) {
|
|
|
109
155
|
}
|
|
110
156
|
return roots;
|
|
111
157
|
}
|
|
158
|
+
export function serializeFolder(folder) {
|
|
159
|
+
return {
|
|
160
|
+
id: folder.id,
|
|
161
|
+
project_id: folder.projectId,
|
|
162
|
+
name: folder.name,
|
|
163
|
+
parent_folder_id: folder.parentFolderId,
|
|
164
|
+
created_at: folder.createdAt.toISOString(),
|
|
165
|
+
updated_at: folder.updatedAt.toISOString(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export function buildFolderTree(folders, documents) {
|
|
169
|
+
const folderNodes = new Map();
|
|
170
|
+
for (const folder of folders) {
|
|
171
|
+
folderNodes.set(folder.id, { ...serializeFolder(folder), folders: [], documents: [] });
|
|
172
|
+
}
|
|
173
|
+
const rootFolders = [];
|
|
174
|
+
for (const folder of folders) {
|
|
175
|
+
const node = folderNodes.get(folder.id);
|
|
176
|
+
if (!node) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const parent = folder.parentFolderId === null ? undefined : folderNodes.get(folder.parentFolderId);
|
|
180
|
+
if (parent) {
|
|
181
|
+
parent.folders.push(node);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
rootFolders.push(node);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const documentsByFolder = new Map();
|
|
188
|
+
for (const document of documents) {
|
|
189
|
+
const key = document.folderId !== null && folderNodes.has(document.folderId) ? document.folderId : null;
|
|
190
|
+
const group = documentsByFolder.get(key);
|
|
191
|
+
if (group) {
|
|
192
|
+
group.push(document);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
documentsByFolder.set(key, [document]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const [folderId, group] of documentsByFolder) {
|
|
199
|
+
if (folderId === null) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const node = folderNodes.get(folderId);
|
|
203
|
+
if (node) {
|
|
204
|
+
node.documents = buildDocumentTree(group);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
folders: rootFolders,
|
|
209
|
+
documents: buildDocumentTree(documentsByFolder.get(null) ?? []),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
112
212
|
export function serializeEdge(edge) {
|
|
113
213
|
return {
|
|
114
214
|
id: edge.id,
|
package/dist/server.js
CHANGED
|
@@ -3,31 +3,39 @@ import { createAuthMiddleware } from './auth.js';
|
|
|
3
3
|
import { healthRouter } from './routes/health.js';
|
|
4
4
|
import { createProjectsRouter } from './routes/projects.js';
|
|
5
5
|
import { createTasksRouter } from './routes/tasks.js';
|
|
6
|
+
import { createTagsRouter } from './routes/tags.js';
|
|
6
7
|
import { createCanvasRouter } from './routes/canvas.js';
|
|
7
8
|
import { createCommentsRouter } from './routes/comments.js';
|
|
8
9
|
import { createDocumentsRouter } from './routes/documents.js';
|
|
10
|
+
import { createFoldersRouter } from './routes/folders.js';
|
|
9
11
|
import { createNotesRouter } from './routes/notes.js';
|
|
10
12
|
import { createEventsRouter } from './routes/events.js';
|
|
11
13
|
import { createTokensRouter } from './routes/tokens.js';
|
|
12
14
|
import { createAgentRunsRouter } from './routes/agent-runs.js';
|
|
15
|
+
import { createGoalsRouter } from './routes/goals.js';
|
|
16
|
+
import { createSubmissionsRouter } from './routes/submissions.js';
|
|
13
17
|
import { mountStatic } from './static.js';
|
|
14
18
|
import { createServices } from './services/index.js';
|
|
15
19
|
export function createApp(deps) {
|
|
16
20
|
const services = deps.services ?? createServices({ db: deps.db, eventBus: deps.eventBus });
|
|
17
|
-
const { eventBus, projectService, taskService, canvasService, documentService, noteService, commentService, agentRunService, tokenService, } = services;
|
|
21
|
+
const { eventBus, projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, tokenService, syncService, } = services;
|
|
18
22
|
const app = new Hono();
|
|
19
23
|
if (deps.authPassword !== undefined && deps.authPassword.length > 0) {
|
|
20
24
|
app.use('*', createAuthMiddleware(deps.authPassword));
|
|
21
25
|
}
|
|
22
26
|
app.route('/api/v1', healthRouter);
|
|
23
27
|
app.route('/api/v1', createProjectsRouter(projectService, taskService));
|
|
28
|
+
app.route('/api/v1', createGoalsRouter(goalService));
|
|
24
29
|
app.route('/api/v1', createTasksRouter(taskService));
|
|
30
|
+
app.route('/api/v1', createTagsRouter(tagService));
|
|
25
31
|
app.route('/api/v1', createCanvasRouter(canvasService));
|
|
26
32
|
app.route('/api/v1', createDocumentsRouter(documentService));
|
|
33
|
+
app.route('/api/v1', createFoldersRouter(folderService));
|
|
27
34
|
app.route('/api/v1', createNotesRouter(noteService));
|
|
28
35
|
app.route('/api/v1', createCommentsRouter(commentService));
|
|
29
36
|
app.route('/api/v1', createTokensRouter(tokenService));
|
|
30
37
|
app.route('/api/v1', createAgentRunsRouter(agentRunService));
|
|
38
|
+
app.route('/api/v1', createSubmissionsRouter(syncService, projectService));
|
|
31
39
|
app.route('/api/v1', createEventsRouter(eventBus));
|
|
32
40
|
mountStatic(app);
|
|
33
41
|
if (deps.mcp) {
|
|
@@ -30,8 +30,10 @@ export declare class InvalidCanvasError extends Error {
|
|
|
30
30
|
export declare function createCanvasService(deps: CanvasServiceDeps): {
|
|
31
31
|
get(projectId: string): {
|
|
32
32
|
nodes: {
|
|
33
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
33
34
|
id: string;
|
|
34
35
|
project_id: string;
|
|
36
|
+
goal_id: string;
|
|
35
37
|
label: string;
|
|
36
38
|
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
37
39
|
description: string | null;
|
|
@@ -71,8 +73,10 @@ export declare function createCanvasService(deps: CanvasServiceDeps): {
|
|
|
71
73
|
} | undefined;
|
|
72
74
|
putLayout(projectId: string, payload: PutCanvasLayoutInput): {
|
|
73
75
|
nodes: {
|
|
76
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
74
77
|
id: string;
|
|
75
78
|
project_id: string;
|
|
79
|
+
goal_id: string;
|
|
76
80
|
label: string;
|
|
77
81
|
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
78
82
|
description: string | null;
|
package/dist/services/canvas.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEdge, createTask, deleteEdge as dbDeleteEdge, getEdgeByProjectAndId, getProject, getTask, listEdges, listTasks, updateEdge, updateProject, updateTask, } from '@plandesk/db';
|
|
1
|
+
import { createEdge, createTask, getOrCreateDefaultGoal, deleteEdge as dbDeleteEdge, getEdgeByProjectAndId, getProject, getTask, listEdges, listTasks, updateEdge, updateProject, updateTask, } from '@plandesk/db';
|
|
2
2
|
import { serializeEdge, serializeTask } from '../serialize.js';
|
|
3
3
|
export class InvalidCanvasError extends Error {
|
|
4
4
|
constructor(message) {
|
|
@@ -21,7 +21,7 @@ export function createCanvasService(deps) {
|
|
|
21
21
|
const tasks = listTasks(db, projectId);
|
|
22
22
|
const edgeRows = listEdges(db, projectId);
|
|
23
23
|
return {
|
|
24
|
-
nodes: tasks.map(serializeTask),
|
|
24
|
+
nodes: tasks.map((task) => serializeTask(task)),
|
|
25
25
|
edges: edgeRows.map(serializeEdge),
|
|
26
26
|
layout: serializeLayout(project.canvasLayout),
|
|
27
27
|
};
|
|
@@ -93,6 +93,7 @@ export function createCanvasService(deps) {
|
|
|
93
93
|
}
|
|
94
94
|
const created = createTask(tx, {
|
|
95
95
|
projectId,
|
|
96
|
+
goalId: getOrCreateDefaultGoal(tx, projectId).id,
|
|
96
97
|
id: node.id,
|
|
97
98
|
label: node.label,
|
|
98
99
|
status: 'todo',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Db } from '@plandesk/db';
|
|
2
|
-
import { type PaginationParams, type SerializedDocument, type SerializedDocumentTree } from '../serialize.js';
|
|
2
|
+
import { type PaginationParams, type SerializedDocument, type SerializedDocumentFolderTree, type SerializedDocumentTree } from '../serialize.js';
|
|
3
3
|
import type { EventBus } from '../events.js';
|
|
4
4
|
export type DocumentServiceDeps = {
|
|
5
5
|
db: Db;
|
|
@@ -10,6 +10,7 @@ export type CreateDocumentInput = {
|
|
|
10
10
|
body?: string | null;
|
|
11
11
|
statusLine?: string | null;
|
|
12
12
|
parentId?: string | null;
|
|
13
|
+
folderId?: string | null;
|
|
13
14
|
linkedTaskId?: string | null;
|
|
14
15
|
};
|
|
15
16
|
export type UpdateDocumentInput = {
|
|
@@ -17,6 +18,7 @@ export type UpdateDocumentInput = {
|
|
|
17
18
|
body?: string | null;
|
|
18
19
|
statusLine?: string | null;
|
|
19
20
|
parentId?: string | null;
|
|
21
|
+
folderId?: string | null;
|
|
20
22
|
linkedTaskId?: string | null;
|
|
21
23
|
};
|
|
22
24
|
export declare class InvalidDocumentError extends Error {
|
|
@@ -24,6 +26,8 @@ export declare class InvalidDocumentError extends Error {
|
|
|
24
26
|
}
|
|
25
27
|
export declare function createDocumentService(deps: DocumentServiceDeps): {
|
|
26
28
|
listTree(projectId: string, pagination?: PaginationParams): SerializedDocumentTree[] | undefined;
|
|
29
|
+
listFolderTree(projectId: string): SerializedDocumentFolderTree | undefined;
|
|
30
|
+
listByFolder(projectId: string, folderId: string): SerializedDocumentTree[] | undefined;
|
|
27
31
|
create(projectId: string, input: CreateDocumentInput): SerializedDocument | undefined;
|
|
28
32
|
get(id: string): SerializedDocument | undefined;
|
|
29
33
|
update(id: string, input: UpdateDocumentInput): SerializedDocument | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createDocument as dbCreateDocument, deleteCommentsByDocumentId, deleteDocument as dbDeleteDocument, detachDocumentChildren, getDocument as dbGetDocument, getDocumentByTask as dbGetDocumentByTask, getProject, getTask, listDocuments as dbListDocuments, updateDocument as dbUpdateDocument, } from '@plandesk/db';
|
|
2
|
-
import { buildDocumentTree, serializeDocument, } from '../serialize.js';
|
|
1
|
+
import { createDocument as dbCreateDocument, deleteCommentsByDocumentId, deleteDocument as dbDeleteDocument, detachDocumentChildren, getDocument as dbGetDocument, getDocumentByTask as dbGetDocumentByTask, getFolderByProjectAndId, getProject, getTask, listDocuments as dbListDocuments, listFolders as dbListFolders, updateDocument as dbUpdateDocument, } from '@plandesk/db';
|
|
2
|
+
import { buildDocumentTree, buildFolderTree, serializeDocument, } from '../serialize.js';
|
|
3
3
|
export class InvalidDocumentError extends Error {
|
|
4
4
|
constructor(message) {
|
|
5
5
|
super(message);
|
|
@@ -18,6 +18,11 @@ function assertParentInProject(db, projectId, parentId) {
|
|
|
18
18
|
throw new InvalidDocumentError('Parent document does not belong to project');
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
function assertFolderInProject(db, projectId, folderId) {
|
|
22
|
+
if (!getFolderByProjectAndId(db, projectId, folderId)) {
|
|
23
|
+
throw new InvalidDocumentError('Folder does not belong to project');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
21
26
|
export function createDocumentService(deps) {
|
|
22
27
|
const { db, eventBus } = deps;
|
|
23
28
|
return {
|
|
@@ -28,6 +33,23 @@ export function createDocumentService(deps) {
|
|
|
28
33
|
}
|
|
29
34
|
return buildDocumentTree(dbListDocuments(db, projectId, pagination));
|
|
30
35
|
},
|
|
36
|
+
listFolderTree(projectId) {
|
|
37
|
+
const project = getProject(db, projectId);
|
|
38
|
+
if (!project) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return buildFolderTree(dbListFolders(db, projectId), dbListDocuments(db, projectId));
|
|
42
|
+
},
|
|
43
|
+
listByFolder(projectId, folderId) {
|
|
44
|
+
const project = getProject(db, projectId);
|
|
45
|
+
if (!project) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (!getFolderByProjectAndId(db, projectId, folderId)) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return buildDocumentTree(dbListDocuments(db, projectId, { folderId }));
|
|
52
|
+
},
|
|
31
53
|
create(projectId, input) {
|
|
32
54
|
const project = getProject(db, projectId);
|
|
33
55
|
if (!project) {
|
|
@@ -39,12 +61,16 @@ export function createDocumentService(deps) {
|
|
|
39
61
|
if (input.parentId !== undefined && input.parentId !== null) {
|
|
40
62
|
assertParentInProject(db, projectId, input.parentId);
|
|
41
63
|
}
|
|
64
|
+
if (input.folderId !== undefined && input.folderId !== null) {
|
|
65
|
+
assertFolderInProject(db, projectId, input.folderId);
|
|
66
|
+
}
|
|
42
67
|
const document = dbCreateDocument(db, {
|
|
43
68
|
projectId,
|
|
44
69
|
title: input.title,
|
|
45
70
|
body: input.body,
|
|
46
71
|
statusLine: input.statusLine,
|
|
47
72
|
parentId: input.parentId,
|
|
73
|
+
folderId: input.folderId,
|
|
48
74
|
linkedTaskId: input.linkedTaskId,
|
|
49
75
|
});
|
|
50
76
|
eventBus.emit({
|
|
@@ -75,6 +101,9 @@ export function createDocumentService(deps) {
|
|
|
75
101
|
}
|
|
76
102
|
assertParentInProject(db, existing.projectId, input.parentId);
|
|
77
103
|
}
|
|
104
|
+
if (input.folderId !== undefined && input.folderId !== null) {
|
|
105
|
+
assertFolderInProject(db, existing.projectId, input.folderId);
|
|
106
|
+
}
|
|
78
107
|
const document = dbUpdateDocument(db, id, input);
|
|
79
108
|
if (!document) {
|
|
80
109
|
return undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Db } from '@plandesk/db';
|
|
2
|
+
import { type SerializedFolder } from '../serialize.js';
|
|
3
|
+
import type { EventBus } from '../events.js';
|
|
4
|
+
export type FolderServiceDeps = {
|
|
5
|
+
db: Db;
|
|
6
|
+
eventBus: EventBus;
|
|
7
|
+
};
|
|
8
|
+
export type CreateFolderInput = {
|
|
9
|
+
name: string;
|
|
10
|
+
parentFolderId?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type UpdateFolderInput = {
|
|
13
|
+
name?: string;
|
|
14
|
+
parentFolderId?: string | null;
|
|
15
|
+
};
|
|
16
|
+
export declare class InvalidFolderError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function createFolderService(deps: FolderServiceDeps): {
|
|
20
|
+
list(projectId: string): SerializedFolder[] | undefined;
|
|
21
|
+
create(projectId: string, input: CreateFolderInput): SerializedFolder | undefined;
|
|
22
|
+
get(id: string): SerializedFolder | undefined;
|
|
23
|
+
update(id: string, input: UpdateFolderInput): SerializedFolder | undefined;
|
|
24
|
+
delete(id: string): boolean;
|
|
25
|
+
};
|
|
26
|
+
export type FolderService = ReturnType<typeof createFolderService>;
|
|
27
|
+
//# sourceMappingURL=folders.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createFolder as dbCreateFolder, deleteFolder as dbDeleteFolder, getFolder as dbGetFolder, getProject, listFolders as dbListFolders, moveDocumentsToFolder, reparentChildFolders, updateFolder as dbUpdateFolder, } from '@plandesk/db';
|
|
2
|
+
import { serializeFolder } from '../serialize.js';
|
|
3
|
+
export class InvalidFolderError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'InvalidFolderError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function assertNonEmptyName(name) {
|
|
10
|
+
if (name.trim() === '') {
|
|
11
|
+
throw new InvalidFolderError('Folder name must not be empty');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function assertParentInProject(db, projectId, parentFolderId) {
|
|
15
|
+
const parent = dbGetFolder(db, parentFolderId);
|
|
16
|
+
if (!parent || parent.projectId !== projectId) {
|
|
17
|
+
throw new InvalidFolderError('Parent folder does not belong to project');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function assertNoCycle(db, folderId, newParentFolderId) {
|
|
21
|
+
const visited = new Set();
|
|
22
|
+
let current = newParentFolderId;
|
|
23
|
+
while (current !== null) {
|
|
24
|
+
if (current === folderId) {
|
|
25
|
+
throw new InvalidFolderError('Re-parenting would create a folder cycle');
|
|
26
|
+
}
|
|
27
|
+
if (visited.has(current)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
visited.add(current);
|
|
31
|
+
current = dbGetFolder(db, current)?.parentFolderId ?? null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function createFolderService(deps) {
|
|
35
|
+
const { db, eventBus } = deps;
|
|
36
|
+
return {
|
|
37
|
+
list(projectId) {
|
|
38
|
+
const project = getProject(db, projectId);
|
|
39
|
+
if (!project) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return dbListFolders(db, projectId).map(serializeFolder);
|
|
43
|
+
},
|
|
44
|
+
create(projectId, input) {
|
|
45
|
+
const project = getProject(db, projectId);
|
|
46
|
+
if (!project) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
assertNonEmptyName(input.name);
|
|
50
|
+
if (input.parentFolderId !== undefined && input.parentFolderId !== null) {
|
|
51
|
+
assertParentInProject(db, projectId, input.parentFolderId);
|
|
52
|
+
}
|
|
53
|
+
const folder = dbCreateFolder(db, {
|
|
54
|
+
projectId,
|
|
55
|
+
name: input.name,
|
|
56
|
+
parentFolderId: input.parentFolderId,
|
|
57
|
+
});
|
|
58
|
+
eventBus.emit({ type: 'folder_created', folderId: folder.id, projectId });
|
|
59
|
+
return serializeFolder(folder);
|
|
60
|
+
},
|
|
61
|
+
get(id) {
|
|
62
|
+
const folder = dbGetFolder(db, id);
|
|
63
|
+
if (!folder) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return serializeFolder(folder);
|
|
67
|
+
},
|
|
68
|
+
update(id, input) {
|
|
69
|
+
const existing = dbGetFolder(db, id);
|
|
70
|
+
if (!existing) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (input.name !== undefined) {
|
|
74
|
+
assertNonEmptyName(input.name);
|
|
75
|
+
}
|
|
76
|
+
if (input.parentFolderId !== undefined && input.parentFolderId !== null) {
|
|
77
|
+
if (input.parentFolderId === id) {
|
|
78
|
+
throw new InvalidFolderError('Folder cannot be its own parent');
|
|
79
|
+
}
|
|
80
|
+
assertParentInProject(db, existing.projectId, input.parentFolderId);
|
|
81
|
+
assertNoCycle(db, id, input.parentFolderId);
|
|
82
|
+
}
|
|
83
|
+
const folder = dbUpdateFolder(db, id, input);
|
|
84
|
+
if (!folder) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
eventBus.emit({ type: 'folder_updated', folderId: folder.id, projectId: folder.projectId });
|
|
88
|
+
return serializeFolder(folder);
|
|
89
|
+
},
|
|
90
|
+
delete(id) {
|
|
91
|
+
const existing = dbGetFolder(db, id);
|
|
92
|
+
if (!existing) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// Never orphan: children folders and contained documents move to the
|
|
96
|
+
// deleted folder's parent (or root when the folder was at root).
|
|
97
|
+
db.transaction((tx) => {
|
|
98
|
+
reparentChildFolders(tx, id, existing.parentFolderId);
|
|
99
|
+
moveDocumentsToFolder(tx, id, existing.parentFolderId);
|
|
100
|
+
dbDeleteFolder(tx, id);
|
|
101
|
+
});
|
|
102
|
+
eventBus.emit({ type: 'folder_updated', folderId: id, projectId: existing.projectId });
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=folders.js.map
|