@plandesk/api 0.6.0 → 0.8.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.
@@ -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
@@ -3,9 +3,11 @@ import { type EventBus } from '../events.js';
3
3
  import { type CanvasService } from './canvas.js';
4
4
  import { type CommentService } from './comments.js';
5
5
  import { type DocumentService } from './documents.js';
6
+ import { type FolderService } from './folders.js';
6
7
  import { type NoteService } from './notes.js';
7
8
  import { type ProjectService } from './projects.js';
8
9
  import { type AgentRunService } from './agent-runs.js';
10
+ import { type TagService } from './tags.js';
9
11
  import { type TaskService } from './tasks.js';
10
12
  import { type TokenService } from './tokens.js';
11
13
  import { type ShareService } from './share.js';
@@ -18,8 +20,10 @@ export type Services = {
18
20
  eventBus: EventBus;
19
21
  projectService: ProjectService;
20
22
  taskService: TaskService;
23
+ tagService: TagService;
21
24
  canvasService: CanvasService;
22
25
  documentService: DocumentService;
26
+ folderService: FolderService;
23
27
  noteService: NoteService;
24
28
  commentService: CommentService;
25
29
  agentRunService: AgentRunService;
@@ -2,9 +2,11 @@ import { createEventBus } from '../events.js';
2
2
  import { createCanvasService } from './canvas.js';
3
3
  import { createCommentService } from './comments.js';
4
4
  import { createDocumentService } from './documents.js';
5
+ import { createFolderService } from './folders.js';
5
6
  import { createNoteService } from './notes.js';
6
7
  import { createProjectService } from './projects.js';
7
8
  import { createAgentRunService } from './agent-runs.js';
9
+ import { createTagService } from './tags.js';
8
10
  import { createTaskService } from './tasks.js';
9
11
  import { createTokenService } from './tokens.js';
10
12
  import { createShareService } from './share.js';
@@ -13,8 +15,10 @@ export function createServices(deps) {
13
15
  const eventBus = deps.eventBus ?? createEventBus();
14
16
  const projectService = createProjectService({ db: deps.db, eventBus });
15
17
  const taskService = createTaskService({ db: deps.db, eventBus });
18
+ const tagService = createTagService({ db: deps.db, eventBus });
16
19
  const canvasService = createCanvasService({ db: deps.db, eventBus });
17
20
  const documentService = createDocumentService({ db: deps.db, eventBus });
21
+ const folderService = createFolderService({ db: deps.db, eventBus });
18
22
  const noteService = createNoteService({ db: deps.db, eventBus });
19
23
  const commentService = createCommentService({ db: deps.db, eventBus });
20
24
  const agentRunService = createAgentRunService({ db: deps.db, eventBus });
@@ -25,8 +29,10 @@ export function createServices(deps) {
25
29
  eventBus,
26
30
  projectService,
27
31
  taskService,
32
+ tagService,
28
33
  canvasService,
29
34
  documentService,
35
+ folderService,
30
36
  noteService,
31
37
  commentService,
32
38
  agentRunService,
@@ -1,4 +1,4 @@
1
- import { clearDocumentParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
1
+ import { clearDocumentParentRefsByProject, clearFolderParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteFoldersByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteTagsByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
2
2
  import { emptyTaskStatusSummary, serializeDocument, serializeEdge, serializeProject, serializeProjectDetail, serializeTask, } from '../serialize.js';
3
3
  export class InvalidScaffoldError extends Error {
4
4
  constructor(message) {
@@ -89,7 +89,10 @@ export function createProjectService(deps) {
89
89
  clearDocumentParentRefsByProject(tx, id);
90
90
  deleteCommentsByProjectId(tx, id);
91
91
  deleteDocumentsByProjectId(tx, id);
92
+ clearFolderParentRefsByProject(tx, id);
93
+ deleteFoldersByProjectId(tx, id);
92
94
  deleteNotesByProjectId(tx, id);
95
+ deleteTagsByProjectId(tx, id);
93
96
  deleteTasksByProjectId(tx, id);
94
97
  deleteShareSubmissionsByProjectId(tx, id);
95
98
  deleteSyncStateByProjectId(tx, id);
@@ -164,7 +167,7 @@ export function createProjectService(deps) {
164
167
  }
165
168
  return {
166
169
  project: serializeProject(project),
167
- tasks: taskRows.map(serializeTask),
170
+ tasks: taskRows.map((task) => serializeTask(task)),
168
171
  edges: edgeRows.map(serializeEdge),
169
172
  documents: documentRows.map(serializeDocument),
170
173
  key_to_id: Object.fromEntries(keyToId),
@@ -0,0 +1,27 @@
1
+ import { type Db } from '@plandesk/db';
2
+ import type { EventBus } from '../events.js';
3
+ import { type SerializedTag } from '../serialize.js';
4
+ export type TagServiceDeps = {
5
+ db: Db;
6
+ eventBus: EventBus;
7
+ };
8
+ export type CreateTagInput = {
9
+ name: string;
10
+ color?: string | null;
11
+ };
12
+ export type UpdateTagInput = {
13
+ name?: string;
14
+ color?: string | null;
15
+ };
16
+ export declare class InvalidTagError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ export declare function normalizeTagName(name: string): string;
20
+ export declare function createTagService(deps: TagServiceDeps): {
21
+ list(projectId: string): SerializedTag[] | undefined;
22
+ create(projectId: string, input: CreateTagInput): SerializedTag | undefined;
23
+ update(id: string, input: UpdateTagInput): SerializedTag | undefined;
24
+ delete(id: string): boolean;
25
+ };
26
+ export type TagService = ReturnType<typeof createTagService>;
27
+ //# sourceMappingURL=tags.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { createTag as dbCreateTag, deleteTag as dbDeleteTag, getProject, getTag as dbGetTag, getTagByName, listTags as dbListTags, updateTag as dbUpdateTag, } from '@plandesk/db';
2
+ import { serializeTag } from '../serialize.js';
3
+ export class InvalidTagError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'InvalidTagError';
7
+ }
8
+ }
9
+ export function normalizeTagName(name) {
10
+ const trimmed = name.trim();
11
+ if (trimmed === '') {
12
+ throw new InvalidTagError('Tag name must not be empty');
13
+ }
14
+ return trimmed;
15
+ }
16
+ export function createTagService(deps) {
17
+ const { db, eventBus } = deps;
18
+ return {
19
+ list(projectId) {
20
+ const project = getProject(db, projectId);
21
+ if (!project) {
22
+ return undefined;
23
+ }
24
+ return dbListTags(db, projectId).map(serializeTag);
25
+ },
26
+ create(projectId, input) {
27
+ const project = getProject(db, projectId);
28
+ if (!project) {
29
+ return undefined;
30
+ }
31
+ const name = normalizeTagName(input.name);
32
+ if (getTagByName(db, projectId, name)) {
33
+ throw new InvalidTagError(`Tag already exists: ${name}`);
34
+ }
35
+ const tag = dbCreateTag(db, { projectId, name, color: input.color });
36
+ eventBus.emit({ type: 'tag_updated', projectId });
37
+ return serializeTag(tag);
38
+ },
39
+ // Renaming propagates everywhere automatically: tasks reference the single
40
+ // tag row through the join table.
41
+ update(id, input) {
42
+ const existing = dbGetTag(db, id);
43
+ if (!existing) {
44
+ return undefined;
45
+ }
46
+ let name;
47
+ if (input.name !== undefined) {
48
+ name = normalizeTagName(input.name);
49
+ const conflict = getTagByName(db, existing.projectId, name);
50
+ if (conflict && conflict.id !== id) {
51
+ throw new InvalidTagError(`Tag already exists: ${name}`);
52
+ }
53
+ }
54
+ const tag = dbUpdateTag(db, id, {
55
+ ...(name !== undefined ? { name } : {}),
56
+ ...(input.color !== undefined ? { color: input.color } : {}),
57
+ });
58
+ if (!tag) {
59
+ return undefined;
60
+ }
61
+ eventBus.emit({ type: 'tag_updated', projectId: tag.projectId });
62
+ return serializeTag(tag);
63
+ },
64
+ // Deleting a tag removes it from all its tasks (cascade on the join table).
65
+ delete(id) {
66
+ const existing = dbGetTag(db, id);
67
+ if (!existing) {
68
+ return false;
69
+ }
70
+ const deleted = dbDeleteTag(db, id);
71
+ if (!deleted) {
72
+ return false;
73
+ }
74
+ eventBus.emit({ type: 'tag_updated', projectId: existing.projectId });
75
+ return true;
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=tags.js.map
@@ -23,6 +23,7 @@ export type CreateTaskInput = {
23
23
  y?: number;
24
24
  assignee?: string | null;
25
25
  dueDate?: Date | null;
26
+ tags?: string[];
26
27
  };
27
28
  export type UpdateTaskInput = {
28
29
  label?: string;
@@ -30,12 +31,29 @@ export type UpdateTaskInput = {
30
31
  description?: string | null;
31
32
  x?: number;
32
33
  y?: number;
34
+ tags?: string[];
33
35
  };
34
36
  export type ListTasksFilter = {
35
37
  status?: string;
38
+ tags?: string[];
36
39
  };
37
40
  export declare function createTaskService(deps: TaskServiceDeps): {
41
+ get(id: string): {
42
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
43
+ id: string;
44
+ project_id: string;
45
+ label: string;
46
+ status: "scope" | "todo" | "in_progress" | "done" | "backlog";
47
+ description: string | null;
48
+ x: number;
49
+ y: number;
50
+ assignee: string | null;
51
+ due_date: string | null;
52
+ created_at: string;
53
+ updated_at: string;
54
+ } | undefined;
38
55
  listByProject(projectId: string, filter?: ListTasksFilter, pagination?: PaginationParams): {
56
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
39
57
  id: string;
40
58
  project_id: string;
41
59
  label: string;
@@ -49,6 +67,7 @@ export declare function createTaskService(deps: TaskServiceDeps): {
49
67
  updated_at: string;
50
68
  }[] | undefined;
51
69
  create(projectId: string, input: CreateTaskInput): {
70
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
52
71
  id: string;
53
72
  project_id: string;
54
73
  label: string;
@@ -62,6 +81,7 @@ export declare function createTaskService(deps: TaskServiceDeps): {
62
81
  updated_at: string;
63
82
  } | undefined;
64
83
  update(id: string, input: UpdateTaskInput): {
84
+ tags?: import("../serialize.js").SerializedTag[] | undefined;
65
85
  id: string;
66
86
  project_id: string;
67
87
  label: string;
@@ -75,7 +95,9 @@ export declare function createTaskService(deps: TaskServiceDeps): {
75
95
  updated_at: string;
76
96
  } | undefined;
77
97
  delete(id: string): boolean;
78
- nextActionable(projectId: string): NextActionableResult | undefined;
98
+ nextActionable(projectId: string, filter?: {
99
+ tags?: string[];
100
+ }): NextActionableResult | undefined;
79
101
  };
80
102
  export type TaskService = ReturnType<typeof createTaskService>;
81
103
  export {};
@@ -1,5 +1,6 @@
1
- import { createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, getProject, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTasks, nullDocumentsLinkedTask, updateTask, } from '@plandesk/db';
1
+ import { createTag, createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, deleteTaskTagsByTaskId, getProject, getTagByName, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTagsByTaskForProject, listTagsForTask, listTasks, nullDocumentsLinkedTask, setTaskTags, taskIdsWithAnyTagName, updateTask, } from '@plandesk/db';
2
2
  import { serializeTask } from '../serialize.js';
3
+ import { normalizeTagName } from './tags.js';
3
4
  // depends_on: prerequisite = to, dependent = from. All other labels: prerequisite = from, dependent = to.
4
5
  function prerequisiteAndDependent(edge) {
5
6
  if (edge.fromTaskId === edge.toTaskId) {
@@ -10,9 +11,31 @@ function prerequisiteAndDependent(edge) {
10
11
  }
11
12
  return { prerequisite: edge.fromTaskId, dependent: edge.toTaskId };
12
13
  }
14
+ // Resolves tag names to ids, auto-creating tags that do not exist yet.
15
+ function resolveTagIdsByName(db, projectId, names) {
16
+ const seen = new Set();
17
+ const ids = [];
18
+ for (const raw of names) {
19
+ const name = normalizeTagName(raw);
20
+ if (seen.has(name)) {
21
+ continue;
22
+ }
23
+ seen.add(name);
24
+ const tag = getTagByName(db, projectId, name) ?? createTag(db, { projectId, name });
25
+ ids.push(tag.id);
26
+ }
27
+ return ids;
28
+ }
13
29
  export function createTaskService(deps) {
14
30
  const { db, eventBus } = deps;
15
31
  return {
32
+ get(id) {
33
+ const task = getTask(db, id);
34
+ if (!task) {
35
+ return undefined;
36
+ }
37
+ return serializeTask(task, listTagsForTask(db, id));
38
+ },
16
39
  listByProject(projectId, filter = {}, pagination = {}) {
17
40
  if (filter.status !== undefined && !isTaskStatus(filter.status)) {
18
41
  throw new InvalidTaskStatusError(filter.status);
@@ -22,10 +45,13 @@ export function createTaskService(deps) {
22
45
  return undefined;
23
46
  }
24
47
  const statusFilter = filter.status;
25
- const tasks = statusFilter !== undefined
26
- ? listTasks(db, projectId, { status: statusFilter, ...pagination })
27
- : listTasks(db, projectId, pagination);
28
- return tasks.map(serializeTask);
48
+ const tasks = listTasks(db, projectId, {
49
+ ...(statusFilter !== undefined ? { status: statusFilter } : {}),
50
+ ...(filter.tags !== undefined ? { tagNames: filter.tags.map(normalizeTagName) } : {}),
51
+ ...pagination,
52
+ });
53
+ const tagsByTask = listTagsByTaskForProject(db, projectId);
54
+ return tasks.map((task) => serializeTask(task, tagsByTask.get(task.id) ?? []));
29
55
  },
30
56
  create(projectId, input) {
31
57
  if (input.status !== undefined && !isTaskStatus(input.status)) {
@@ -35,22 +61,28 @@ export function createTaskService(deps) {
35
61
  if (!project) {
36
62
  return undefined;
37
63
  }
38
- const task = createTask(db, {
39
- projectId,
40
- label: input.label,
41
- status: input.status,
42
- description: input.description,
43
- x: input.x,
44
- y: input.y,
45
- assignee: input.assignee,
46
- dueDate: input.dueDate,
64
+ const { task, tags } = db.transaction((tx) => {
65
+ const row = createTask(tx, {
66
+ projectId,
67
+ label: input.label,
68
+ status: input.status,
69
+ description: input.description,
70
+ x: input.x,
71
+ y: input.y,
72
+ assignee: input.assignee,
73
+ dueDate: input.dueDate,
74
+ });
75
+ if (input.tags !== undefined) {
76
+ setTaskTags(tx, row.id, resolveTagIdsByName(tx, projectId, input.tags));
77
+ }
78
+ return { task: row, tags: listTagsForTask(tx, row.id) };
47
79
  });
48
80
  eventBus.emit({
49
81
  type: 'task_updated',
50
82
  taskId: task.id,
51
83
  projectId,
52
84
  });
53
- return serializeTask(task);
85
+ return serializeTask(task, tags);
54
86
  },
55
87
  update(id, input) {
56
88
  if (input.status !== undefined && !isTaskStatus(input.status)) {
@@ -60,16 +92,26 @@ export function createTaskService(deps) {
60
92
  if (!existing) {
61
93
  return undefined;
62
94
  }
63
- const task = updateTask(db, id, input);
64
- if (!task) {
95
+ const { tags: tagNames, ...columns } = input;
96
+ const result = db.transaction((tx) => {
97
+ const row = updateTask(tx, id, columns);
98
+ if (!row) {
99
+ return undefined;
100
+ }
101
+ if (tagNames !== undefined) {
102
+ setTaskTags(tx, id, resolveTagIdsByName(tx, existing.projectId, tagNames));
103
+ }
104
+ return { task: row, tags: listTagsForTask(tx, id) };
105
+ });
106
+ if (!result) {
65
107
  return undefined;
66
108
  }
67
109
  eventBus.emit({
68
110
  type: 'task_updated',
69
- taskId: task.id,
70
- projectId: task.projectId,
111
+ taskId: result.task.id,
112
+ projectId: result.task.projectId,
71
113
  });
72
- return serializeTask(task);
114
+ return serializeTask(result.task, result.tags);
73
115
  },
74
116
  delete(id) {
75
117
  const task = getTask(db, id);
@@ -80,12 +122,16 @@ export function createTaskService(deps) {
80
122
  db.transaction((tx) => {
81
123
  deleteEdgesByTaskId(tx, id);
82
124
  nullDocumentsLinkedTask(tx, id);
125
+ deleteTaskTagsByTaskId(tx, id);
83
126
  dbDeleteTask(tx, id);
84
127
  });
85
128
  eventBus.emit({ type: 'canvas_updated', projectId });
86
129
  return true;
87
130
  },
88
- nextActionable(projectId) {
131
+ // filter.tags (OR semantics): only tasks carrying ANY of the given tag names
132
+ // are candidates for next_task / blocked; prerequisite completion is still
133
+ // evaluated against all tasks in the project.
134
+ nextActionable(projectId, filter = {}) {
89
135
  const project = getProject(db, projectId);
90
136
  if (!project) {
91
137
  return undefined;
@@ -93,6 +139,11 @@ export function createTaskService(deps) {
93
139
  const tasks = listTasks(db, projectId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
94
140
  const edges = listEdges(db, projectId);
95
141
  const taskById = new Map(tasks.map((task) => [task.id, task]));
142
+ const tagsByTask = listTagsByTaskForProject(db, projectId);
143
+ const tagMatches = filter.tags !== undefined && filter.tags.length > 0
144
+ ? taskIdsWithAnyTagName(db, projectId, filter.tags.map(normalizeTagName))
145
+ : undefined;
146
+ const serialize = (task) => serializeTask(task, tagsByTask.get(task.id) ?? []);
96
147
  const prerequisites = new Map();
97
148
  for (const edge of edges) {
98
149
  const pair = prerequisiteAndDependent(edge);
@@ -109,7 +160,7 @@ export function createTaskService(deps) {
109
160
  if (tasks.length === 0) {
110
161
  return { next_task: null, reason: 'no_tasks', blocked: [] };
111
162
  }
112
- const todoTasks = tasks.filter((task) => task.status === 'todo');
163
+ const todoTasks = tasks.filter((task) => task.status === 'todo' && (tagMatches === undefined || tagMatches.has(task.id)));
113
164
  if (todoTasks.length === 0) {
114
165
  return { next_task: null, reason: 'no_todo_tasks', blocked: [] };
115
166
  }
@@ -134,19 +185,19 @@ export function createTaskService(deps) {
134
185
  return [...prereqs]
135
186
  .map((id) => taskById.get(id))
136
187
  .filter((task) => task !== undefined && task.status !== 'done')
137
- .map(serializeTask);
188
+ .map(serialize);
138
189
  };
139
190
  const blocked = [];
140
191
  let nextTask = null;
141
192
  for (const task of todoTasks) {
142
193
  if (isActionable(task.id)) {
143
194
  if (nextTask === null) {
144
- nextTask = serializeTask(task);
195
+ nextTask = serialize(task);
145
196
  }
146
197
  }
147
198
  else {
148
199
  blocked.push({
149
- task: serializeTask(task),
200
+ task: serialize(task),
150
201
  waiting_on: unfinishedPrereqs(task.id),
151
202
  });
152
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plandesk/api",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -18,8 +18,8 @@
18
18
  "devDependencies": {
19
19
  "typescript": "^6.0.3",
20
20
  "vitest": "^3.2.6",
21
- "@plandesk/db": "0.5.0",
22
- "@plandesk/sync-server": "0.4.2"
21
+ "@plandesk/db": "0.6.0",
22
+ "@plandesk/sync-server": "0.5.0"
23
23
  },
24
24
  "dependencies": {
25
25
  "@hono/node-server": "^2.0.4",