@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.
Files changed (44) hide show
  1. package/dist/events.d.ts +20 -1
  2. package/dist/index.d.ts +8 -1
  3. package/dist/index.js +5 -1
  4. package/dist/routes/agent-runs.d.ts +1 -1
  5. package/dist/routes/agent-runs.js +20 -0
  6. package/dist/routes/documents.js +2 -0
  7. package/dist/routes/folders.d.ts +4 -0
  8. package/dist/routes/folders.js +69 -0
  9. package/dist/routes/goals.d.ts +4 -0
  10. package/dist/routes/goals.js +138 -0
  11. package/dist/routes/projects.d.ts +1 -1
  12. package/dist/routes/projects.js +23 -3
  13. package/dist/routes/submissions.d.ts +5 -0
  14. package/dist/routes/submissions.js +58 -0
  15. package/dist/routes/tags.d.ts +4 -0
  16. package/dist/routes/tags.js +62 -0
  17. package/dist/routes/tasks.d.ts +1 -0
  18. package/dist/routes/tasks.js +9 -1
  19. package/dist/serialize.d.ts +51 -2
  20. package/dist/serialize.js +101 -1
  21. package/dist/server.js +9 -1
  22. package/dist/services/canvas.d.ts +4 -0
  23. package/dist/services/canvas.js +3 -2
  24. package/dist/services/documents.d.ts +5 -1
  25. package/dist/services/documents.js +31 -2
  26. package/dist/services/folders.d.ts +27 -0
  27. package/dist/services/folders.js +107 -0
  28. package/dist/services/goals.d.ts +239 -0
  29. package/dist/services/goals.js +323 -0
  30. package/dist/services/index.d.ts +6 -0
  31. package/dist/services/index.js +9 -0
  32. package/dist/services/projects.js +8 -2
  33. package/dist/services/sync.d.ts +5 -3
  34. package/dist/services/sync.js +45 -2
  35. package/dist/services/tags.d.ts +27 -0
  36. package/dist/services/tags.js +79 -0
  37. package/dist/services/tasks.d.ts +20 -2
  38. package/dist/services/tasks.js +98 -26
  39. package/package.json +3 -3
  40. package/web/assets/index-DcD7-VGi.css +1 -0
  41. package/web/assets/index-Dcaq3wR-.js +318 -0
  42. package/web/index.html +2 -2
  43. package/web/assets/index-CUxSE_WZ.css +0 -1
  44. package/web/assets/index-cyyf9-Uw.js +0 -234
@@ -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 declare function serializeTask(task: Task): {
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 serializeTask(task) {
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;
@@ -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