@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,282 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { config } from "../config.js";
|
|
6
|
+
import { DONE_STAGE_ID, sortTasks, } from "../domain/task.js";
|
|
7
|
+
import { isWorkStatus } from "../domain/work-status.js";
|
|
8
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
function resolveDatabasePath() {
|
|
10
|
+
if (config.databasePath)
|
|
11
|
+
return config.databasePath;
|
|
12
|
+
return join(moduleDir, "..", "..", "..", "..", "data", "bridge.db");
|
|
13
|
+
}
|
|
14
|
+
let db = null;
|
|
15
|
+
function migrate(database) {
|
|
16
|
+
database.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
18
|
+
id INTEGER PRIMARY KEY,
|
|
19
|
+
project_id TEXT NOT NULL,
|
|
20
|
+
project_name TEXT NOT NULL,
|
|
21
|
+
parent_id INTEGER,
|
|
22
|
+
epic_id INTEGER,
|
|
23
|
+
template_id TEXT,
|
|
24
|
+
title TEXT NOT NULL,
|
|
25
|
+
description TEXT NOT NULL DEFAULT '',
|
|
26
|
+
priority TEXT,
|
|
27
|
+
labels_json TEXT NOT NULL DEFAULT '[]',
|
|
28
|
+
assignee TEXT,
|
|
29
|
+
assignee_role TEXT,
|
|
30
|
+
created_by TEXT NOT NULL,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
updated_at TEXT NOT NULL,
|
|
33
|
+
claimed_by TEXT,
|
|
34
|
+
claimed_at TEXT,
|
|
35
|
+
answered_by TEXT,
|
|
36
|
+
answered_at TEXT,
|
|
37
|
+
answer TEXT,
|
|
38
|
+
stage_id TEXT,
|
|
39
|
+
work_status TEXT,
|
|
40
|
+
comments_json TEXT NOT NULL DEFAULT '[]',
|
|
41
|
+
events_json TEXT NOT NULL DEFAULT '[]'
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_stage_id ON tasks(stage_id);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_epic_id ON tasks(epic_id);
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
function rowToTask(row) {
|
|
50
|
+
let parentId = null;
|
|
51
|
+
if (row.parent_id !== null) {
|
|
52
|
+
parentId = Number(row.parent_id);
|
|
53
|
+
}
|
|
54
|
+
let epicId = null;
|
|
55
|
+
if (row.epic_id !== null) {
|
|
56
|
+
epicId = Number(row.epic_id);
|
|
57
|
+
}
|
|
58
|
+
let templateId = null;
|
|
59
|
+
if (row.template_id !== null) {
|
|
60
|
+
const rawTemplateId = row.template_id;
|
|
61
|
+
templateId = String(rawTemplateId) || null;
|
|
62
|
+
}
|
|
63
|
+
let priority = null;
|
|
64
|
+
if (row.priority !== null) {
|
|
65
|
+
const rawPriority = row.priority;
|
|
66
|
+
priority = String(rawPriority) || null;
|
|
67
|
+
}
|
|
68
|
+
let assignee = "";
|
|
69
|
+
if (row.assignee !== null) {
|
|
70
|
+
const rawAssignee = row.assignee;
|
|
71
|
+
assignee = String(rawAssignee);
|
|
72
|
+
}
|
|
73
|
+
if (!assignee && row.created_by !== null) {
|
|
74
|
+
assignee = String(row.created_by);
|
|
75
|
+
}
|
|
76
|
+
if (!assignee) {
|
|
77
|
+
assignee = "unassigned";
|
|
78
|
+
}
|
|
79
|
+
let assigneeRole = null;
|
|
80
|
+
if (row.assignee_role !== null) {
|
|
81
|
+
const rawAssigneeRole = row.assignee_role;
|
|
82
|
+
assigneeRole = String(rawAssigneeRole) || null;
|
|
83
|
+
}
|
|
84
|
+
let claimedBy = null;
|
|
85
|
+
if (row.claimed_by !== null) {
|
|
86
|
+
const rawClaimedBy = row.claimed_by;
|
|
87
|
+
claimedBy = String(rawClaimedBy) || null;
|
|
88
|
+
}
|
|
89
|
+
let claimedAt = null;
|
|
90
|
+
if (row.claimed_at !== null) {
|
|
91
|
+
const rawClaimedAt = row.claimed_at;
|
|
92
|
+
claimedAt = String(rawClaimedAt);
|
|
93
|
+
}
|
|
94
|
+
let answeredBy = null;
|
|
95
|
+
if (row.answered_by !== null) {
|
|
96
|
+
const rawAnsweredBy = row.answered_by;
|
|
97
|
+
answeredBy = String(rawAnsweredBy) || null;
|
|
98
|
+
}
|
|
99
|
+
let answeredAt = null;
|
|
100
|
+
if (row.answered_at !== null) {
|
|
101
|
+
const rawAnsweredAt = row.answered_at;
|
|
102
|
+
answeredAt = String(rawAnsweredAt);
|
|
103
|
+
}
|
|
104
|
+
let answer = null;
|
|
105
|
+
if (row.answer !== null) {
|
|
106
|
+
const rawAnswer = row.answer;
|
|
107
|
+
answer = String(rawAnswer) || null;
|
|
108
|
+
}
|
|
109
|
+
let stageId = null;
|
|
110
|
+
if (row.stage_id !== null) {
|
|
111
|
+
const rawStageId = row.stage_id;
|
|
112
|
+
stageId = String(rawStageId) || null;
|
|
113
|
+
}
|
|
114
|
+
let workStatus = null;
|
|
115
|
+
if (isWorkStatus(row.work_status)) {
|
|
116
|
+
workStatus = row.work_status;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
id: Number(row.id),
|
|
120
|
+
projectId: String(row.project_id),
|
|
121
|
+
projectName: String(row.project_name),
|
|
122
|
+
parentId,
|
|
123
|
+
epicId,
|
|
124
|
+
templateId,
|
|
125
|
+
title: String(row.title),
|
|
126
|
+
description: String(row.description),
|
|
127
|
+
acceptanceCriteria: null,
|
|
128
|
+
priority,
|
|
129
|
+
labels: JSON.parse(String(row.labels_json)),
|
|
130
|
+
assignee,
|
|
131
|
+
assigneeRole,
|
|
132
|
+
assigneeKind: null,
|
|
133
|
+
createdBy: String(row.created_by),
|
|
134
|
+
createdAt: String(row.created_at),
|
|
135
|
+
updatedAt: String(row.updated_at),
|
|
136
|
+
claimedBy,
|
|
137
|
+
claimedAt,
|
|
138
|
+
answeredBy,
|
|
139
|
+
answeredAt,
|
|
140
|
+
answer,
|
|
141
|
+
stageId,
|
|
142
|
+
workStatus,
|
|
143
|
+
comments: JSON.parse(String(row.comments_json)),
|
|
144
|
+
events: JSON.parse(String(row.events_json)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function taskToRow(task) {
|
|
148
|
+
return {
|
|
149
|
+
id: task.id,
|
|
150
|
+
project_id: task.projectId,
|
|
151
|
+
project_name: task.projectName,
|
|
152
|
+
parent_id: task.parentId,
|
|
153
|
+
epic_id: task.epicId,
|
|
154
|
+
template_id: task.templateId,
|
|
155
|
+
title: task.title,
|
|
156
|
+
description: task.description,
|
|
157
|
+
priority: task.priority,
|
|
158
|
+
labels_json: JSON.stringify(task.labels),
|
|
159
|
+
assignee: task.assignee,
|
|
160
|
+
assignee_role: task.assigneeRole,
|
|
161
|
+
created_by: task.createdBy,
|
|
162
|
+
created_at: task.createdAt,
|
|
163
|
+
updated_at: task.updatedAt,
|
|
164
|
+
claimed_by: task.claimedBy,
|
|
165
|
+
claimed_at: task.claimedAt,
|
|
166
|
+
answered_by: task.answeredBy,
|
|
167
|
+
answered_at: task.answeredAt,
|
|
168
|
+
answer: task.answer,
|
|
169
|
+
stage_id: task.stageId,
|
|
170
|
+
work_status: task.workStatus,
|
|
171
|
+
comments_json: JSON.stringify(task.comments),
|
|
172
|
+
events_json: JSON.stringify(task.events),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function getTasksDb() {
|
|
176
|
+
if (db)
|
|
177
|
+
return db;
|
|
178
|
+
const path = resolveDatabasePath();
|
|
179
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
180
|
+
db = new Database(path);
|
|
181
|
+
db.pragma("journal_mode = WAL");
|
|
182
|
+
migrate(db);
|
|
183
|
+
return db;
|
|
184
|
+
}
|
|
185
|
+
export function deleteEpicSubtasks(epicId) {
|
|
186
|
+
const database = getTasksDb();
|
|
187
|
+
database
|
|
188
|
+
.prepare("DELETE FROM tasks WHERE epic_id = ? AND id != ?")
|
|
189
|
+
.run(epicId, epicId);
|
|
190
|
+
}
|
|
191
|
+
export function deleteTaskRows(ids) {
|
|
192
|
+
if (ids.length === 0)
|
|
193
|
+
return;
|
|
194
|
+
const database = getTasksDb();
|
|
195
|
+
const statement = database.prepare("DELETE FROM tasks WHERE id = ?");
|
|
196
|
+
for (const id of ids) {
|
|
197
|
+
statement.run(id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function countActiveTasksOnStage(projectId, stageId) {
|
|
201
|
+
const database = getTasksDb();
|
|
202
|
+
const row = database
|
|
203
|
+
.prepare(`SELECT COUNT(*) AS count FROM tasks
|
|
204
|
+
WHERE project_id = ? AND stage_id = ? AND stage_id != ?`)
|
|
205
|
+
.get(projectId, stageId, DONE_STAGE_ID);
|
|
206
|
+
return row.count;
|
|
207
|
+
}
|
|
208
|
+
export function listTaskRows(filter) {
|
|
209
|
+
const database = getTasksDb();
|
|
210
|
+
if (filter.id > 0) {
|
|
211
|
+
const rows = database.prepare("SELECT * FROM tasks WHERE id = ?").all(filter.id);
|
|
212
|
+
return rows.map((row) => rowToTask(row));
|
|
213
|
+
}
|
|
214
|
+
const rows = database
|
|
215
|
+
.prepare("SELECT * FROM tasks ORDER BY updated_at DESC, id DESC")
|
|
216
|
+
.all();
|
|
217
|
+
return sortTasks(rows.map((row) => rowToTask(row)));
|
|
218
|
+
}
|
|
219
|
+
export function allocateTaskRowId() {
|
|
220
|
+
const database = getTasksDb();
|
|
221
|
+
const row = database.prepare("SELECT MAX(id) AS maxId FROM tasks").get();
|
|
222
|
+
let base = 0;
|
|
223
|
+
if (row.maxId !== null) {
|
|
224
|
+
base = row.maxId;
|
|
225
|
+
}
|
|
226
|
+
return base + 1;
|
|
227
|
+
}
|
|
228
|
+
export function upsertTaskRow(task) {
|
|
229
|
+
const database = getTasksDb();
|
|
230
|
+
const row = taskToRow(task);
|
|
231
|
+
database
|
|
232
|
+
.prepare(`
|
|
233
|
+
INSERT INTO tasks (
|
|
234
|
+
id, project_id, project_name, parent_id, epic_id, template_id, title, description, priority, labels_json,
|
|
235
|
+
assignee, assignee_role, created_by, created_at, updated_at, claimed_by,
|
|
236
|
+
claimed_at, answered_by, answered_at, answer, stage_id, work_status, comments_json, events_json
|
|
237
|
+
) VALUES (
|
|
238
|
+
@id, @project_id, @project_name, @parent_id, @epic_id, @template_id, @title, @description, @priority, @labels_json,
|
|
239
|
+
@assignee, @assignee_role, @created_by, @created_at, @updated_at, @claimed_by,
|
|
240
|
+
@claimed_at, @answered_by, @answered_at, @answer, @stage_id, @work_status, @comments_json, @events_json
|
|
241
|
+
)
|
|
242
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
243
|
+
project_id = excluded.project_id,
|
|
244
|
+
project_name = excluded.project_name,
|
|
245
|
+
parent_id = excluded.parent_id,
|
|
246
|
+
epic_id = excluded.epic_id,
|
|
247
|
+
template_id = excluded.template_id,
|
|
248
|
+
title = excluded.title,
|
|
249
|
+
description = excluded.description,
|
|
250
|
+
priority = excluded.priority,
|
|
251
|
+
labels_json = excluded.labels_json,
|
|
252
|
+
assignee = excluded.assignee,
|
|
253
|
+
assignee_role = excluded.assignee_role,
|
|
254
|
+
created_by = excluded.created_by,
|
|
255
|
+
created_at = excluded.created_at,
|
|
256
|
+
updated_at = excluded.updated_at,
|
|
257
|
+
claimed_by = excluded.claimed_by,
|
|
258
|
+
claimed_at = excluded.claimed_at,
|
|
259
|
+
answered_by = excluded.answered_by,
|
|
260
|
+
answered_at = excluded.answered_at,
|
|
261
|
+
answer = excluded.answer,
|
|
262
|
+
stage_id = excluded.stage_id,
|
|
263
|
+
work_status = excluded.work_status,
|
|
264
|
+
comments_json = excluded.comments_json,
|
|
265
|
+
events_json = excluded.events_json
|
|
266
|
+
`)
|
|
267
|
+
.run(row);
|
|
268
|
+
}
|
|
269
|
+
export function mutateTaskRow(id, mutator) {
|
|
270
|
+
const database = getTasksDb();
|
|
271
|
+
return database.transaction(() => {
|
|
272
|
+
const tasks = listTaskRows({ id });
|
|
273
|
+
if (tasks.length === 0)
|
|
274
|
+
return null;
|
|
275
|
+
const task = tasks[0];
|
|
276
|
+
if (!task)
|
|
277
|
+
return null;
|
|
278
|
+
mutator(task);
|
|
279
|
+
upsertTaskRow(task);
|
|
280
|
+
return task;
|
|
281
|
+
})();
|
|
282
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import { getProjectsDb } from "./projects-db.js";
|
|
4
|
+
export function hasAnyUser() {
|
|
5
|
+
const db = getProjectsDb();
|
|
6
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM users").get();
|
|
7
|
+
return row.count > 0;
|
|
8
|
+
}
|
|
9
|
+
function generateToken() {
|
|
10
|
+
return randomBytes(32).toString("hex");
|
|
11
|
+
}
|
|
12
|
+
export function listUserRows(filter) {
|
|
13
|
+
const db = getProjectsDb();
|
|
14
|
+
const id = filter.id;
|
|
15
|
+
const email = filter.email;
|
|
16
|
+
const token = filter.token;
|
|
17
|
+
if (id !== "") {
|
|
18
|
+
return db.prepare("SELECT * FROM users WHERE id = ?").all(id);
|
|
19
|
+
}
|
|
20
|
+
if (email !== "") {
|
|
21
|
+
return db.prepare("SELECT * FROM users WHERE email = ?").all(email);
|
|
22
|
+
}
|
|
23
|
+
if (token !== "") {
|
|
24
|
+
return db.prepare("SELECT * FROM users WHERE token = ?").all(token);
|
|
25
|
+
}
|
|
26
|
+
return db
|
|
27
|
+
.prepare("SELECT * FROM users ORDER BY is_system_admin DESC, created_at ASC")
|
|
28
|
+
.all();
|
|
29
|
+
}
|
|
30
|
+
export function createUser(params) {
|
|
31
|
+
const db = getProjectsDb();
|
|
32
|
+
const id = randomBytes(8).toString("hex");
|
|
33
|
+
const token = generateToken();
|
|
34
|
+
const passwordHash = bcrypt.hashSync(params.password, 10);
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
let isAdminFlag = 0;
|
|
37
|
+
if (params.isSystemAdmin) {
|
|
38
|
+
isAdminFlag = 1;
|
|
39
|
+
}
|
|
40
|
+
let mustChangePasswordFlag = 0;
|
|
41
|
+
if (params.mustChangePassword) {
|
|
42
|
+
mustChangePasswordFlag = 1;
|
|
43
|
+
}
|
|
44
|
+
db.prepare(`
|
|
45
|
+
INSERT INTO users (id, name, email, password_hash, role, is_system_admin, token, must_change_password, created_at, updated_at)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
47
|
+
`).run(id, params.name, params.email, passwordHash, params.role, isAdminFlag, token, mustChangePasswordFlag, now, now);
|
|
48
|
+
const rows = listUserRows({ id, email: "", token: "" });
|
|
49
|
+
const created = rows[0];
|
|
50
|
+
if (!created) {
|
|
51
|
+
throw new Error("Failed to retrieve created user");
|
|
52
|
+
}
|
|
53
|
+
return rowToPublic(created);
|
|
54
|
+
}
|
|
55
|
+
export function listPublicUsers() {
|
|
56
|
+
return listUserRows({ id: "", email: "", token: "" }).map(rowToPublic);
|
|
57
|
+
}
|
|
58
|
+
export function deleteUser(id) {
|
|
59
|
+
const rows = listUserRows({ id, email: "", token: "" });
|
|
60
|
+
if (rows.length === 0)
|
|
61
|
+
return { deleted: false, reason: "User not found" };
|
|
62
|
+
const user = rows[0];
|
|
63
|
+
if (!user)
|
|
64
|
+
return { deleted: false, reason: "User not found" };
|
|
65
|
+
if (user.is_system_admin)
|
|
66
|
+
return { deleted: false, reason: "Cannot delete the system admin" };
|
|
67
|
+
getProjectsDb().prepare("DELETE FROM users WHERE id = ?").run(id);
|
|
68
|
+
return { deleted: true, reason: "" };
|
|
69
|
+
}
|
|
70
|
+
export function updateUser(id, input) {
|
|
71
|
+
const rows = listUserRows({ id, email: "", token: "" });
|
|
72
|
+
if (rows.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
getProjectsDb()
|
|
75
|
+
.prepare(`UPDATE users SET name = ?, role = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
76
|
+
.run(input.name, input.role, id);
|
|
77
|
+
const updated = listUserRows({ id, email: "", token: "" });
|
|
78
|
+
if (updated.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const updatedRow = updated[0];
|
|
82
|
+
if (!updatedRow) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return rowToPublic(updatedRow);
|
|
86
|
+
}
|
|
87
|
+
export function verifyPassword(user, password) {
|
|
88
|
+
return bcrypt.compareSync(password, user.password_hash);
|
|
89
|
+
}
|
|
90
|
+
export function updateUserPassword(userId, newPassword) {
|
|
91
|
+
const passwordHash = bcrypt.hashSync(newPassword, 10);
|
|
92
|
+
getProjectsDb()
|
|
93
|
+
.prepare(`UPDATE users SET password_hash = ?, must_change_password = 0, updated_at = datetime('now') WHERE id = ?`)
|
|
94
|
+
.run(passwordHash, userId);
|
|
95
|
+
}
|
|
96
|
+
export function userMustChangePassword(user) {
|
|
97
|
+
return user.must_change_password === 1;
|
|
98
|
+
}
|
|
99
|
+
export function readUserToken(id) {
|
|
100
|
+
const rows = listUserRows({ id, email: "", token: "" });
|
|
101
|
+
if (rows.length === 0)
|
|
102
|
+
return "";
|
|
103
|
+
const tokenRow = rows[0];
|
|
104
|
+
if (!tokenRow)
|
|
105
|
+
return "";
|
|
106
|
+
return tokenRow.token;
|
|
107
|
+
}
|
|
108
|
+
function rowToPublic(row) {
|
|
109
|
+
return {
|
|
110
|
+
id: row.id,
|
|
111
|
+
name: row.name,
|
|
112
|
+
email: row.email,
|
|
113
|
+
role: row.role,
|
|
114
|
+
isSystemAdmin: row.is_system_admin === 1,
|
|
115
|
+
createdAt: row.created_at,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { getProjectsDb } from "./projects-db.js";
|
|
2
|
+
export function migrateWorkflowTables() {
|
|
3
|
+
const db = getProjectsDb();
|
|
4
|
+
db.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS workflow_stages (
|
|
6
|
+
id TEXT NOT NULL,
|
|
7
|
+
project_id TEXT NOT NULL,
|
|
8
|
+
title TEXT NOT NULL,
|
|
9
|
+
description TEXT NOT NULL DEFAULT '',
|
|
10
|
+
purpose TEXT NOT NULL DEFAULT '',
|
|
11
|
+
rules_json TEXT NOT NULL DEFAULT '[]',
|
|
12
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
auto_assign INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
15
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
PRIMARY KEY (project_id, id)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS project_members (
|
|
20
|
+
id TEXT NOT NULL,
|
|
21
|
+
project_id TEXT NOT NULL,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
available INTEGER NOT NULL DEFAULT 1,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
PRIMARY KEY (id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_project_members_project
|
|
30
|
+
ON project_members(project_id);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS project_workflow_settings (
|
|
33
|
+
project_id TEXT NOT NULL PRIMARY KEY,
|
|
34
|
+
roles_json TEXT NOT NULL DEFAULT '[]',
|
|
35
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
36
|
+
);
|
|
37
|
+
`);
|
|
38
|
+
const columns = db.prepare("PRAGMA table_info(workflow_stages)").all();
|
|
39
|
+
const names = new Set(columns.map((column) => column.name));
|
|
40
|
+
if (!names.has("layout_x")) {
|
|
41
|
+
db.exec("ALTER TABLE workflow_stages ADD COLUMN layout_x REAL");
|
|
42
|
+
}
|
|
43
|
+
if (!names.has("layout_y")) {
|
|
44
|
+
db.exec("ALTER TABLE workflow_stages ADD COLUMN layout_y REAL");
|
|
45
|
+
}
|
|
46
|
+
if (!names.has("spawn_task_count")) {
|
|
47
|
+
db.exec("ALTER TABLE workflow_stages ADD COLUMN spawn_task_count INTEGER NOT NULL DEFAULT 0");
|
|
48
|
+
}
|
|
49
|
+
if (!names.has("task_templates_json")) {
|
|
50
|
+
db.exec("ALTER TABLE workflow_stages ADD COLUMN task_templates_json TEXT NOT NULL DEFAULT '[]'");
|
|
51
|
+
}
|
|
52
|
+
if (!names.has("roles_json")) {
|
|
53
|
+
db.exec("ALTER TABLE workflow_stages ADD COLUMN roles_json TEXT NOT NULL DEFAULT '[]'");
|
|
54
|
+
}
|
|
55
|
+
if (!names.has("auto_assign_role")) {
|
|
56
|
+
db.exec("ALTER TABLE workflow_stages ADD COLUMN auto_assign_role TEXT NOT NULL DEFAULT ''");
|
|
57
|
+
}
|
|
58
|
+
const memberColumns = db.prepare("PRAGMA table_info(project_members)").all();
|
|
59
|
+
const memberNames = new Set(memberColumns.map((column) => column.name));
|
|
60
|
+
if (!memberNames.has("stage_roles_json")) {
|
|
61
|
+
db.exec("ALTER TABLE project_members ADD COLUMN stage_roles_json TEXT NOT NULL DEFAULT '{}'");
|
|
62
|
+
}
|
|
63
|
+
if (!memberNames.has("role")) {
|
|
64
|
+
db.exec("ALTER TABLE project_members ADD COLUMN role TEXT NOT NULL DEFAULT ''");
|
|
65
|
+
}
|
|
66
|
+
if (!memberNames.has("actor_kind")) {
|
|
67
|
+
db.exec("ALTER TABLE project_members ADD COLUMN actor_kind TEXT NOT NULL DEFAULT ''");
|
|
68
|
+
}
|
|
69
|
+
db.exec("UPDATE project_members SET actor_kind = '' WHERE actor_kind = 'human'");
|
|
70
|
+
db.exec("DELETE FROM project_members WHERE role IS NULL OR trim(role) = ''");
|
|
71
|
+
db.exec("DROP TABLE IF EXISTS project_decisions");
|
|
72
|
+
}
|
|
73
|
+
export function countWorkflowStages(projectId) {
|
|
74
|
+
migrateWorkflowTables();
|
|
75
|
+
const row = getProjectsDb()
|
|
76
|
+
.prepare("SELECT COUNT(*) AS count FROM workflow_stages WHERE project_id = ?")
|
|
77
|
+
.get(projectId);
|
|
78
|
+
return row.count;
|
|
79
|
+
}
|
|
80
|
+
export function listWorkflowStageRows(filter) {
|
|
81
|
+
migrateWorkflowTables();
|
|
82
|
+
const projectId = filter.projectId;
|
|
83
|
+
const stageId = filter.stageId;
|
|
84
|
+
if (projectId !== "" && stageId !== "") {
|
|
85
|
+
return getProjectsDb()
|
|
86
|
+
.prepare(`SELECT id, project_id, title, description, purpose, rules_json, position, auto_assign, auto_assign_role, layout_x, layout_y, spawn_task_count, task_templates_json, roles_json
|
|
87
|
+
FROM workflow_stages WHERE project_id = ? AND id = ?`)
|
|
88
|
+
.all(projectId, stageId);
|
|
89
|
+
}
|
|
90
|
+
if (projectId !== "") {
|
|
91
|
+
return getProjectsDb()
|
|
92
|
+
.prepare(`SELECT id, project_id, title, description, purpose, rules_json, position, auto_assign, auto_assign_role, layout_x, layout_y, spawn_task_count, task_templates_json, roles_json
|
|
93
|
+
FROM workflow_stages WHERE project_id = ? ORDER BY position ASC, title COLLATE NOCASE ASC`)
|
|
94
|
+
.all(projectId);
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
export function deleteWorkflowStagesForProject(projectId) {
|
|
99
|
+
migrateWorkflowTables();
|
|
100
|
+
getProjectsDb()
|
|
101
|
+
.prepare("DELETE FROM workflow_stages WHERE project_id = ?")
|
|
102
|
+
.run(projectId);
|
|
103
|
+
}
|
|
104
|
+
export function insertWorkflowStageRow(row) {
|
|
105
|
+
migrateWorkflowTables();
|
|
106
|
+
const autoAssignRole = row.autoAssignRole;
|
|
107
|
+
let autoAssignFlag = 0;
|
|
108
|
+
if (autoAssignRole) {
|
|
109
|
+
autoAssignFlag = 1;
|
|
110
|
+
}
|
|
111
|
+
getProjectsDb()
|
|
112
|
+
.prepare(`INSERT INTO workflow_stages
|
|
113
|
+
(id, project_id, title, description, purpose, rules_json, position, auto_assign, auto_assign_role, layout_x, layout_y, spawn_task_count, task_templates_json, roles_json, updated_at)
|
|
114
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '[]', datetime('now'))`)
|
|
115
|
+
.run(row.id, row.projectId, row.title, row.description, row.purpose, row.rulesJson, row.position, autoAssignFlag, autoAssignRole, row.layoutX, row.layoutY, row.spawnTaskCount, row.taskTemplatesJson);
|
|
116
|
+
}
|
|
117
|
+
export function listProjectMemberRows(filter) {
|
|
118
|
+
migrateWorkflowTables();
|
|
119
|
+
const projectId = filter.projectId;
|
|
120
|
+
const id = filter.id;
|
|
121
|
+
if (id !== "") {
|
|
122
|
+
return getProjectsDb()
|
|
123
|
+
.prepare(`SELECT id, project_id, name, available, stage_roles_json, role FROM project_members WHERE id = ?`)
|
|
124
|
+
.all(id);
|
|
125
|
+
}
|
|
126
|
+
if (projectId !== "") {
|
|
127
|
+
return getProjectsDb()
|
|
128
|
+
.prepare(`SELECT id, project_id, name, available, stage_roles_json, role FROM project_members WHERE project_id = ? ORDER BY name COLLATE NOCASE ASC`)
|
|
129
|
+
.all(projectId);
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
export function insertProjectMemberRow(row) {
|
|
134
|
+
migrateWorkflowTables();
|
|
135
|
+
getProjectsDb()
|
|
136
|
+
.prepare(`INSERT INTO project_members (id, project_id, name, available, stage_roles_json, role, updated_at)
|
|
137
|
+
VALUES (?, ?, ?, 1, '{}', ?, datetime('now'))`)
|
|
138
|
+
.run(row.id, row.projectId, row.name, row.role);
|
|
139
|
+
}
|
|
140
|
+
export function updateProjectMemberRow(id, patch) {
|
|
141
|
+
migrateWorkflowTables();
|
|
142
|
+
const existingRows = listProjectMemberRows({ projectId: "", id });
|
|
143
|
+
if (existingRows.length === 0)
|
|
144
|
+
return false;
|
|
145
|
+
const existing = existingRows[0];
|
|
146
|
+
if (!existing)
|
|
147
|
+
return false;
|
|
148
|
+
let name = existing.name;
|
|
149
|
+
if (patch.name !== null) {
|
|
150
|
+
name = patch.name;
|
|
151
|
+
}
|
|
152
|
+
let role = existing.role;
|
|
153
|
+
if (patch.role !== null) {
|
|
154
|
+
role = patch.role;
|
|
155
|
+
}
|
|
156
|
+
const result = getProjectsDb()
|
|
157
|
+
.prepare(`UPDATE project_members SET name = ?, role = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
158
|
+
.run(name, role, id);
|
|
159
|
+
return result.changes > 0;
|
|
160
|
+
}
|
|
161
|
+
export function listProjectWorkflowSettingsRows(filter) {
|
|
162
|
+
migrateWorkflowTables();
|
|
163
|
+
const projectId = filter.projectId;
|
|
164
|
+
if (projectId === "")
|
|
165
|
+
return [];
|
|
166
|
+
return getProjectsDb()
|
|
167
|
+
.prepare(`SELECT project_id, roles_json FROM project_workflow_settings WHERE project_id = ?`)
|
|
168
|
+
.all(projectId);
|
|
169
|
+
}
|
|
170
|
+
export function upsertProjectWorkflowSettingsRow(projectId, rolesJson) {
|
|
171
|
+
migrateWorkflowTables();
|
|
172
|
+
getProjectsDb()
|
|
173
|
+
.prepare(`INSERT INTO project_workflow_settings (project_id, roles_json, updated_at)
|
|
174
|
+
VALUES (?, ?, datetime('now'))
|
|
175
|
+
ON CONFLICT(project_id) DO UPDATE SET
|
|
176
|
+
roles_json = excluded.roles_json,
|
|
177
|
+
updated_at = datetime('now')`)
|
|
178
|
+
.run(projectId, rolesJson);
|
|
179
|
+
}
|
|
180
|
+
export function deleteProjectMemberRow(id) {
|
|
181
|
+
migrateWorkflowTables();
|
|
182
|
+
const result = getProjectsDb()
|
|
183
|
+
.prepare("DELETE FROM project_members WHERE id = ?")
|
|
184
|
+
.run(id);
|
|
185
|
+
return result.changes > 0;
|
|
186
|
+
}
|