@proletariat/cli 0.3.35 → 0.3.40
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/README.md +37 -2
- package/bin/dev.js +0 -0
- package/dist/commands/agent/auth.d.ts +12 -2
- package/dist/commands/agent/auth.js +128 -4
- package/dist/commands/agent/list.js +16 -7
- package/dist/commands/agent/status.js +32 -4
- package/dist/commands/board/watch.js +6 -0
- package/dist/commands/branch/list.d.ts +1 -0
- package/dist/commands/branch/list.js +43 -12
- package/dist/commands/branch/where.js +9 -19
- package/dist/commands/category/list.d.ts +2 -1
- package/dist/commands/category/list.js +38 -13
- package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
- package/dist/commands/{claude.js → claude/index.js} +12 -12
- package/dist/commands/claude/open.d.ts +13 -0
- package/dist/commands/claude/open.js +175 -0
- package/dist/commands/diet.js +18 -2
- package/dist/commands/docker/logs.js +7 -3
- package/dist/commands/docker/shell.js +6 -0
- package/dist/commands/docker/start.js +20 -4
- package/dist/commands/docker/sync.d.ts +4 -0
- package/dist/commands/docker/sync.js +30 -2
- package/dist/commands/epic/show.d.ts +13 -0
- package/dist/commands/epic/show.js +16 -0
- package/dist/commands/epic/ticket.js +7 -24
- package/dist/commands/epic/view.js +27 -0
- package/dist/commands/execution/config.d.ts +0 -4
- package/dist/commands/execution/config.js +14 -46
- package/dist/commands/execution/index.js +2 -1
- package/dist/commands/execution/logs.js +7 -1
- package/dist/commands/execution/stop.js +2 -1
- package/dist/commands/execution/view.js +30 -26
- package/dist/commands/init.js +2 -19
- package/dist/commands/label/create.js +2 -1
- package/dist/commands/label/delete.js +2 -1
- package/dist/commands/label/group/create.js +2 -1
- package/dist/commands/label/group/list.js +2 -1
- package/dist/commands/label/list.js +2 -1
- package/dist/commands/mcp-server.js +27 -1
- package/dist/commands/phase/template/list.js +2 -1
- package/dist/commands/pmo/init.js +12 -40
- package/dist/commands/project/create.js +3 -4
- package/dist/commands/project/update.js +5 -6
- package/dist/commands/pull.js +24 -0
- package/dist/commands/qa/index.d.ts +54 -0
- package/dist/commands/qa/index.js +762 -0
- package/dist/commands/repo/view.js +2 -8
- package/dist/commands/session/attach.js +4 -4
- package/dist/commands/session/create.d.ts +19 -0
- package/dist/commands/session/create.js +102 -0
- package/dist/commands/session/health.js +4 -23
- package/dist/commands/session/index.js +14 -1
- package/dist/commands/session/list.js +9 -8
- package/dist/commands/session/peek.d.ts +38 -0
- package/dist/commands/session/peek.js +316 -0
- package/dist/commands/session/poke.d.ts +27 -0
- package/dist/commands/session/poke.js +219 -0
- package/dist/commands/spec/view.js +29 -0
- package/dist/commands/template/list.js +2 -1
- package/dist/commands/theme/add-names.d.ts +4 -0
- package/dist/commands/theme/add-names.js +11 -1
- package/dist/commands/theme/create.d.ts +2 -0
- package/dist/commands/theme/create.js +8 -0
- package/dist/commands/ticket/bulk.js +2 -2
- package/dist/commands/ticket/complete.js +2 -2
- package/dist/commands/ticket/create.js +21 -0
- package/dist/commands/ticket/delete.js +8 -0
- package/dist/commands/ticket/edit.js +25 -0
- package/dist/commands/ticket/epic.js +17 -43
- package/dist/commands/ticket/index.js +2 -2
- package/dist/commands/ticket/move.js +25 -2
- package/dist/commands/ticket/resolve.js +3 -4
- package/dist/commands/ticket/show.d.ts +13 -0
- package/dist/commands/ticket/show.js +16 -0
- package/dist/commands/ticket/template/list.js +2 -1
- package/dist/commands/ticket/view.d.ts +0 -1
- package/dist/commands/ticket/view.js +30 -1
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/spawn-all.js +1 -1
- package/dist/commands/work/spawn.js +15 -4
- package/dist/commands/work/start.js +186 -103
- package/dist/commands/work/status.d.ts +14 -0
- package/dist/commands/work/status.js +60 -0
- package/dist/commands/work/watch.js +1 -1
- package/dist/commands/workflow/index.js +2 -1
- package/dist/commands/workflow/show.d.ts +13 -0
- package/dist/commands/workflow/show.js +16 -0
- package/dist/commands/workspace/add.js +15 -0
- package/dist/commands/workspace/list.js +2 -1
- package/dist/commands/workspace/prune.js +7 -7
- package/dist/hooks/init.js +10 -2
- package/dist/lib/agents/commands.d.ts +5 -0
- package/dist/lib/agents/commands.js +143 -97
- package/dist/lib/branch/index.d.ts +1 -0
- package/dist/lib/database/drizzle-schema.d.ts +465 -0
- package/dist/lib/database/drizzle-schema.js +53 -0
- package/dist/lib/database/index.d.ts +47 -1
- package/dist/lib/database/index.js +138 -20
- package/dist/lib/execution/config.d.ts +15 -1
- package/dist/lib/execution/config.js +28 -0
- package/dist/lib/execution/runners.d.ts +45 -0
- package/dist/lib/execution/runners.js +187 -26
- package/dist/lib/execution/session-utils.d.ts +16 -1
- package/dist/lib/execution/session-utils.js +71 -4
- package/dist/lib/execution/spawner.js +15 -2
- package/dist/lib/execution/storage.d.ts +6 -1
- package/dist/lib/execution/storage.js +35 -5
- package/dist/lib/execution/types.d.ts +3 -0
- package/dist/lib/mcp/tools/board.js +4 -6
- package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
- package/dist/lib/mcp/tools/epic.js +8 -3
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/spec.js +1 -1
- package/dist/lib/mcp/tools/ticket.js +11 -9
- package/dist/lib/mcp/tools/tmux.d.ts +16 -0
- package/dist/lib/mcp/tools/tmux.js +182 -0
- package/dist/lib/mcp/tools/work.js +148 -6
- package/dist/lib/mcp/types.d.ts +10 -0
- package/dist/lib/multiline-input.js +2 -1
- package/dist/lib/pmo/base-command.js +4 -4
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -0
- package/dist/lib/pmo/storage/actions.js +1 -1
- package/dist/lib/pmo/storage/base.js +402 -50
- package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
- package/dist/lib/pmo/storage/dependencies.js +11 -3
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/helpers.d.ts +4 -4
- package/dist/lib/pmo/storage/helpers.js +36 -26
- package/dist/lib/pmo/storage/projects.d.ts +2 -0
- package/dist/lib/pmo/storage/projects.js +207 -119
- package/dist/lib/pmo/storage/specs.d.ts +2 -0
- package/dist/lib/pmo/storage/specs.js +274 -188
- package/dist/lib/pmo/storage/tickets.d.ts +2 -0
- package/dist/lib/pmo/storage/tickets.js +350 -290
- package/dist/lib/pmo/storage/types.d.ts +1 -0
- package/dist/lib/pmo/storage/views.d.ts +2 -0
- package/dist/lib/pmo/storage/views.js +183 -130
- package/dist/lib/prompt-command.d.ts +20 -0
- package/dist/lib/prompt-command.js +38 -2
- package/dist/lib/prompt-json.d.ts +41 -4
- package/dist/lib/prompt-json.js +138 -7
- package/dist/lib/styles.d.ts +37 -0
- package/dist/lib/styles.js +73 -0
- package/oclif.manifest.json +4046 -3385
- package/package.json +11 -6
- package/LICENSE +0 -190
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* Tickets reference workflow statuses directly via status_id.
|
|
4
4
|
* Tickets have a position column for force-ranked ordering within a status.
|
|
5
5
|
* Positions use gapped integers (1000, 2000, 3000...) for stable reordering.
|
|
6
|
+
*
|
|
7
|
+
* This module uses Drizzle ORM for type-safe database queries.
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
9
|
+
import { eq, and, like, or, asc, sql, gt } from 'drizzle-orm';
|
|
10
|
+
import { pmoTickets, pmoProjects, pmoWorkflowStatuses, pmoSubtasks, pmoTicketMetadata, pmoCategories, pmoTicketLabels, pmoLabels, pmoLabelGroups, } from '../../database/drizzle-schema.js';
|
|
8
11
|
import { PMOError } from '../types.js';
|
|
9
12
|
import { slugify, generateEntityId } from '../utils.js';
|
|
10
13
|
import { rowToTicket, wrapSqliteError } from './helpers.js';
|
|
11
|
-
const T = PMO_TABLES;
|
|
12
14
|
export class TicketStorage {
|
|
13
15
|
ctx;
|
|
14
16
|
constructor(ctx) {
|
|
@@ -21,15 +23,18 @@ export class TicketStorage {
|
|
|
21
23
|
async validateCategory(category) {
|
|
22
24
|
if (!category)
|
|
23
25
|
return null;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const row = this.ctx.drizzle
|
|
27
|
+
.select({ name: pmoCategories.name })
|
|
28
|
+
.from(pmoCategories)
|
|
29
|
+
.where(and(sql `LOWER(${pmoCategories.name}) = LOWER(${category})`, eq(pmoCategories.type, 'ticket')))
|
|
30
|
+
.get();
|
|
28
31
|
if (!row) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const validCategories = this.ctx.drizzle
|
|
33
|
+
.select({ name: pmoCategories.name })
|
|
34
|
+
.from(pmoCategories)
|
|
35
|
+
.where(eq(pmoCategories.type, 'ticket'))
|
|
36
|
+
.orderBy(asc(pmoCategories.position))
|
|
37
|
+
.all();
|
|
33
38
|
const validNames = validCategories.map(c => c.name).join(', ');
|
|
34
39
|
throw new PMOError('INVALID', `Invalid category "${category}". Valid categories: ${validNames}`);
|
|
35
40
|
}
|
|
@@ -48,33 +53,42 @@ export class TicketStorage {
|
|
|
48
53
|
if (!identifier)
|
|
49
54
|
return null;
|
|
50
55
|
// 1. Exact ID match
|
|
51
|
-
const exactMatch = this.ctx.
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const exactMatch = this.ctx.drizzle
|
|
57
|
+
.select({ id: pmoProjects.id })
|
|
58
|
+
.from(pmoProjects)
|
|
59
|
+
.where(eq(pmoProjects.id, identifier))
|
|
60
|
+
.get();
|
|
54
61
|
if (exactMatch)
|
|
55
62
|
return exactMatch.id;
|
|
56
63
|
// 2. Case-insensitive ID match
|
|
57
|
-
const caseInsensitiveId = this.ctx.
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
const caseInsensitiveId = this.ctx.drizzle
|
|
65
|
+
.select({ id: pmoProjects.id })
|
|
66
|
+
.from(pmoProjects)
|
|
67
|
+
.where(sql `LOWER(${pmoProjects.id}) = LOWER(${identifier})`)
|
|
68
|
+
.get();
|
|
60
69
|
if (caseInsensitiveId)
|
|
61
70
|
return caseInsensitiveId.id;
|
|
62
71
|
// 3. Exact name match
|
|
63
|
-
const nameMatch = this.ctx.
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
const nameMatch = this.ctx.drizzle
|
|
73
|
+
.select({ id: pmoProjects.id })
|
|
74
|
+
.from(pmoProjects)
|
|
75
|
+
.where(eq(pmoProjects.name, identifier))
|
|
76
|
+
.get();
|
|
66
77
|
if (nameMatch)
|
|
67
78
|
return nameMatch.id;
|
|
68
79
|
// 4. Case-insensitive name match
|
|
69
|
-
const caseInsensitiveName = this.ctx.
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const caseInsensitiveName = this.ctx.drizzle
|
|
81
|
+
.select({ id: pmoProjects.id })
|
|
82
|
+
.from(pmoProjects)
|
|
83
|
+
.where(sql `LOWER(${pmoProjects.name}) = LOWER(${identifier})`)
|
|
84
|
+
.get();
|
|
72
85
|
if (caseInsensitiveName)
|
|
73
86
|
return caseInsensitiveName.id;
|
|
74
87
|
// 5. Slugified name match
|
|
75
|
-
const allProjects = this.ctx.
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
const allProjects = this.ctx.drizzle
|
|
89
|
+
.select({ id: pmoProjects.id, name: pmoProjects.name })
|
|
90
|
+
.from(pmoProjects)
|
|
91
|
+
.all();
|
|
78
92
|
const identifierLower = identifier.toLowerCase();
|
|
79
93
|
for (const project of allProjects) {
|
|
80
94
|
const projectSlug = slugify(project.name);
|
|
@@ -98,48 +112,51 @@ export class TicketStorage {
|
|
|
98
112
|
// Get status_id from project's workflow
|
|
99
113
|
let statusId = ticket.statusId;
|
|
100
114
|
// Get the project's workflow
|
|
101
|
-
const project = this.ctx.
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
const project = this.ctx.drizzle
|
|
116
|
+
.select({ workflowId: pmoProjects.workflowId })
|
|
117
|
+
.from(pmoProjects)
|
|
118
|
+
.where(eq(pmoProjects.id, projectId))
|
|
119
|
+
.get();
|
|
104
120
|
if (!project) {
|
|
105
121
|
throw new PMOError('NOT_FOUND', `Project not found: ${projectId}`);
|
|
106
122
|
}
|
|
107
|
-
const workflowId = project.
|
|
123
|
+
const workflowId = project.workflowId || 'default';
|
|
108
124
|
// If statusName is provided, look up status by name
|
|
109
125
|
if (!statusId && ticket.statusName) {
|
|
110
|
-
const namedStatus = this.ctx.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
const namedStatus = this.ctx.drizzle
|
|
127
|
+
.select({ id: pmoWorkflowStatuses.id })
|
|
128
|
+
.from(pmoWorkflowStatuses)
|
|
129
|
+
.where(and(eq(pmoWorkflowStatuses.workflowId, workflowId), sql `LOWER(${pmoWorkflowStatuses.name}) = LOWER(${ticket.statusName})`))
|
|
130
|
+
.get();
|
|
114
131
|
if (namedStatus) {
|
|
115
132
|
statusId = namedStatus.id;
|
|
116
133
|
}
|
|
117
134
|
}
|
|
118
135
|
if (!statusId) {
|
|
119
136
|
// Get default status from workflow
|
|
120
|
-
const defaultStatus = this.ctx.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
const defaultStatus = this.ctx.drizzle
|
|
138
|
+
.select({ id: pmoWorkflowStatuses.id })
|
|
139
|
+
.from(pmoWorkflowStatuses)
|
|
140
|
+
.where(and(eq(pmoWorkflowStatuses.workflowId, workflowId), eq(pmoWorkflowStatuses.isDefault, true)))
|
|
141
|
+
.get();
|
|
124
142
|
if (defaultStatus) {
|
|
125
143
|
statusId = defaultStatus.id;
|
|
126
144
|
}
|
|
127
145
|
else {
|
|
128
146
|
// Fall back to first status in workflow (by category then position)
|
|
129
|
-
const firstStatus = this.ctx.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
const firstStatus = this.ctx.drizzle
|
|
148
|
+
.select({ id: pmoWorkflowStatuses.id })
|
|
149
|
+
.from(pmoWorkflowStatuses)
|
|
150
|
+
.where(eq(pmoWorkflowStatuses.workflowId, workflowId))
|
|
151
|
+
.orderBy(sql `CASE ${pmoWorkflowStatuses.category}
|
|
134
152
|
WHEN 'backlog' THEN 1
|
|
135
153
|
WHEN 'unstarted' THEN 2
|
|
136
154
|
WHEN 'started' THEN 3
|
|
137
155
|
WHEN 'completed' THEN 4
|
|
138
156
|
WHEN 'canceled' THEN 5
|
|
139
|
-
END
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`).get(workflowId);
|
|
157
|
+
END`, asc(pmoWorkflowStatuses.position))
|
|
158
|
+
.limit(1)
|
|
159
|
+
.get();
|
|
143
160
|
if (firstStatus) {
|
|
144
161
|
statusId = firstStatus.id;
|
|
145
162
|
}
|
|
@@ -149,43 +166,60 @@ export class TicketStorage {
|
|
|
149
166
|
}
|
|
150
167
|
}
|
|
151
168
|
// Get next position for the target status (append to end with gapped integer)
|
|
152
|
-
const maxPos = this.ctx.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
const maxPos = this.ctx.drizzle
|
|
170
|
+
.select({ maxPos: sql `COALESCE(MAX(${pmoTickets.position}), 0)` })
|
|
171
|
+
.from(pmoTickets)
|
|
172
|
+
.where(eq(pmoTickets.statusId, statusId))
|
|
173
|
+
.get();
|
|
174
|
+
const position = (maxPos?.maxPos ?? 0) + 1000;
|
|
156
175
|
// Insert ticket
|
|
157
176
|
const labels = ticket.labels || [];
|
|
158
177
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
const ticketValues = {
|
|
179
|
+
id,
|
|
180
|
+
projectId,
|
|
181
|
+
title,
|
|
182
|
+
description: ticket.description || null,
|
|
183
|
+
priority: ticket.priority || null,
|
|
184
|
+
category: validatedCategory,
|
|
185
|
+
status: 'backlog',
|
|
186
|
+
statusId: statusId,
|
|
187
|
+
owner: ticket.owner || null,
|
|
188
|
+
assignee: ticket.assignee || null,
|
|
189
|
+
specId,
|
|
190
|
+
epicId: ticket.epicId || null,
|
|
191
|
+
labels: JSON.stringify(labels),
|
|
192
|
+
position,
|
|
193
|
+
createdAt: String(now),
|
|
194
|
+
updatedAt: String(now),
|
|
195
|
+
lastSyncedFromSpec: ticket.lastSyncedFromSpec ? String(ticket.lastSyncedFromSpec) : null,
|
|
196
|
+
lastSyncedFromBoard: ticket.lastSyncedFromBoard ? String(ticket.lastSyncedFromBoard) : null,
|
|
197
|
+
};
|
|
198
|
+
this.ctx.drizzle.insert(pmoTickets).values(ticketValues).run();
|
|
167
199
|
}
|
|
168
200
|
catch (err) {
|
|
169
201
|
wrapSqliteError('Ticket', 'create', err);
|
|
170
202
|
}
|
|
171
203
|
// Insert subtasks
|
|
172
204
|
if (ticket.subtasks && ticket.subtasks.length > 0) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
for (const [idx, st] of ticket.subtasks.entries()) {
|
|
206
|
+
this.ctx.drizzle.insert(pmoSubtasks).values({
|
|
207
|
+
id: st.id || slugify(st.title),
|
|
208
|
+
ticketId: id,
|
|
209
|
+
title: st.title,
|
|
210
|
+
done: st.done || false,
|
|
211
|
+
position: idx,
|
|
212
|
+
}).run();
|
|
213
|
+
}
|
|
180
214
|
}
|
|
181
215
|
// Insert metadata
|
|
182
216
|
if (ticket.metadata) {
|
|
183
|
-
const insertMeta = this.ctx.db.prepare(`
|
|
184
|
-
INSERT INTO ${T.ticket_metadata} (ticket_id, key, value)
|
|
185
|
-
VALUES (?, ?, ?)
|
|
186
|
-
`);
|
|
187
217
|
for (const [key, value] of Object.entries(ticket.metadata)) {
|
|
188
|
-
|
|
218
|
+
this.ctx.drizzle.insert(pmoTicketMetadata).values({
|
|
219
|
+
ticketId: id,
|
|
220
|
+
key,
|
|
221
|
+
value,
|
|
222
|
+
}).run();
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
225
|
this.ctx.updateBoardTimestamp(projectId);
|
|
@@ -203,18 +237,38 @@ export class TicketStorage {
|
|
|
203
237
|
* Joins workflow_statuses to get column name (status name is the column).
|
|
204
238
|
*/
|
|
205
239
|
async getTicketById(id) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
const rows = this.ctx.drizzle
|
|
241
|
+
.select({
|
|
242
|
+
id: pmoTickets.id,
|
|
243
|
+
project_id: pmoTickets.projectId,
|
|
244
|
+
title: pmoTickets.title,
|
|
245
|
+
description: pmoTickets.description,
|
|
246
|
+
priority: pmoTickets.priority,
|
|
247
|
+
category: pmoTickets.category,
|
|
248
|
+
status_id: pmoTickets.statusId,
|
|
249
|
+
owner: pmoTickets.owner,
|
|
250
|
+
assignee: pmoTickets.assignee,
|
|
251
|
+
branch: pmoTickets.branch,
|
|
252
|
+
spec_id: pmoTickets.specId,
|
|
253
|
+
epic_id: pmoTickets.epicId,
|
|
254
|
+
labels: pmoTickets.labels,
|
|
255
|
+
position: pmoTickets.position,
|
|
256
|
+
created_at: pmoTickets.createdAt,
|
|
257
|
+
updated_at: pmoTickets.updatedAt,
|
|
258
|
+
last_synced_from_spec: pmoTickets.lastSyncedFromSpec,
|
|
259
|
+
last_synced_from_board: pmoTickets.lastSyncedFromBoard,
|
|
260
|
+
column_id: pmoWorkflowStatuses.id,
|
|
261
|
+
column_name: pmoWorkflowStatuses.name,
|
|
262
|
+
project_name: sql `NULL`,
|
|
263
|
+
})
|
|
264
|
+
.from(pmoTickets)
|
|
265
|
+
.leftJoin(pmoWorkflowStatuses, eq(pmoTickets.statusId, pmoWorkflowStatuses.id))
|
|
266
|
+
.where(sql `LOWER(${pmoTickets.id}) = LOWER(${id})`)
|
|
267
|
+
.all();
|
|
268
|
+
if (rows.length === 0)
|
|
216
269
|
return null;
|
|
217
|
-
|
|
270
|
+
const row = rows[0];
|
|
271
|
+
return rowToTicket(this.ctx.drizzle, row);
|
|
218
272
|
}
|
|
219
273
|
/**
|
|
220
274
|
* Update a ticket.
|
|
@@ -230,82 +284,63 @@ export class TicketStorage {
|
|
|
230
284
|
if (changes.category !== undefined) {
|
|
231
285
|
validatedCategory = await this.validateCategory(changes.category);
|
|
232
286
|
}
|
|
233
|
-
const updates =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
updates.push('status_id = ?');
|
|
253
|
-
params.push(changes.statusId);
|
|
254
|
-
}
|
|
255
|
-
if (changes.owner !== undefined) {
|
|
256
|
-
updates.push('owner = ?');
|
|
257
|
-
params.push(changes.owner);
|
|
258
|
-
}
|
|
259
|
-
if (changes.assignee !== undefined) {
|
|
260
|
-
updates.push('assignee = ?');
|
|
261
|
-
params.push(changes.assignee);
|
|
262
|
-
}
|
|
263
|
-
if (changes.branch !== undefined) {
|
|
264
|
-
updates.push('branch = ?');
|
|
265
|
-
params.push(changes.branch);
|
|
266
|
-
}
|
|
267
|
-
if (changes.specId !== undefined) {
|
|
268
|
-
updates.push('spec_id = ?');
|
|
269
|
-
params.push(changes.specId);
|
|
270
|
-
}
|
|
287
|
+
const updates = {};
|
|
288
|
+
if (changes.title !== undefined)
|
|
289
|
+
updates.title = changes.title;
|
|
290
|
+
if (changes.description !== undefined)
|
|
291
|
+
updates.description = changes.description;
|
|
292
|
+
if (changes.priority !== undefined)
|
|
293
|
+
updates.priority = changes.priority;
|
|
294
|
+
if (validatedCategory !== undefined)
|
|
295
|
+
updates.category = validatedCategory;
|
|
296
|
+
if (changes.statusId !== undefined)
|
|
297
|
+
updates.statusId = changes.statusId;
|
|
298
|
+
if (changes.owner !== undefined)
|
|
299
|
+
updates.owner = changes.owner;
|
|
300
|
+
if (changes.assignee !== undefined)
|
|
301
|
+
updates.assignee = changes.assignee;
|
|
302
|
+
if (changes.branch !== undefined)
|
|
303
|
+
updates.branch = changes.branch;
|
|
304
|
+
if (changes.specId !== undefined)
|
|
305
|
+
updates.specId = changes.specId;
|
|
271
306
|
if (changes.lastSyncedFromSpec !== undefined) {
|
|
272
|
-
updates.
|
|
273
|
-
params.push(changes.lastSyncedFromSpec);
|
|
307
|
+
updates.lastSyncedFromSpec = changes.lastSyncedFromSpec;
|
|
274
308
|
}
|
|
275
309
|
if (changes.lastSyncedFromBoard !== undefined) {
|
|
276
|
-
updates.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
this.ctx.db.prepare(`UPDATE ${T.tickets} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
310
|
+
updates.lastSyncedFromBoard = changes.lastSyncedFromBoard;
|
|
311
|
+
}
|
|
312
|
+
if (changes.labels !== undefined)
|
|
313
|
+
updates.labels = JSON.stringify(changes.labels);
|
|
314
|
+
if (Object.keys(updates).length > 0) {
|
|
315
|
+
updates.updatedAt = String(Date.now());
|
|
316
|
+
this.ctx.drizzle
|
|
317
|
+
.update(pmoTickets)
|
|
318
|
+
.set(updates)
|
|
319
|
+
.where(eq(pmoTickets.id, id))
|
|
320
|
+
.run();
|
|
288
321
|
}
|
|
289
322
|
// Update subtasks if provided
|
|
290
323
|
if (changes.subtasks !== undefined) {
|
|
291
|
-
this.ctx.
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
324
|
+
this.ctx.drizzle.delete(pmoSubtasks).where(eq(pmoSubtasks.ticketId, id)).run();
|
|
325
|
+
for (const [idx, st] of changes.subtasks.entries()) {
|
|
326
|
+
this.ctx.drizzle.insert(pmoSubtasks).values({
|
|
327
|
+
id: st.id || slugify(st.title),
|
|
328
|
+
ticketId: id,
|
|
329
|
+
title: st.title,
|
|
330
|
+
done: st.done || false,
|
|
331
|
+
position: idx,
|
|
332
|
+
}).run();
|
|
333
|
+
}
|
|
299
334
|
}
|
|
300
335
|
// Update metadata if provided
|
|
301
336
|
if (changes.metadata !== undefined) {
|
|
302
|
-
this.ctx.
|
|
303
|
-
const insertMeta = this.ctx.db.prepare(`
|
|
304
|
-
INSERT INTO ${T.ticket_metadata} (ticket_id, key, value)
|
|
305
|
-
VALUES (?, ?, ?)
|
|
306
|
-
`);
|
|
337
|
+
this.ctx.drizzle.delete(pmoTicketMetadata).where(eq(pmoTicketMetadata.ticketId, id)).run();
|
|
307
338
|
for (const [key, value] of Object.entries(changes.metadata)) {
|
|
308
|
-
|
|
339
|
+
this.ctx.drizzle.insert(pmoTicketMetadata).values({
|
|
340
|
+
ticketId: id,
|
|
341
|
+
key,
|
|
342
|
+
value,
|
|
343
|
+
}).run();
|
|
309
344
|
}
|
|
310
345
|
}
|
|
311
346
|
// Update board timestamp for the ticket's actual project
|
|
@@ -318,11 +353,11 @@ export class TicketStorage {
|
|
|
318
353
|
* Update the timestamp for a specific project.
|
|
319
354
|
*/
|
|
320
355
|
updateProjectTimestamp(projectId) {
|
|
321
|
-
this.ctx.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
356
|
+
this.ctx.drizzle
|
|
357
|
+
.update(pmoProjects)
|
|
358
|
+
.set({ updatedAt: String(Date.now()) })
|
|
359
|
+
.where(eq(pmoProjects.id, projectId))
|
|
360
|
+
.run();
|
|
326
361
|
}
|
|
327
362
|
/**
|
|
328
363
|
* Move a ticket to a different status (column).
|
|
@@ -336,18 +371,21 @@ export class TicketStorage {
|
|
|
336
371
|
throw new PMOError('NOT_FOUND', `Ticket not found: ${id}`, id);
|
|
337
372
|
}
|
|
338
373
|
// Get project's workflow
|
|
339
|
-
const project = this.ctx.
|
|
340
|
-
|
|
341
|
-
|
|
374
|
+
const project = this.ctx.drizzle
|
|
375
|
+
.select({ workflowId: pmoProjects.workflowId })
|
|
376
|
+
.from(pmoProjects)
|
|
377
|
+
.where(eq(pmoProjects.id, projectId))
|
|
378
|
+
.get();
|
|
342
379
|
if (!project) {
|
|
343
380
|
throw new PMOError('NOT_FOUND', `Project not found: ${projectId}`);
|
|
344
381
|
}
|
|
345
|
-
const workflowId = project.
|
|
382
|
+
const workflowId = project.workflowId || 'default';
|
|
346
383
|
// Find target status by ID or name
|
|
347
|
-
const targetStatus = this.ctx.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
384
|
+
const targetStatus = this.ctx.drizzle
|
|
385
|
+
.select({ id: pmoWorkflowStatuses.id })
|
|
386
|
+
.from(pmoWorkflowStatuses)
|
|
387
|
+
.where(and(eq(pmoWorkflowStatuses.workflowId, workflowId), or(eq(pmoWorkflowStatuses.id, column), sql `LOWER(${pmoWorkflowStatuses.name}) = LOWER(${column})`)))
|
|
388
|
+
.get();
|
|
351
389
|
if (!targetStatus) {
|
|
352
390
|
throw new PMOError('NOT_FOUND', `Status not found: ${column}`);
|
|
353
391
|
}
|
|
@@ -357,17 +395,23 @@ export class TicketStorage {
|
|
|
357
395
|
newPosition = position;
|
|
358
396
|
}
|
|
359
397
|
else {
|
|
360
|
-
const maxPos = this.ctx.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
398
|
+
const maxPos = this.ctx.drizzle
|
|
399
|
+
.select({ maxPos: sql `COALESCE(MAX(${pmoTickets.position}), 0)` })
|
|
400
|
+
.from(pmoTickets)
|
|
401
|
+
.where(eq(pmoTickets.statusId, targetStatus.id))
|
|
402
|
+
.get();
|
|
403
|
+
newPosition = (maxPos?.maxPos ?? 0) + 1000;
|
|
364
404
|
}
|
|
365
405
|
// Update ticket's status_id and position
|
|
366
|
-
this.ctx.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
406
|
+
this.ctx.drizzle
|
|
407
|
+
.update(pmoTickets)
|
|
408
|
+
.set({
|
|
409
|
+
statusId: targetStatus.id,
|
|
410
|
+
position: newPosition,
|
|
411
|
+
updatedAt: String(Date.now()),
|
|
412
|
+
})
|
|
413
|
+
.where(eq(pmoTickets.id, id))
|
|
414
|
+
.run();
|
|
371
415
|
this.ctx.updateBoardTimestamp(projectId);
|
|
372
416
|
return (await this.getTicketById(id));
|
|
373
417
|
}
|
|
@@ -395,12 +439,13 @@ export class TicketStorage {
|
|
|
395
439
|
}
|
|
396
440
|
const afterPosition = afterTicket.position ?? 0;
|
|
397
441
|
// Find the next ticket after the target
|
|
398
|
-
const nextTicket = this.ctx.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
442
|
+
const nextTicket = this.ctx.drizzle
|
|
443
|
+
.select({ position: pmoTickets.position })
|
|
444
|
+
.from(pmoTickets)
|
|
445
|
+
.where(and(eq(pmoTickets.statusId, existing.statusId), gt(pmoTickets.position, afterPosition), sql `${pmoTickets.id} != ${id}`))
|
|
446
|
+
.orderBy(asc(pmoTickets.position))
|
|
447
|
+
.limit(1)
|
|
448
|
+
.get();
|
|
404
449
|
if (nextTicket) {
|
|
405
450
|
// Place between afterTicket and nextTicket
|
|
406
451
|
const gap = nextTicket.position - afterPosition;
|
|
@@ -413,12 +458,13 @@ export class TicketStorage {
|
|
|
413
458
|
// Re-read the after ticket position after regapping
|
|
414
459
|
const refreshedAfter = await this.getTicketById(opts.afterTicketId);
|
|
415
460
|
const refreshedAfterPos = refreshedAfter?.position ?? 0;
|
|
416
|
-
const refreshedNext = this.ctx.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
461
|
+
const refreshedNext = this.ctx.drizzle
|
|
462
|
+
.select({ position: pmoTickets.position })
|
|
463
|
+
.from(pmoTickets)
|
|
464
|
+
.where(and(eq(pmoTickets.statusId, existing.statusId), gt(pmoTickets.position, refreshedAfterPos), sql `${pmoTickets.id} != ${id}`))
|
|
465
|
+
.orderBy(asc(pmoTickets.position))
|
|
466
|
+
.limit(1)
|
|
467
|
+
.get();
|
|
422
468
|
newPosition = refreshedNext
|
|
423
469
|
? refreshedAfterPos + Math.floor((refreshedNext.position - refreshedAfterPos) / 2)
|
|
424
470
|
: refreshedAfterPos + 1000;
|
|
@@ -435,11 +481,14 @@ export class TicketStorage {
|
|
|
435
481
|
else {
|
|
436
482
|
throw new PMOError('INVALID', 'Must provide either position or after_ticket_id');
|
|
437
483
|
}
|
|
438
|
-
this.ctx.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
484
|
+
this.ctx.drizzle
|
|
485
|
+
.update(pmoTickets)
|
|
486
|
+
.set({
|
|
487
|
+
position: newPosition,
|
|
488
|
+
updatedAt: String(Date.now()),
|
|
489
|
+
})
|
|
490
|
+
.where(eq(pmoTickets.id, id))
|
|
491
|
+
.run();
|
|
443
492
|
if (existing.projectId) {
|
|
444
493
|
this.ctx.updateBoardTimestamp(existing.projectId);
|
|
445
494
|
}
|
|
@@ -450,24 +499,24 @@ export class TicketStorage {
|
|
|
450
499
|
* Optionally excludes a ticket (e.g., the one being moved).
|
|
451
500
|
*/
|
|
452
501
|
regapPositions(statusId, excludeTicketId) {
|
|
453
|
-
|
|
454
|
-
SELECT id FROM ${T.tickets}
|
|
455
|
-
WHERE status_id = ?
|
|
456
|
-
`;
|
|
457
|
-
const params = [statusId];
|
|
502
|
+
const conditions = [eq(pmoTickets.statusId, statusId)];
|
|
458
503
|
if (excludeTicketId) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
504
|
+
conditions.push(sql `${pmoTickets.id} != ${excludeTicketId}`);
|
|
505
|
+
}
|
|
506
|
+
const tickets = this.ctx.drizzle
|
|
507
|
+
.select({ id: pmoTickets.id })
|
|
508
|
+
.from(pmoTickets)
|
|
509
|
+
.where(and(...conditions))
|
|
510
|
+
.orderBy(asc(pmoTickets.position), asc(pmoTickets.createdAt))
|
|
511
|
+
.all();
|
|
512
|
+
this.ctx.drizzle.transaction((tx) => {
|
|
466
513
|
tickets.forEach((ticket, idx) => {
|
|
467
|
-
update
|
|
514
|
+
tx.update(pmoTickets)
|
|
515
|
+
.set({ position: (idx + 1) * 1000 })
|
|
516
|
+
.where(eq(pmoTickets.id, ticket.id))
|
|
517
|
+
.run();
|
|
468
518
|
});
|
|
469
519
|
});
|
|
470
|
-
regap();
|
|
471
520
|
}
|
|
472
521
|
/**
|
|
473
522
|
* Delete a ticket.
|
|
@@ -485,10 +534,10 @@ export class TicketStorage {
|
|
|
485
534
|
// Delete ticket (by ID only, since IDs are globally unique)
|
|
486
535
|
// Related data (subtasks, metadata) are deleted via CASCADE
|
|
487
536
|
try {
|
|
488
|
-
const result = this.ctx.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
537
|
+
const result = this.ctx.drizzle
|
|
538
|
+
.delete(pmoTickets)
|
|
539
|
+
.where(eq(pmoTickets.id, id))
|
|
540
|
+
.run();
|
|
492
541
|
if (result.changes === 0) {
|
|
493
542
|
throw new PMOError('NOT_FOUND', `Ticket not found: ${id}`, id);
|
|
494
543
|
}
|
|
@@ -507,7 +556,6 @@ export class TicketStorage {
|
|
|
507
556
|
* @param filter - Additional filters to apply.
|
|
508
557
|
*/
|
|
509
558
|
async listTickets(projectIdOrName, filter) {
|
|
510
|
-
const params = [];
|
|
511
559
|
// Resolve project identifier to actual ID if provided
|
|
512
560
|
let resolvedProjectId;
|
|
513
561
|
if (projectIdOrName !== undefined) {
|
|
@@ -517,91 +565,94 @@ export class TicketStorage {
|
|
|
517
565
|
resolvedProjectId = projectIdOrName;
|
|
518
566
|
}
|
|
519
567
|
}
|
|
520
|
-
// Build
|
|
521
|
-
|
|
522
|
-
SELECT t.*,
|
|
523
|
-
ws.id as column_id,
|
|
524
|
-
t.position as position,
|
|
525
|
-
ws.name as column_name,
|
|
526
|
-
p.name as project_name
|
|
527
|
-
FROM ${T.tickets} t
|
|
528
|
-
LEFT JOIN ${T.workflow_statuses} ws ON t.status_id = ws.id
|
|
529
|
-
LEFT JOIN ${T.projects} p ON t.project_id = p.id
|
|
530
|
-
WHERE 1=1
|
|
531
|
-
`;
|
|
568
|
+
// Build conditions array for dynamic WHERE clause
|
|
569
|
+
const conditions = [];
|
|
532
570
|
// Apply project scoping
|
|
533
571
|
if (resolvedProjectId !== undefined) {
|
|
534
|
-
|
|
535
|
-
params.push(resolvedProjectId);
|
|
572
|
+
conditions.push(eq(pmoTickets.projectId, resolvedProjectId));
|
|
536
573
|
}
|
|
537
|
-
// If projectId is undefined, list all tickets across all projects
|
|
538
574
|
if (filter?.statusId) {
|
|
539
|
-
|
|
540
|
-
params.push(filter.statusId);
|
|
575
|
+
conditions.push(eq(pmoTickets.statusId, filter.statusId));
|
|
541
576
|
}
|
|
542
577
|
if (filter?.statusCategory) {
|
|
543
|
-
|
|
544
|
-
params.push(filter.statusCategory);
|
|
578
|
+
conditions.push(eq(pmoWorkflowStatuses.category, filter.statusCategory));
|
|
545
579
|
}
|
|
546
580
|
if (filter?.priority) {
|
|
547
|
-
|
|
548
|
-
params.push(filter.priority);
|
|
581
|
+
conditions.push(eq(pmoTickets.priority, filter.priority));
|
|
549
582
|
}
|
|
550
583
|
if (filter?.category) {
|
|
551
|
-
|
|
552
|
-
params.push(filter.category);
|
|
584
|
+
conditions.push(eq(pmoTickets.category, filter.category));
|
|
553
585
|
}
|
|
554
586
|
if (filter?.owner) {
|
|
555
|
-
|
|
556
|
-
params.push(filter.owner);
|
|
587
|
+
conditions.push(eq(pmoTickets.owner, filter.owner));
|
|
557
588
|
}
|
|
558
589
|
if (filter?.assignee) {
|
|
559
|
-
|
|
560
|
-
params.push(filter.assignee);
|
|
590
|
+
conditions.push(eq(pmoTickets.assignee, filter.assignee));
|
|
561
591
|
}
|
|
562
592
|
if (filter?.search) {
|
|
563
|
-
|
|
564
|
-
params.push(`%${filter.search}%`, `%${filter.search}%`);
|
|
593
|
+
conditions.push(or(like(pmoTickets.title, `%${filter.search}%`), like(pmoTickets.description, `%${filter.search}%`)));
|
|
565
594
|
}
|
|
566
595
|
if (filter?.spec) {
|
|
567
|
-
|
|
568
|
-
params.push(filter.spec);
|
|
596
|
+
conditions.push(eq(pmoTickets.specId, filter.spec));
|
|
569
597
|
}
|
|
570
598
|
if (filter?.epic) {
|
|
571
|
-
|
|
572
|
-
params.push(filter.epic);
|
|
599
|
+
conditions.push(eq(pmoTickets.epicId, filter.epic));
|
|
573
600
|
}
|
|
574
601
|
if (filter?.column) {
|
|
575
|
-
|
|
576
|
-
query += ' AND ws.name = ?';
|
|
577
|
-
params.push(filter.column);
|
|
602
|
+
conditions.push(eq(pmoWorkflowStatuses.name, filter.column));
|
|
578
603
|
}
|
|
579
604
|
if (filter?.label) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
params.push(filter.label);
|
|
605
|
+
conditions.push(sql `${pmoTickets.id} IN (
|
|
606
|
+
SELECT ${pmoTicketLabels.ticketId} FROM ${pmoTicketLabels}
|
|
607
|
+
JOIN ${pmoLabels} ON ${pmoTicketLabels.labelId} = ${pmoLabels.id}
|
|
608
|
+
WHERE LOWER(${pmoLabels.name}) = LOWER(${filter.label})
|
|
609
|
+
)`);
|
|
586
610
|
}
|
|
587
611
|
if (filter?.labelGroup) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
612
|
+
conditions.push(sql `${pmoTickets.id} IN (
|
|
613
|
+
SELECT ${pmoTicketLabels.ticketId} FROM ${pmoTicketLabels}
|
|
614
|
+
JOIN ${pmoLabels} ON ${pmoTicketLabels.labelId} = ${pmoLabels.id}
|
|
615
|
+
JOIN ${pmoLabelGroups} ON ${pmoLabels.groupId} = ${pmoLabelGroups.id}
|
|
616
|
+
WHERE LOWER(${pmoLabelGroups.name}) = LOWER(${filter.labelGroup})
|
|
617
|
+
)`);
|
|
618
|
+
}
|
|
619
|
+
// Build order clause
|
|
620
|
+
const orderClauses = projectIdOrName === undefined
|
|
621
|
+
? [asc(pmoProjects.name), asc(pmoWorkflowStatuses.position), asc(pmoTickets.position), asc(pmoTickets.createdAt)]
|
|
622
|
+
: [asc(pmoWorkflowStatuses.position), asc(pmoTickets.position), asc(pmoTickets.createdAt)];
|
|
623
|
+
let query = this.ctx.drizzle
|
|
624
|
+
.select({
|
|
625
|
+
id: pmoTickets.id,
|
|
626
|
+
project_id: pmoTickets.projectId,
|
|
627
|
+
title: pmoTickets.title,
|
|
628
|
+
description: pmoTickets.description,
|
|
629
|
+
priority: pmoTickets.priority,
|
|
630
|
+
category: pmoTickets.category,
|
|
631
|
+
status_id: pmoTickets.statusId,
|
|
632
|
+
owner: pmoTickets.owner,
|
|
633
|
+
assignee: pmoTickets.assignee,
|
|
634
|
+
branch: pmoTickets.branch,
|
|
635
|
+
spec_id: pmoTickets.specId,
|
|
636
|
+
epic_id: pmoTickets.epicId,
|
|
637
|
+
labels: pmoTickets.labels,
|
|
638
|
+
position: pmoTickets.position,
|
|
639
|
+
created_at: pmoTickets.createdAt,
|
|
640
|
+
updated_at: pmoTickets.updatedAt,
|
|
641
|
+
last_synced_from_spec: pmoTickets.lastSyncedFromSpec,
|
|
642
|
+
last_synced_from_board: pmoTickets.lastSyncedFromBoard,
|
|
643
|
+
column_id: pmoWorkflowStatuses.id,
|
|
644
|
+
column_name: pmoWorkflowStatuses.name,
|
|
645
|
+
project_name: pmoProjects.name,
|
|
646
|
+
})
|
|
647
|
+
.from(pmoTickets)
|
|
648
|
+
.leftJoin(pmoWorkflowStatuses, eq(pmoTickets.statusId, pmoWorkflowStatuses.id))
|
|
649
|
+
.leftJoin(pmoProjects, eq(pmoTickets.projectId, pmoProjects.id))
|
|
650
|
+
.$dynamic();
|
|
651
|
+
if (conditions.length > 0) {
|
|
652
|
+
query = query.where(and(...conditions));
|
|
653
|
+
}
|
|
654
|
+
const rows = query.orderBy(...orderClauses).all();
|
|
655
|
+
return Promise.all(rows.map((row) => rowToTicket(this.ctx.drizzle, row)));
|
|
605
656
|
}
|
|
606
657
|
/**
|
|
607
658
|
* Move a ticket to a different project.
|
|
@@ -617,55 +668,64 @@ export class TicketStorage {
|
|
|
617
668
|
throw new PMOError('INVALID', `Ticket ${ticketId} has no associated project`, ticketId);
|
|
618
669
|
}
|
|
619
670
|
// Check if target project exists and get its workflow
|
|
620
|
-
const targetProject = this.ctx.
|
|
621
|
-
|
|
622
|
-
|
|
671
|
+
const targetProject = this.ctx.drizzle
|
|
672
|
+
.select({ id: pmoProjects.id, workflowId: pmoProjects.workflowId })
|
|
673
|
+
.from(pmoProjects)
|
|
674
|
+
.where(eq(pmoProjects.id, newProjectId))
|
|
675
|
+
.get();
|
|
623
676
|
if (!targetProject) {
|
|
624
677
|
throw new PMOError('NOT_FOUND', `Project not found: ${newProjectId}`, newProjectId);
|
|
625
678
|
}
|
|
626
|
-
const workflowId = targetProject.
|
|
679
|
+
const workflowId = targetProject.workflowId || 'default';
|
|
627
680
|
// Get default status for target project's workflow
|
|
628
681
|
let newStatusId;
|
|
629
|
-
const defaultStatus = this.ctx.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
682
|
+
const defaultStatus = this.ctx.drizzle
|
|
683
|
+
.select({ id: pmoWorkflowStatuses.id })
|
|
684
|
+
.from(pmoWorkflowStatuses)
|
|
685
|
+
.where(and(eq(pmoWorkflowStatuses.workflowId, workflowId), eq(pmoWorkflowStatuses.isDefault, true)))
|
|
686
|
+
.get();
|
|
633
687
|
if (defaultStatus) {
|
|
634
688
|
newStatusId = defaultStatus.id;
|
|
635
689
|
}
|
|
636
690
|
else {
|
|
637
691
|
// Get first status in workflow
|
|
638
|
-
const firstStatus = this.ctx.
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
692
|
+
const firstStatus = this.ctx.drizzle
|
|
693
|
+
.select({ id: pmoWorkflowStatuses.id })
|
|
694
|
+
.from(pmoWorkflowStatuses)
|
|
695
|
+
.where(eq(pmoWorkflowStatuses.workflowId, workflowId))
|
|
696
|
+
.orderBy(sql `CASE ${pmoWorkflowStatuses.category}
|
|
643
697
|
WHEN 'backlog' THEN 1
|
|
644
698
|
WHEN 'unstarted' THEN 2
|
|
645
699
|
WHEN 'started' THEN 3
|
|
646
700
|
WHEN 'completed' THEN 4
|
|
647
701
|
WHEN 'canceled' THEN 5
|
|
648
|
-
END
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
`).get(workflowId);
|
|
702
|
+
END`, asc(pmoWorkflowStatuses.position))
|
|
703
|
+
.limit(1)
|
|
704
|
+
.get();
|
|
652
705
|
if (firstStatus) {
|
|
653
706
|
newStatusId = firstStatus.id;
|
|
654
707
|
}
|
|
655
708
|
}
|
|
656
709
|
// Get next position for the target status
|
|
657
710
|
const targetStatusId = newStatusId || existing.statusId;
|
|
658
|
-
const maxPos = this.ctx.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
711
|
+
const maxPos = this.ctx.drizzle
|
|
712
|
+
.select({ maxPos: sql `COALESCE(MAX(${pmoTickets.position}), 0)` })
|
|
713
|
+
.from(pmoTickets)
|
|
714
|
+
.where(eq(pmoTickets.statusId, targetStatusId))
|
|
715
|
+
.get();
|
|
716
|
+
const newTicketPosition = (maxPos?.maxPos ?? 0) + 1000;
|
|
662
717
|
// Update ticket's project_id, status_id, and position
|
|
663
718
|
const now = Date.now();
|
|
664
|
-
this.ctx.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
719
|
+
this.ctx.drizzle
|
|
720
|
+
.update(pmoTickets)
|
|
721
|
+
.set({
|
|
722
|
+
projectId: newProjectId,
|
|
723
|
+
statusId: targetStatusId,
|
|
724
|
+
position: newTicketPosition,
|
|
725
|
+
updatedAt: String(now),
|
|
726
|
+
})
|
|
727
|
+
.where(eq(pmoTickets.id, ticketId))
|
|
728
|
+
.run();
|
|
669
729
|
// Update timestamps for both projects
|
|
670
730
|
this.updateProjectTimestamp(oldProjectId);
|
|
671
731
|
this.updateProjectTimestamp(newProjectId);
|