@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 umudik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# task-bridge
|
|
2
|
+
|
|
3
|
+
Epic ve task yönetimi için tek backend, web paneli ve Android uygulaması.
|
|
4
|
+
|
|
5
|
+
Projeler workflow stage’leri üzerinden ilerler; mobil veya web’den task açılır, yorumlar inbox’a düşer, worker kuyruğu otomasyon/agent tarafından tüketilir.
|
|
6
|
+
|
|
7
|
+
## Repo
|
|
8
|
+
|
|
9
|
+
| Dizin | Ne |
|
|
10
|
+
|-------|-----|
|
|
11
|
+
| `apps/backend` | Fastify API, SQLite, worker queue |
|
|
12
|
+
| `apps/web` | React dashboard (Vite) |
|
|
13
|
+
| `apps/mobile` | Android (Kotlin + Compose) |
|
|
14
|
+
| `apps/mcp` | MCP server for AI agents |
|
|
15
|
+
|
|
16
|
+
## Geliştirme
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
copy .env.example .env
|
|
20
|
+
npm install
|
|
21
|
+
npm run dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Servis | URL |
|
|
25
|
+
|--------|-----|
|
|
26
|
+
| Web UI | http://localhost:5173/app/login |
|
|
27
|
+
| API | http://localhost:3000 |
|
|
28
|
+
|
|
29
|
+
İlk kurulumda web’den `/setup` ile admin oluştur, sonra `/app/login`.
|
|
30
|
+
|
|
31
|
+
## npx ile çalıştır
|
|
32
|
+
|
|
33
|
+
Build edilmiş API + web tek paket olarak npm'de (`@umudik/task-bridge`):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @umudik/task-bridge
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Opsiyon | Açıklama |
|
|
40
|
+
|---------|----------|
|
|
41
|
+
| `-p, --port <port>` | Dinlenecek port (varsayılan 3000 / `$PORT`) |
|
|
42
|
+
| `-d, --data <dir>` | SQLite veritabanı dizini (varsayılan `./task-bridge-data`) |
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @umudik/task-bridge --port 8080 --data ./tb-data
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Aç: **http://localhost:3000/app/login** — ilk açılışta `/app/setup` ile admin oluştur. Veritabanı çalıştırdığın klasörde `task-bridge-data/bridge.db` olarak kalır.
|
|
49
|
+
|
|
50
|
+
## API (agent / otomasyon)
|
|
51
|
+
|
|
52
|
+
OpenAPI 3.1 spec backend’de yaşar ve çalışan sunucudan JSON olarak okunur:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
GET /api/docs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Kaynak: `apps/backend/src/openapi.ts`
|
|
59
|
+
|
|
60
|
+
## MCP (AI agents)
|
|
61
|
+
|
|
62
|
+
Cursor / Claude Desktop → [apps/mcp/README.md](apps/mcp/README.md)
|
|
63
|
+
|
|
64
|
+
Stdio MCP server; Task Bridge API’yi tool olarak sunar. `.cursor/mcp.json` örneği repoda.
|
|
65
|
+
|
|
66
|
+
## Deploy & mobil
|
|
67
|
+
|
|
68
|
+
Sunucu (API + web) → `npx @umudik/task-bridge` (yukarıdaki npx bölümü)
|
|
69
|
+
|
|
70
|
+
Web UI detayları → [apps/web/README.md](apps/web/README.md)
|
|
71
|
+
|
|
72
|
+
Android → [apps/mobile/README.md](apps/mobile/README.md)
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function getEnv(key) {
|
|
2
|
+
const keys = Object.keys(process.env);
|
|
3
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
4
|
+
const envKey = keys[i];
|
|
5
|
+
if (envKey !== key) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
const value = process.env[envKey];
|
|
9
|
+
if (value === null) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
let configPort = 3000;
|
|
17
|
+
if ("PORT" in process.env) {
|
|
18
|
+
const rawPort = process.env["PORT"];
|
|
19
|
+
if (rawPort !== null) {
|
|
20
|
+
configPort = Number(rawPort);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const config = {
|
|
24
|
+
port: configPort,
|
|
25
|
+
databasePath: getEnv("DATABASE_PATH") || getEnv("BRIDGE_DB_PATH") || "",
|
|
26
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getProjectsDb } from "./projects-db.js";
|
|
2
|
+
import { parseWorkflowStateData, serializeWorkflowStateData, } from "../domain/workflow-state.js";
|
|
3
|
+
export function migrateEpicWorkflowTables() {
|
|
4
|
+
const db = getProjectsDb();
|
|
5
|
+
db.exec(`
|
|
6
|
+
CREATE TABLE IF NOT EXISTS epics (
|
|
7
|
+
id INTEGER PRIMARY KEY,
|
|
8
|
+
project_id TEXT NOT NULL,
|
|
9
|
+
title TEXT NOT NULL,
|
|
10
|
+
description TEXT NOT NULL DEFAULT '',
|
|
11
|
+
stage_id TEXT,
|
|
12
|
+
created_by TEXT NOT NULL,
|
|
13
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_epics_project_id ON epics(project_id);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS workflow_state (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
epic_id INTEGER NOT NULL UNIQUE,
|
|
22
|
+
state_json TEXT NOT NULL DEFAULT '{}',
|
|
23
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
FOREIGN KEY (epic_id) REFERENCES epics(id)
|
|
25
|
+
);
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
export function listEpicRows(filter) {
|
|
29
|
+
migrateEpicWorkflowTables();
|
|
30
|
+
const db = getProjectsDb();
|
|
31
|
+
const id = filter.id;
|
|
32
|
+
const projectId = filter.projectId;
|
|
33
|
+
if (id > 0) {
|
|
34
|
+
return db.prepare("SELECT * FROM epics WHERE id = ?").all(id);
|
|
35
|
+
}
|
|
36
|
+
if (projectId !== "") {
|
|
37
|
+
return db
|
|
38
|
+
.prepare("SELECT * FROM epics WHERE project_id = ? ORDER BY updated_at DESC")
|
|
39
|
+
.all(projectId);
|
|
40
|
+
}
|
|
41
|
+
return db.prepare("SELECT * FROM epics ORDER BY updated_at DESC").all();
|
|
42
|
+
}
|
|
43
|
+
export function insertEpicRow(input) {
|
|
44
|
+
migrateEpicWorkflowTables();
|
|
45
|
+
const db = getProjectsDb();
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
db.prepare(`INSERT INTO epics (id, project_id, title, description, stage_id, created_by, created_at, updated_at)
|
|
48
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.id, input.projectId, input.title, input.description, input.stageId, input.createdBy || "system", now, now);
|
|
49
|
+
const rows = listEpicRows({ id: input.id, projectId: "" });
|
|
50
|
+
const row = rows[0];
|
|
51
|
+
if (!row)
|
|
52
|
+
throw new Error("Failed to insert epic");
|
|
53
|
+
return row;
|
|
54
|
+
}
|
|
55
|
+
export function updateEpicStageRow(epicId, stageId) {
|
|
56
|
+
migrateEpicWorkflowTables();
|
|
57
|
+
getProjectsDb()
|
|
58
|
+
.prepare(`UPDATE epics SET stage_id = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
59
|
+
.run(stageId, epicId);
|
|
60
|
+
}
|
|
61
|
+
export function updateEpicSpecRow(epicId, input) {
|
|
62
|
+
migrateEpicWorkflowTables();
|
|
63
|
+
const rows = listEpicRows({ id: epicId, projectId: "" });
|
|
64
|
+
if (rows.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const row = rows[0];
|
|
67
|
+
if (!row)
|
|
68
|
+
return;
|
|
69
|
+
let title = row.title;
|
|
70
|
+
if (input.title !== null)
|
|
71
|
+
title = input.title;
|
|
72
|
+
let description = row.description;
|
|
73
|
+
if (input.description !== null)
|
|
74
|
+
description = input.description;
|
|
75
|
+
getProjectsDb()
|
|
76
|
+
.prepare(`UPDATE epics SET title = ?, description = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
77
|
+
.run(title, description, epicId);
|
|
78
|
+
}
|
|
79
|
+
export function listWorkflowStateRows(filter) {
|
|
80
|
+
migrateEpicWorkflowTables();
|
|
81
|
+
const db = getProjectsDb();
|
|
82
|
+
if (filter.epicId > 0) {
|
|
83
|
+
return db
|
|
84
|
+
.prepare("SELECT * FROM workflow_state WHERE epic_id = ?")
|
|
85
|
+
.all(filter.epicId);
|
|
86
|
+
}
|
|
87
|
+
return db.prepare("SELECT * FROM workflow_state ORDER BY epic_id ASC").all();
|
|
88
|
+
}
|
|
89
|
+
export function getEpicWorkflowStateData(epicId) {
|
|
90
|
+
const rows = listWorkflowStateRows({ epicId });
|
|
91
|
+
const row = rows[0];
|
|
92
|
+
if (!row)
|
|
93
|
+
return null;
|
|
94
|
+
return parseWorkflowStateData(row.state_json);
|
|
95
|
+
}
|
|
96
|
+
export function insertWorkflowStateRow(epicId, data) {
|
|
97
|
+
migrateEpicWorkflowTables();
|
|
98
|
+
const db = getProjectsDb();
|
|
99
|
+
const payload = serializeWorkflowStateData(data);
|
|
100
|
+
db.prepare(`INSERT INTO workflow_state (epic_id, state_json, updated_at) VALUES (?, ?, datetime('now'))`).run(epicId, payload);
|
|
101
|
+
const rows = listWorkflowStateRows({ epicId });
|
|
102
|
+
const row = rows[0];
|
|
103
|
+
if (!row)
|
|
104
|
+
throw new Error("Failed to insert workflow state");
|
|
105
|
+
return row;
|
|
106
|
+
}
|
|
107
|
+
export function saveEpicWorkflowStateData(epicId, data) {
|
|
108
|
+
migrateEpicWorkflowTables();
|
|
109
|
+
const payload = serializeWorkflowStateData(data);
|
|
110
|
+
const db = getProjectsDb();
|
|
111
|
+
const existing = listWorkflowStateRows({ epicId });
|
|
112
|
+
if (existing.length === 0) {
|
|
113
|
+
insertWorkflowStateRow(epicId, data);
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
db.prepare(`UPDATE workflow_state SET state_json = ?, updated_at = datetime('now') WHERE epic_id = ?`).run(payload, epicId);
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
export function mutateEpicWorkflowState(epicId, mutator) {
|
|
120
|
+
const data = getEpicWorkflowStateData(epicId);
|
|
121
|
+
if (!data)
|
|
122
|
+
return null;
|
|
123
|
+
mutator(data);
|
|
124
|
+
return saveEpicWorkflowStateData(epicId, data);
|
|
125
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getProjectsDb } from "./projects-db.js";
|
|
2
|
+
function migrateLibraryTables() {
|
|
3
|
+
const db = getProjectsDb();
|
|
4
|
+
db.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS libraries (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
title TEXT NOT NULL,
|
|
8
|
+
description TEXT NOT NULL DEFAULT '',
|
|
9
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10
|
+
);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS library_documents (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
library_id TEXT NOT NULL,
|
|
14
|
+
title TEXT NOT NULL,
|
|
15
|
+
description TEXT NOT NULL DEFAULT '',
|
|
16
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
FOREIGN KEY (library_id) REFERENCES libraries(id) ON DELETE CASCADE
|
|
18
|
+
);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_library_documents_library_id ON library_documents(library_id);
|
|
20
|
+
CREATE TABLE IF NOT EXISTS library_document_links (
|
|
21
|
+
document_id TEXT NOT NULL,
|
|
22
|
+
task_id INTEGER NOT NULL,
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
PRIMARY KEY (document_id, task_id),
|
|
25
|
+
FOREIGN KEY (document_id) REFERENCES library_documents(id) ON DELETE CASCADE
|
|
26
|
+
);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_library_document_links_task_id ON library_document_links(task_id);
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
function ensureMigrated() {
|
|
31
|
+
migrateLibraryTables();
|
|
32
|
+
}
|
|
33
|
+
export function listLibraryRows(filter) {
|
|
34
|
+
ensureMigrated();
|
|
35
|
+
const id = filter.id;
|
|
36
|
+
if (id !== "") {
|
|
37
|
+
return getProjectsDb()
|
|
38
|
+
.prepare(`SELECT id, title, description, updated_at FROM libraries WHERE id = ?`)
|
|
39
|
+
.all(id);
|
|
40
|
+
}
|
|
41
|
+
return getProjectsDb()
|
|
42
|
+
.prepare(`SELECT id, title, description, updated_at FROM libraries ORDER BY title COLLATE NOCASE ASC`)
|
|
43
|
+
.all();
|
|
44
|
+
}
|
|
45
|
+
export function insertLibraryRow(row) {
|
|
46
|
+
ensureMigrated();
|
|
47
|
+
getProjectsDb()
|
|
48
|
+
.prepare(`INSERT INTO libraries (id, title, description, updated_at) VALUES (?, ?, ?, datetime('now'))`)
|
|
49
|
+
.run(row.id, row.title, row.description);
|
|
50
|
+
}
|
|
51
|
+
export function updateLibraryRow(id, patch) {
|
|
52
|
+
ensureMigrated();
|
|
53
|
+
getProjectsDb()
|
|
54
|
+
.prepare(`UPDATE libraries SET title = ?, description = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
55
|
+
.run(patch.title, patch.description, id);
|
|
56
|
+
}
|
|
57
|
+
export function deleteLibraryRow(id) {
|
|
58
|
+
ensureMigrated();
|
|
59
|
+
const db = getProjectsDb();
|
|
60
|
+
db.prepare(`DELETE FROM library_document_links WHERE document_id IN (SELECT id FROM library_documents WHERE library_id = ?)`).run(id);
|
|
61
|
+
db.prepare(`DELETE FROM library_documents WHERE library_id = ?`).run(id);
|
|
62
|
+
db.prepare(`DELETE FROM libraries WHERE id = ?`).run(id);
|
|
63
|
+
}
|
|
64
|
+
export function listLibraryDocumentRows(filter) {
|
|
65
|
+
ensureMigrated();
|
|
66
|
+
const documentId = filter.documentId;
|
|
67
|
+
const libraryId = filter.libraryId;
|
|
68
|
+
if (documentId !== "") {
|
|
69
|
+
return getProjectsDb()
|
|
70
|
+
.prepare(`SELECT id, library_id, title, description, updated_at FROM library_documents WHERE id = ?`)
|
|
71
|
+
.all(documentId);
|
|
72
|
+
}
|
|
73
|
+
if (libraryId !== "") {
|
|
74
|
+
return getProjectsDb()
|
|
75
|
+
.prepare(`SELECT id, library_id, title, description, updated_at FROM library_documents WHERE library_id = ? ORDER BY title COLLATE NOCASE ASC`)
|
|
76
|
+
.all(libraryId);
|
|
77
|
+
}
|
|
78
|
+
return getProjectsDb()
|
|
79
|
+
.prepare(`SELECT id, library_id, title, description, updated_at FROM library_documents ORDER BY title COLLATE NOCASE ASC`)
|
|
80
|
+
.all();
|
|
81
|
+
}
|
|
82
|
+
export function insertLibraryDocumentRow(row) {
|
|
83
|
+
ensureMigrated();
|
|
84
|
+
getProjectsDb()
|
|
85
|
+
.prepare(`INSERT INTO library_documents (id, library_id, title, description, updated_at) VALUES (?, ?, ?, ?, datetime('now'))`)
|
|
86
|
+
.run(row.id, row.libraryId, row.title, row.description);
|
|
87
|
+
}
|
|
88
|
+
export function updateLibraryDocumentRow(id, patch) {
|
|
89
|
+
ensureMigrated();
|
|
90
|
+
getProjectsDb()
|
|
91
|
+
.prepare(`UPDATE library_documents SET title = ?, description = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
92
|
+
.run(patch.title, patch.description, id);
|
|
93
|
+
}
|
|
94
|
+
export function deleteLibraryDocumentRow(id) {
|
|
95
|
+
ensureMigrated();
|
|
96
|
+
const db = getProjectsDb();
|
|
97
|
+
db.prepare(`DELETE FROM library_document_links WHERE document_id = ?`).run(id);
|
|
98
|
+
db.prepare(`DELETE FROM library_documents WHERE id = ?`).run(id);
|
|
99
|
+
}
|
|
100
|
+
export function insertLibraryDocumentLink(documentId, taskId) {
|
|
101
|
+
ensureMigrated();
|
|
102
|
+
getProjectsDb()
|
|
103
|
+
.prepare(`INSERT OR IGNORE INTO library_document_links (document_id, task_id, created_at) VALUES (?, ?, datetime('now'))`)
|
|
104
|
+
.run(documentId, taskId);
|
|
105
|
+
}
|
|
106
|
+
export function deleteLibraryDocumentLink(documentId, taskId) {
|
|
107
|
+
ensureMigrated();
|
|
108
|
+
getProjectsDb()
|
|
109
|
+
.prepare(`DELETE FROM library_document_links WHERE document_id = ? AND task_id = ?`)
|
|
110
|
+
.run(documentId, taskId);
|
|
111
|
+
}
|
|
112
|
+
export function listLibraryDocumentLinkRowsForTask(taskId) {
|
|
113
|
+
ensureMigrated();
|
|
114
|
+
return getProjectsDb()
|
|
115
|
+
.prepare(`SELECT document_id, task_id, created_at FROM library_document_links WHERE task_id = ? ORDER BY created_at ASC`)
|
|
116
|
+
.all(taskId);
|
|
117
|
+
}
|
|
118
|
+
export function listLibraryDocumentLinkRowsForDocument(documentId) {
|
|
119
|
+
ensureMigrated();
|
|
120
|
+
return getProjectsDb()
|
|
121
|
+
.prepare(`SELECT document_id, task_id, created_at FROM library_document_links WHERE document_id = ? ORDER BY created_at ASC`)
|
|
122
|
+
.all(documentId);
|
|
123
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { DEFAULT_WORKFLOW_TEMPLATE_ID } from "../domain/workflow-template-id.js";
|
|
7
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function resolveDatabasePath() {
|
|
9
|
+
if (config.databasePath)
|
|
10
|
+
return config.databasePath;
|
|
11
|
+
return join(moduleDir, "..", "..", "..", "..", "data", "bridge.db");
|
|
12
|
+
}
|
|
13
|
+
let db = null;
|
|
14
|
+
function columnExists(database, table, column) {
|
|
15
|
+
const rows = database.prepare(`PRAGMA table_info(${table})`).all();
|
|
16
|
+
return rows.some((row) => row.name === column);
|
|
17
|
+
}
|
|
18
|
+
function migrate(database) {
|
|
19
|
+
database.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
repo_path TEXT NOT NULL DEFAULT '',
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
email TEXT NOT NULL UNIQUE,
|
|
32
|
+
password_hash TEXT NOT NULL,
|
|
33
|
+
role TEXT NOT NULL DEFAULT 'read-write',
|
|
34
|
+
is_system_admin INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
token TEXT NOT NULL UNIQUE,
|
|
36
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38
|
+
);
|
|
39
|
+
`);
|
|
40
|
+
if (!columnExists(database, "projects", "description")) {
|
|
41
|
+
database.exec("ALTER TABLE projects ADD COLUMN description TEXT NOT NULL DEFAULT ''");
|
|
42
|
+
}
|
|
43
|
+
if (!columnExists(database, "projects", "workflow_template_id")) {
|
|
44
|
+
database.exec(`ALTER TABLE projects ADD COLUMN workflow_template_id TEXT NOT NULL DEFAULT '${DEFAULT_WORKFLOW_TEMPLATE_ID}'`);
|
|
45
|
+
}
|
|
46
|
+
if (!columnExists(database, "users", "must_change_password")) {
|
|
47
|
+
database.exec("ALTER TABLE users ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0");
|
|
48
|
+
}
|
|
49
|
+
database
|
|
50
|
+
.prepare(`UPDATE projects SET workflow_template_id = ? WHERE trim(workflow_template_id) = ''`)
|
|
51
|
+
.run(DEFAULT_WORKFLOW_TEMPLATE_ID);
|
|
52
|
+
}
|
|
53
|
+
export function getProjectsDb() {
|
|
54
|
+
if (db)
|
|
55
|
+
return db;
|
|
56
|
+
const path = resolveDatabasePath();
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
+
db = new Database(path);
|
|
59
|
+
db.pragma("journal_mode = WAL");
|
|
60
|
+
migrate(db);
|
|
61
|
+
return db;
|
|
62
|
+
}
|
|
63
|
+
export function countProjects() {
|
|
64
|
+
const row = getProjectsDb()
|
|
65
|
+
.prepare("SELECT COUNT(*) AS count FROM projects")
|
|
66
|
+
.get();
|
|
67
|
+
return row.count;
|
|
68
|
+
}
|
|
69
|
+
export function listProjectRows() {
|
|
70
|
+
return getProjectsDb()
|
|
71
|
+
.prepare("SELECT id, name, repo_path, description, workflow_template_id FROM projects ORDER BY name COLLATE NOCASE")
|
|
72
|
+
.all();
|
|
73
|
+
}
|
|
74
|
+
export function listProjectRowsById(id) {
|
|
75
|
+
if (id === "")
|
|
76
|
+
return [];
|
|
77
|
+
return getProjectsDb()
|
|
78
|
+
.prepare("SELECT id, name, repo_path, description, workflow_template_id FROM projects WHERE id = ?")
|
|
79
|
+
.all(id);
|
|
80
|
+
}
|
|
81
|
+
export function upsertProjectRow(id, name, repoPath) {
|
|
82
|
+
getProjectsDb()
|
|
83
|
+
.prepare(`INSERT INTO projects (id, name, repo_path, updated_at)
|
|
84
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
85
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
86
|
+
name = excluded.name,
|
|
87
|
+
repo_path = excluded.repo_path,
|
|
88
|
+
updated_at = datetime('now')`)
|
|
89
|
+
.run(id, name || id, repoPath);
|
|
90
|
+
}
|
|
91
|
+
export function insertProjectRow(id, name, repoPath, description = "", workflowTemplateId = DEFAULT_WORKFLOW_TEMPLATE_ID) {
|
|
92
|
+
try {
|
|
93
|
+
getProjectsDb()
|
|
94
|
+
.prepare(`INSERT INTO projects (id, name, repo_path, description, workflow_template_id, updated_at)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`)
|
|
96
|
+
.run(id, name || id, repoPath, description, workflowTemplateId);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function updateProjectRow(id, input) {
|
|
104
|
+
if (listProjectRowsById(id).length === 0)
|
|
105
|
+
return false;
|
|
106
|
+
const result = getProjectsDb()
|
|
107
|
+
.prepare(`UPDATE projects SET name = ?, repo_path = ?, description = ?, workflow_template_id = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
108
|
+
.run(input.name, input.repoPath, input.description, input.workflowTemplateId, id);
|
|
109
|
+
return result.changes > 0;
|
|
110
|
+
}
|