@umudik/task-bridge 0.0.1
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/LICENSE +21 -0
- package/README.md +75 -0
- package/apps/backend/dist/config.js +26 -0
- package/apps/backend/dist/db/epic-workflow-db.js +125 -0
- package/apps/backend/dist/db/library-db.js +123 -0
- package/apps/backend/dist/db/projects-db.js +110 -0
- package/apps/backend/dist/db/tasks-db.js +282 -0
- package/apps/backend/dist/db/users-db.js +117 -0
- package/apps/backend/dist/db/workflow-db.js +186 -0
- package/apps/backend/dist/db/workflow-template-db.js +715 -0
- package/apps/backend/dist/domain/project-member.js +15 -0
- package/apps/backend/dist/domain/task-template-graph.js +63 -0
- package/apps/backend/dist/domain/task.js +93 -0
- package/apps/backend/dist/domain/work-status.js +30 -0
- package/apps/backend/dist/domain/workflow-stage.js +186 -0
- package/apps/backend/dist/domain/workflow-state.js +73 -0
- package/apps/backend/dist/domain/workflow-template-id.js +6 -0
- package/apps/backend/dist/errors/app-error.js +24 -0
- package/apps/backend/dist/index.js +67 -0
- package/apps/backend/dist/lib/bridge-project.js +24 -0
- package/apps/backend/dist/lib/inbox-cursor.js +34 -0
- package/apps/backend/dist/lib/strings.js +15 -0
- package/apps/backend/dist/logger.js +29 -0
- package/apps/backend/dist/mappers/task-response.js +261 -0
- package/apps/backend/dist/middleware/auth.js +29 -0
- package/apps/backend/dist/openapi.js +716 -0
- package/apps/backend/dist/routes/admin-users.js +79 -0
- package/apps/backend/dist/routes/auth.js +81 -0
- package/apps/backend/dist/routes/connect.js +1 -0
- package/apps/backend/dist/routes/docs.js +13 -0
- package/apps/backend/dist/routes/health.js +6 -0
- package/apps/backend/dist/routes/library.js +139 -0
- package/apps/backend/dist/routes/projects.js +95 -0
- package/apps/backend/dist/routes/tasks.js +522 -0
- package/apps/backend/dist/routes/web.js +79 -0
- package/apps/backend/dist/routes/workflow-templates.js +152 -0
- package/apps/backend/dist/routes/workflow.js +165 -0
- package/apps/backend/dist/services/connect-target.js +4 -0
- package/apps/backend/dist/services/epic-service.js +269 -0
- package/apps/backend/dist/services/library-service.js +222 -0
- package/apps/backend/dist/services/project-registry.js +122 -0
- package/apps/backend/dist/services/task-assignee-service.js +42 -0
- package/apps/backend/dist/services/task-claim-policy.js +310 -0
- package/apps/backend/dist/services/task-queue.js +105 -0
- package/apps/backend/dist/services/task-service.js +198 -0
- package/apps/backend/dist/services/workflow-rules.js +18 -0
- package/apps/backend/dist/services/workflow-service.js +418 -0
- package/apps/backend/dist/services/workflow-spawn-service.js +179 -0
- package/apps/backend/dist/services/workflow-state-service.js +157 -0
- package/apps/backend/dist/services/workflow-template-service.js +204 -0
- package/apps/backend/public/assets/index-Bl1ciVpY.js +409 -0
- package/apps/backend/public/assets/index-ByKECv-I.css +1 -0
- package/apps/backend/public/index.html +13 -0
- package/bin/task-bridge.mjs +86 -0
- package/package.json +41 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { canonicalDescription, isDoneStage, isTaskClaimed, listEpicWorkflowTasks, listSubtasks, resolveTaskStageId, } from "../domain/task.js";
|
|
2
|
+
import { isWorkDone, resolveWorkStatus, workStatusLabel } from "../domain/work-status.js";
|
|
3
|
+
import { syncEpicStage } from "../services/epic-service.js";
|
|
4
|
+
import { listTaskLibraryLinks } from "../services/library-service.js";
|
|
5
|
+
import { getStageSnapshot, getStageTitleLookup } from "../services/workflow-service.js";
|
|
6
|
+
import { AppError } from "../errors/app-error.js";
|
|
7
|
+
import { decodeInboxCursor, encodeInboxCursor, inboxItemBeforeCursor, } from "../lib/inbox-cursor.js";
|
|
8
|
+
import { listBridgeTasks } from "../services/task-service.js";
|
|
9
|
+
import { listWorkflowStateSummaries } from "../services/workflow-state-service.js";
|
|
10
|
+
function latestCommentByRole(comments, role) {
|
|
11
|
+
for (let index = comments.length - 1; index >= 0; index -= 1) {
|
|
12
|
+
const entry = comments[index];
|
|
13
|
+
if (!entry)
|
|
14
|
+
continue;
|
|
15
|
+
if (entry.role === role)
|
|
16
|
+
return entry;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function consumerStatus(task) {
|
|
21
|
+
if (!task)
|
|
22
|
+
return "sent";
|
|
23
|
+
if (isTaskClaimed(task))
|
|
24
|
+
return "sent";
|
|
25
|
+
if (isWorkDone(task))
|
|
26
|
+
return "ready";
|
|
27
|
+
const lastUser = latestCommentByRole(task.comments, "user");
|
|
28
|
+
const lastSystem = latestCommentByRole(task.comments, "system");
|
|
29
|
+
if (lastUser !== null && lastSystem !== null) {
|
|
30
|
+
const userAt = Date.parse(lastUser.at);
|
|
31
|
+
const systemAt = Date.parse(lastSystem.at);
|
|
32
|
+
if (!Number.isNaN(userAt) && !Number.isNaN(systemAt) && userAt > systemAt) {
|
|
33
|
+
return "sent";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (lastSystem !== null)
|
|
37
|
+
return "ready";
|
|
38
|
+
if (lastUser !== null && lastUser.authorId !== task.createdBy)
|
|
39
|
+
return "ready";
|
|
40
|
+
return "sent";
|
|
41
|
+
}
|
|
42
|
+
export function mapComments(task) {
|
|
43
|
+
return task.comments.map((comment) => ({
|
|
44
|
+
id: comment.id,
|
|
45
|
+
role: comment.role,
|
|
46
|
+
authorId: comment.authorId,
|
|
47
|
+
tags: comment.tags,
|
|
48
|
+
body: comment.body,
|
|
49
|
+
at: comment.at,
|
|
50
|
+
metadata: comment.metadata,
|
|
51
|
+
by: comment.authorId,
|
|
52
|
+
text: comment.body,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
function previewForTask(task) {
|
|
56
|
+
const lastComment = task.comments[task.comments.length - 1];
|
|
57
|
+
if (!lastComment)
|
|
58
|
+
return null;
|
|
59
|
+
const text = lastComment.body;
|
|
60
|
+
if (!text)
|
|
61
|
+
return null;
|
|
62
|
+
if (text.length > 160)
|
|
63
|
+
return `${text.slice(0, 157)}...`;
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
function resolveStageTitle(stageId, projectId, stageTitles) {
|
|
67
|
+
if (stageId === null)
|
|
68
|
+
return null;
|
|
69
|
+
const fromMap = stageTitles.get(`${projectId}:${stageId}`);
|
|
70
|
+
if (fromMap)
|
|
71
|
+
return fromMap;
|
|
72
|
+
return stageId;
|
|
73
|
+
}
|
|
74
|
+
function mapSubtaskSummary(task, stageTitles, allTasks) {
|
|
75
|
+
const resolvedStage = resolveTaskStageId(allTasks, task);
|
|
76
|
+
let stageId = task.stageId;
|
|
77
|
+
if (resolvedStage !== null) {
|
|
78
|
+
stageId = resolvedStage;
|
|
79
|
+
}
|
|
80
|
+
const stageTitle = resolveStageTitle(stageId, task.projectId, stageTitles);
|
|
81
|
+
const workStatus = resolveWorkStatus(task);
|
|
82
|
+
return {
|
|
83
|
+
taskId: task.id,
|
|
84
|
+
parentId: task.parentId,
|
|
85
|
+
title: task.title,
|
|
86
|
+
stageId,
|
|
87
|
+
stageTitle,
|
|
88
|
+
templateId: task.templateId,
|
|
89
|
+
assignee: task.assignee,
|
|
90
|
+
claimedBy: task.claimedBy,
|
|
91
|
+
workStatus,
|
|
92
|
+
workStatusLabel: workStatusLabel(workStatus),
|
|
93
|
+
done: workStatus === "done",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function mapTaskDetail(task) {
|
|
97
|
+
if (task.parentId === null) {
|
|
98
|
+
syncEpicStage(task.id);
|
|
99
|
+
const refreshed = listBridgeTasks().find((entry) => entry.id === task.id);
|
|
100
|
+
if (refreshed)
|
|
101
|
+
task = refreshed;
|
|
102
|
+
}
|
|
103
|
+
const stage = getStageSnapshot(task.projectId, task.stageId);
|
|
104
|
+
const allTasks = listBridgeTasks();
|
|
105
|
+
const stageTitles = getStageTitleLookup(task.projectId);
|
|
106
|
+
let workflowSubtasks;
|
|
107
|
+
if (task.parentId === null) {
|
|
108
|
+
workflowSubtasks = listEpicWorkflowTasks(allTasks, task.id);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
workflowSubtasks = listSubtasks(allTasks, task.id);
|
|
112
|
+
}
|
|
113
|
+
const subtasks = workflowSubtasks.map((entry) => mapSubtaskSummary(entry, stageTitles, allTasks));
|
|
114
|
+
let parent = null;
|
|
115
|
+
if (task.parentId !== null) {
|
|
116
|
+
const found = allTasks.find((entry) => entry.id === task.parentId);
|
|
117
|
+
if (found)
|
|
118
|
+
parent = found;
|
|
119
|
+
}
|
|
120
|
+
let parentPayload = null;
|
|
121
|
+
if (parent !== null) {
|
|
122
|
+
parentPayload = { taskId: parent.id, title: parent.title, stageId: parent.stageId };
|
|
123
|
+
}
|
|
124
|
+
let workStatusPayload = null;
|
|
125
|
+
if (task.parentId !== null) {
|
|
126
|
+
workStatusPayload = resolveWorkStatus(task);
|
|
127
|
+
}
|
|
128
|
+
let workStatusLabelPayload = null;
|
|
129
|
+
if (task.parentId !== null) {
|
|
130
|
+
workStatusLabelPayload = workStatusLabel(resolveWorkStatus(task));
|
|
131
|
+
}
|
|
132
|
+
let workflowState = null;
|
|
133
|
+
if (task.parentId === null) {
|
|
134
|
+
workflowState = listWorkflowStateSummaries(task.id).map((node) => Object.assign({}, node, { workStatusLabel: workStatusLabel(node.workStatus) }));
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
taskId: task.id,
|
|
138
|
+
title: task.title,
|
|
139
|
+
request: canonicalDescription(task),
|
|
140
|
+
description: canonicalDescription(task),
|
|
141
|
+
priority: task.priority,
|
|
142
|
+
labels: task.labels,
|
|
143
|
+
assignee: task.assignee,
|
|
144
|
+
parentId: task.parentId,
|
|
145
|
+
parent: parentPayload,
|
|
146
|
+
subtasks,
|
|
147
|
+
stageId: task.stageId,
|
|
148
|
+
stage,
|
|
149
|
+
workStatus: workStatusPayload,
|
|
150
|
+
workStatusLabel: workStatusLabelPayload,
|
|
151
|
+
isEpic: task.parentId === null,
|
|
152
|
+
status: consumerStatus(task),
|
|
153
|
+
createdAt: task.createdAt,
|
|
154
|
+
updatedAt: task.updatedAt,
|
|
155
|
+
createdBy: task.createdBy,
|
|
156
|
+
projectId: task.projectId,
|
|
157
|
+
projectName: task.projectName,
|
|
158
|
+
claimedBy: task.claimedBy,
|
|
159
|
+
events: task.events,
|
|
160
|
+
comments: mapComments(task),
|
|
161
|
+
libraryLinks: listTaskLibraryLinks(task.id),
|
|
162
|
+
workflowState,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function parseActivityTime(item) {
|
|
166
|
+
let raw = null;
|
|
167
|
+
if (item.activityAt !== null) {
|
|
168
|
+
raw = item.activityAt;
|
|
169
|
+
}
|
|
170
|
+
else if (item.createdAt !== null) {
|
|
171
|
+
raw = item.createdAt;
|
|
172
|
+
}
|
|
173
|
+
if (!raw)
|
|
174
|
+
return NaN;
|
|
175
|
+
return Date.parse(raw);
|
|
176
|
+
}
|
|
177
|
+
function sortInboxByActivity(items) {
|
|
178
|
+
return items.slice().sort((a, b) => {
|
|
179
|
+
const aTime = parseActivityTime(a);
|
|
180
|
+
const bTime = parseActivityTime(b);
|
|
181
|
+
if (!Number.isNaN(aTime) && !Number.isNaN(bTime) && aTime !== bTime) {
|
|
182
|
+
return bTime - aTime;
|
|
183
|
+
}
|
|
184
|
+
return b.taskId - a.taskId;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function buildInboxItems(query) {
|
|
188
|
+
const bridgeTasks = listBridgeTasks();
|
|
189
|
+
const stageTitles = new Map();
|
|
190
|
+
for (const task of bridgeTasks) {
|
|
191
|
+
if (stageTitles.has(task.projectId))
|
|
192
|
+
continue;
|
|
193
|
+
const lookup = getStageTitleLookup(task.projectId);
|
|
194
|
+
for (const [stageId, title] of lookup.entries()) {
|
|
195
|
+
stageTitles.set(`${task.projectId}:${stageId}`, title);
|
|
196
|
+
}
|
|
197
|
+
stageTitles.set(task.projectId, "loaded");
|
|
198
|
+
}
|
|
199
|
+
let items = bridgeTasks.map((task) => {
|
|
200
|
+
let activityAt = task.createdAt;
|
|
201
|
+
if (task.updatedAt) {
|
|
202
|
+
activityAt = task.updatedAt;
|
|
203
|
+
}
|
|
204
|
+
else if (task.claimedAt !== null) {
|
|
205
|
+
activityAt = task.claimedAt;
|
|
206
|
+
}
|
|
207
|
+
const stageTitle = resolveStageTitle(task.stageId, task.projectId, stageTitles);
|
|
208
|
+
return {
|
|
209
|
+
taskId: task.id,
|
|
210
|
+
title: task.title,
|
|
211
|
+
preview: previewForTask(task),
|
|
212
|
+
status: consumerStatus(task),
|
|
213
|
+
activityAt,
|
|
214
|
+
updatedAt: activityAt,
|
|
215
|
+
createdAt: task.createdAt,
|
|
216
|
+
done: isDoneStage(task.stageId),
|
|
217
|
+
parentId: task.parentId,
|
|
218
|
+
projectId: task.projectId,
|
|
219
|
+
projectName: task.projectName,
|
|
220
|
+
createdBy: task.createdBy,
|
|
221
|
+
claimedBy: task.claimedBy,
|
|
222
|
+
assignee: task.assignee,
|
|
223
|
+
stageId: task.stageId,
|
|
224
|
+
stageTitle,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
if (query.projectId !== null) {
|
|
228
|
+
items = items.filter((item) => item.projectId === query.projectId);
|
|
229
|
+
}
|
|
230
|
+
if (query.commentsOnly) {
|
|
231
|
+
items = items.filter((item) => item.status === "ready");
|
|
232
|
+
}
|
|
233
|
+
if (query.epicsOnly) {
|
|
234
|
+
items = items.filter((item) => item.parentId === null);
|
|
235
|
+
}
|
|
236
|
+
items = sortInboxByActivity(items);
|
|
237
|
+
if (query.cursor !== null) {
|
|
238
|
+
const decoded = decodeInboxCursor(query.cursor);
|
|
239
|
+
if (!decoded)
|
|
240
|
+
throw new AppError("Invalid cursor", 400);
|
|
241
|
+
const cursorTime = Date.parse(decoded.activityAt);
|
|
242
|
+
items = items.filter((item) => inboxItemBeforeCursor(item, cursorTime, decoded.taskId, parseActivityTime));
|
|
243
|
+
}
|
|
244
|
+
const slice = items.slice(0, query.limit + 1);
|
|
245
|
+
const hasMore = slice.length > query.limit;
|
|
246
|
+
let pageItems = slice;
|
|
247
|
+
if (hasMore) {
|
|
248
|
+
pageItems = slice.slice(0, query.limit);
|
|
249
|
+
}
|
|
250
|
+
const lastItem = pageItems[pageItems.length - 1];
|
|
251
|
+
let nextCursor = null;
|
|
252
|
+
if (hasMore && lastItem) {
|
|
253
|
+
nextCursor = encodeInboxCursor(lastItem);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
items: pageItems,
|
|
257
|
+
limit: query.limit,
|
|
258
|
+
nextCursor,
|
|
259
|
+
hasMore,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AppError } from "../errors/app-error.js";
|
|
2
|
+
import { listUserRows } from "../db/users-db.js";
|
|
3
|
+
export function resolveAuthUser(request) {
|
|
4
|
+
const authHeader = request.headers["authorization"];
|
|
5
|
+
let token = "";
|
|
6
|
+
if (String(authHeader) === authHeader && authHeader.startsWith("Bearer ")) {
|
|
7
|
+
token = authHeader.slice(7);
|
|
8
|
+
}
|
|
9
|
+
if (token === "") {
|
|
10
|
+
throw new AppError("Unauthorized", 401);
|
|
11
|
+
}
|
|
12
|
+
const rows = listUserRows({ id: "", email: "", token });
|
|
13
|
+
if (rows.length === 0) {
|
|
14
|
+
throw new AppError("Unauthorized", 401);
|
|
15
|
+
}
|
|
16
|
+
const row = rows[0];
|
|
17
|
+
if (!row)
|
|
18
|
+
throw new AppError("Unauthorized", 401);
|
|
19
|
+
return row;
|
|
20
|
+
}
|
|
21
|
+
export function assertAuth(request) {
|
|
22
|
+
const row = resolveAuthUser(request);
|
|
23
|
+
if (row.must_change_password === 1) {
|
|
24
|
+
throw new AppError("Password change required", 403, {
|
|
25
|
+
code: "PASSWORD_CHANGE_REQUIRED",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return row;
|
|
29
|
+
}
|