soja-cli 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/finance.js +1 -0
- package/dist/application/intake.js +1 -0
- package/dist/application/services/task-service.js +26 -2
- package/dist/application/services/workspace-service.js +5 -2
- package/dist/application/timeline.js +2 -0
- package/dist/cli/commands/workspace.js +12 -1
- package/dist/cli/output.js +2 -1
- package/dist/data/local/tasks.js +5 -1
- package/dist/data/local/workspaces.js +6 -3
- package/dist/data/sync/engine.js +1 -1
- package/dist/data/sync/finance.js +15 -0
- package/dist/data/sync/index.js +4 -0
- package/dist/data/sync/intake.js +27 -0
- package/dist/data/sync/services.js +7 -2
- package/dist/data/sync/store.js +3 -3
- package/dist/database/migrations/0007_finances.sql +5 -0
- package/dist/database/migrations/0008_finances_designates.sql +44 -0
- package/dist/database/migrations/0009_external_approval.sql +1 -0
- package/dist/database/migrations/meta/0008_snapshot.json +1221 -0
- package/dist/database/migrations/meta/0009_snapshot.json +1228 -0
- package/dist/database/migrations/meta/_journal.json +21 -0
- package/dist/database/schema.js +7 -0
- package/dist/ui/Shell.js +7 -0
- package/dist/ui/chrome/Sidebar.js +1 -1
- package/dist/ui/components/TaskTable.js +1 -1
- package/dist/ui/hooks/use-commands.js +1 -0
- package/dist/ui/hooks/use-task-actions.js +36 -0
- package/dist/ui/screens/FinanceScreen.js +88 -0
- package/dist/ui/screens/HelpScreen.js +9 -0
- package/dist/ui/screens/ProjectsScreen.js +185 -1
- package/dist/ui/screens/TaskScreen.js +9 -1
- package/dist/ui/screens/WorkspacesScreen.js +29 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -30,6 +30,9 @@ const changesSchema = z.object({
|
|
|
30
30
|
status: z.enum(TASK_STATUSES).optional(),
|
|
31
31
|
assigneeId: z.string().nullable().optional(),
|
|
32
32
|
requester: optionalText(80),
|
|
33
|
+
remunerated: z.boolean().optional(),
|
|
34
|
+
priceMinor: z.number().int().nonnegative().nullable().optional(),
|
|
35
|
+
currencyCode: z.string().regex(/^[A-Z]{3}$/).nullable().optional(),
|
|
33
36
|
branch: optionalText(250),
|
|
34
37
|
/** Bookkeeping for merges; changes are not shown in the timeline. */
|
|
35
38
|
baseBranch: optionalText(250),
|
|
@@ -125,6 +128,10 @@ export class TaskService {
|
|
|
125
128
|
assigneeId,
|
|
126
129
|
creatorId: session.user.id,
|
|
127
130
|
requester: data.requester ?? null,
|
|
131
|
+
remunerated: false,
|
|
132
|
+
priceMinor: null,
|
|
133
|
+
externalApprovalStatus: null,
|
|
134
|
+
currencyCode: null,
|
|
128
135
|
branch: null,
|
|
129
136
|
baseBranch: null,
|
|
130
137
|
branchStart: null,
|
|
@@ -164,6 +171,23 @@ export class TaskService {
|
|
|
164
171
|
patch.requester = data.requester;
|
|
165
172
|
events.push({ type: 'task_updated', metadata: { field: 'requester', from: task.requester, to: data.requester } });
|
|
166
173
|
}
|
|
174
|
+
const nextRemunerated = data.remunerated ?? task.remunerated;
|
|
175
|
+
const nextPrice = data.remunerated === false && data.priceMinor === undefined ? null : data.priceMinor === undefined ? task.priceMinor : data.priceMinor;
|
|
176
|
+
const nextCurrency = data.remunerated === false && data.currencyCode === undefined ? null : data.currencyCode === undefined ? task.currencyCode : data.currencyCode;
|
|
177
|
+
if (nextRemunerated && (nextPrice === null || nextCurrency === null))
|
|
178
|
+
throw new ValidationError('A paid task needs a price and currency.');
|
|
179
|
+
if (!nextRemunerated && (nextPrice !== null || nextCurrency !== null))
|
|
180
|
+
throw new ValidationError('Remove the price and currency before marking a task unpaid.');
|
|
181
|
+
if (nextRemunerated !== task.remunerated || nextPrice !== task.priceMinor || nextCurrency !== task.currencyCode) {
|
|
182
|
+
patch.remunerated = nextRemunerated;
|
|
183
|
+
patch.priceMinor = nextPrice;
|
|
184
|
+
patch.currencyCode = nextCurrency;
|
|
185
|
+
events.push({ type: 'task_updated', metadata: {
|
|
186
|
+
field: 'price',
|
|
187
|
+
from: task.priceMinor === null ? null : `${task.priceMinor} ${task.currencyCode ?? ''}`.trim(),
|
|
188
|
+
to: nextPrice === null ? null : `${nextPrice} ${nextCurrency ?? ''}`.trim(),
|
|
189
|
+
} });
|
|
190
|
+
}
|
|
167
191
|
if (changed(data.branch, task.branch)) {
|
|
168
192
|
patch.branch = data.branch;
|
|
169
193
|
events.push({ type: 'task_updated', metadata: { field: 'branch', from: task.branch, to: data.branch } });
|
|
@@ -289,7 +313,7 @@ export class TaskService {
|
|
|
289
313
|
async resolve(session, target) {
|
|
290
314
|
if (typeof target === 'object') {
|
|
291
315
|
const task = await this.repos.tasks.findById(target.id);
|
|
292
|
-
if (task && task.workspaceId === session.workspace.id)
|
|
316
|
+
if (task && task.workspaceId === session.workspace.id && task.externalApprovalStatus !== 'pending' && task.externalApprovalStatus !== 'rejected')
|
|
293
317
|
return task;
|
|
294
318
|
throw new NotFoundError('That task no longer exists.');
|
|
295
319
|
}
|
|
@@ -298,7 +322,7 @@ export class TaskService {
|
|
|
298
322
|
throw new ValidationError(`“${String(target)}” is not a task ID.`, { hint: 'Use something like SOJA-12.' });
|
|
299
323
|
}
|
|
300
324
|
const task = await this.repos.tasks.findByNumber(session.workspace.id, number);
|
|
301
|
-
if (!task)
|
|
325
|
+
if (!task || task.externalApprovalStatus === 'pending' || task.externalApprovalStatus === 'rejected')
|
|
302
326
|
throw new NotFoundError(`${formatTaskRef(number)} does not exist in ${session.workspace.name}.`);
|
|
303
327
|
return task;
|
|
304
328
|
}
|
|
@@ -44,7 +44,7 @@ export class WorkspaceService {
|
|
|
44
44
|
}
|
|
45
45
|
async members(session) {
|
|
46
46
|
const members = await this.repos.workspaces.listMembers(session.workspace.id);
|
|
47
|
-
return members.map(({ id, username, displayName, role }) => ({ id, username, displayName, role }));
|
|
47
|
+
return members.map(({ id, username, displayName, role, designated }) => ({ id, username, displayName, role, designated: role === 'owner' || Boolean(designated) }));
|
|
48
48
|
}
|
|
49
49
|
async findMember(session, username) {
|
|
50
50
|
const normalized = username.trim().replace(/^@/, '').toLowerCase();
|
|
@@ -54,6 +54,9 @@ export class WorkspaceService {
|
|
|
54
54
|
}
|
|
55
55
|
return member;
|
|
56
56
|
}
|
|
57
|
+
async setDesignated(session, userId, designated) {
|
|
58
|
+
await this.repos.workspaces.setDesignated(session.workspace.id, userId, designated);
|
|
59
|
+
}
|
|
57
60
|
/**
|
|
58
61
|
* Adds a developer to the workspace. In local mode developers are plain
|
|
59
62
|
* records (no login), so an unknown username creates one.
|
|
@@ -67,7 +70,7 @@ export class WorkspaceService {
|
|
|
67
70
|
throw new ConflictError(`@${username} is already in ${session.workspace.name}.`);
|
|
68
71
|
}
|
|
69
72
|
await this.repos.workspaces.addMember({ workspaceId: session.workspace.id, userId: user.id, role: 'member' });
|
|
70
|
-
return { id: user.id, username: user.username, displayName: user.displayName, role: 'member' };
|
|
73
|
+
return { id: user.id, username: user.username, displayName: user.displayName, role: 'member', designated: false };
|
|
71
74
|
});
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -74,6 +74,8 @@ export function describe(item, lookups) {
|
|
|
74
74
|
return to ? `set requester to ${to}` : 'cleared the requester';
|
|
75
75
|
case 'branch':
|
|
76
76
|
return to ? `linked branch ${to}` : 'unlinked the branch';
|
|
77
|
+
case 'price':
|
|
78
|
+
return to ? `set ticket price to ${to}` : 'removed the ticket price';
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
}
|
|
@@ -13,7 +13,9 @@ export async function workspaceCommand(args) {
|
|
|
13
13
|
return create(rest);
|
|
14
14
|
if (sub === 'add' || sub === 'add-member')
|
|
15
15
|
return addMember(rest);
|
|
16
|
-
|
|
16
|
+
if (sub === 'designate')
|
|
17
|
+
return designate(rest);
|
|
18
|
+
throw new ValidationError(`Unknown workspace command “${sub}”.`, { hint: 'Try: list, create <name>, add <username>, designate <username>.' });
|
|
17
19
|
}
|
|
18
20
|
async function list() {
|
|
19
21
|
await withSession(async (services, session) => {
|
|
@@ -53,3 +55,12 @@ async function addMember(args) {
|
|
|
53
55
|
success(`@${member.username} joined ${bold(session.workspace.name)}`);
|
|
54
56
|
});
|
|
55
57
|
}
|
|
58
|
+
async function designate(args) {
|
|
59
|
+
const { values, positionals } = parseCommand(args, { remove: { type: 'boolean', short: 'r' } });
|
|
60
|
+
const username = requireArg(positionals[0], 'workspace member', 'soja workspace designate angel');
|
|
61
|
+
await withSession(async (services, session) => {
|
|
62
|
+
const member = await services.workspaces.findMember(session, username);
|
|
63
|
+
await services.workspaces.setDesignated(session, member.id, !values.remove);
|
|
64
|
+
success(`${values.remove ? 'Removed designate access from' : 'Designated'} @${member.username} ${values.remove ? 'in ' + bold(session.workspace.name) : 'for ' + bold(session.workspace.name)}`);
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/cli/output.js
CHANGED
|
@@ -51,6 +51,7 @@ export function taskLine(task, options = {}, columns = process.stdout.columns ||
|
|
|
51
51
|
used += 11;
|
|
52
52
|
}
|
|
53
53
|
const room = Math.max(12, columns - used - 2);
|
|
54
|
-
|
|
54
|
+
const title = `${task.remunerated ? '$ ' : ''}${task.title}`;
|
|
55
|
+
parts.push(title.length > room ? `${title.slice(0, room - 1)}${symbols.ellipsis}` : title);
|
|
55
56
|
return ` ${parts.join('')}`;
|
|
56
57
|
}
|
package/dist/data/local/tasks.js
CHANGED
|
@@ -44,6 +44,9 @@ export class LocalTaskRepository {
|
|
|
44
44
|
}
|
|
45
45
|
async list(query) {
|
|
46
46
|
const conditions = [eq(tasks.workspaceId, query.workspaceId)];
|
|
47
|
+
const externallyVisible = or(isNull(tasks.externalApprovalStatus), eq(tasks.externalApprovalStatus, 'approved'));
|
|
48
|
+
if (externallyVisible)
|
|
49
|
+
conditions.push(externallyVisible);
|
|
47
50
|
if (query.assigneeId)
|
|
48
51
|
conditions.push(eq(tasks.assigneeId, query.assigneeId));
|
|
49
52
|
if (query.projectId)
|
|
@@ -79,10 +82,11 @@ export class LocalTaskRepository {
|
|
|
79
82
|
return task;
|
|
80
83
|
}
|
|
81
84
|
async countByProjectAndStatus(workspaceId) {
|
|
85
|
+
const externallyVisible = or(isNull(tasks.externalApprovalStatus), eq(tasks.externalApprovalStatus, 'approved'));
|
|
82
86
|
return this.db
|
|
83
87
|
.select({ projectId: tasks.projectId, status: tasks.status, count: count() })
|
|
84
88
|
.from(tasks)
|
|
85
|
-
.where(and(eq(tasks.workspaceId, workspaceId), isNull(tasks.archivedAt)))
|
|
89
|
+
.where(and(eq(tasks.workspaceId, workspaceId), isNull(tasks.archivedAt), ...(externallyVisible ? [externallyVisible] : [])))
|
|
86
90
|
.groupBy(tasks.projectId, tasks.status);
|
|
87
91
|
}
|
|
88
92
|
async delete(id) {
|
|
@@ -25,7 +25,7 @@ export class LocalWorkspaceRepository {
|
|
|
25
25
|
}
|
|
26
26
|
async listForUser(userId) {
|
|
27
27
|
const rows = await this.db
|
|
28
|
-
.select({ workspace: workspaces, role: workspaceMembers.role })
|
|
28
|
+
.select({ workspace: workspaces, role: workspaceMembers.role, designated: workspaceMembers.designated })
|
|
29
29
|
.from(workspaceMembers)
|
|
30
30
|
.innerJoin(workspaces, eq(workspaces.id, workspaceMembers.workspaceId))
|
|
31
31
|
.where(eq(workspaceMembers.userId, userId))
|
|
@@ -45,11 +45,14 @@ export class LocalWorkspaceRepository {
|
|
|
45
45
|
}
|
|
46
46
|
async listMembers(workspaceId) {
|
|
47
47
|
const rows = await this.db
|
|
48
|
-
.select({ user: users, role: workspaceMembers.role })
|
|
48
|
+
.select({ user: users, role: workspaceMembers.role, designated: workspaceMembers.designated })
|
|
49
49
|
.from(workspaceMembers)
|
|
50
50
|
.innerJoin(users, eq(users.id, workspaceMembers.userId))
|
|
51
51
|
.where(eq(workspaceMembers.workspaceId, workspaceId))
|
|
52
52
|
.orderBy(asc(users.username));
|
|
53
|
-
return rows.map((row) => ({ ...row.user, role: row.role }));
|
|
53
|
+
return rows.map((row) => ({ ...row.user, role: row.role, designated: row.role === 'owner' || row.designated }));
|
|
54
|
+
}
|
|
55
|
+
async setDesignated(workspaceId, userId, designated) {
|
|
56
|
+
await this.db.update(workspaceMembers).set({ designated }).where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.userId, userId)));
|
|
54
57
|
}
|
|
55
58
|
}
|
package/dist/data/sync/engine.js
CHANGED
|
@@ -3,7 +3,7 @@ import { formatTaskRef, isProvisional, TASK_REF_PREFIX } from '../../domain/task
|
|
|
3
3
|
import { OfflineError } from '../remote/api-client.js';
|
|
4
4
|
import { isChatOp } from './store.js';
|
|
5
5
|
const BATCH = 100;
|
|
6
|
-
const TASK_FIELDS = new Set(['title', 'description', 'projectId', 'type', 'priority', 'status', 'assigneeId', 'requester', 'branch', 'baseBranch', 'branchStart']);
|
|
6
|
+
const TASK_FIELDS = new Set(['title', 'description', 'projectId', 'type', 'priority', 'status', 'assigneeId', 'requester', 'remunerated', 'priceMinor', 'currencyCode', 'branch', 'baseBranch', 'branchStart']);
|
|
7
7
|
/** Server task view → stored task row (presentation fields are dropped by the store). */
|
|
8
8
|
const toTask = (view) => view;
|
|
9
9
|
const emptyReport = () => ({
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class ReplicaFinanceService {
|
|
2
|
+
context;
|
|
3
|
+
constructor(context) {
|
|
4
|
+
this.context = context;
|
|
5
|
+
}
|
|
6
|
+
summary(session) {
|
|
7
|
+
return this.context.api.get(`/v1/workspaces/${session.workspace.id}/finance`);
|
|
8
|
+
}
|
|
9
|
+
async configureCurrency(session, currencyCode) {
|
|
10
|
+
await this.context.api.request('PUT', `/v1/workspaces/${session.workspace.id}/finance/currency`, { currencyCode });
|
|
11
|
+
}
|
|
12
|
+
async setMySalary(session, input) {
|
|
13
|
+
await this.context.api.request('PUT', `/v1/workspaces/${session.workspace.id}/finance/salary`, input);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/data/sync/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { TaskService } from '../../application/services/task-service.js';
|
|
|
7
7
|
import { createLocalRepositories } from '../local/index.js';
|
|
8
8
|
import { LiveConnection } from '../remote/live.js';
|
|
9
9
|
import { ReplicaChatService } from './chat.js';
|
|
10
|
+
import { ReplicaFinanceService } from './finance.js';
|
|
11
|
+
import { ReplicaIntakeService } from './intake.js';
|
|
10
12
|
import { LocalImporter } from './importer.js';
|
|
11
13
|
import { SyncEngine } from './engine.js';
|
|
12
14
|
import { ReplicaProjectService, ReplicaSessionService, ReplicaTaskService, ReplicaWorkspaceService } from './services.js';
|
|
@@ -83,6 +85,8 @@ export function createReplicaServices(api, handle, config, git, gitConsole, WebS
|
|
|
83
85
|
projects,
|
|
84
86
|
tasks,
|
|
85
87
|
chat: new ReplicaChatService(context, tasks),
|
|
88
|
+
finance: new ReplicaFinanceService(context),
|
|
89
|
+
intake: new ReplicaIntakeService(context),
|
|
86
90
|
...(localDatabaseFile ? { importer: new LocalImporter(localDatabaseFile, config, api, store, engine) } : {}),
|
|
87
91
|
git: new GitWorkflowService(tasks, projects, git),
|
|
88
92
|
folders: new FolderService(config),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class ReplicaIntakeService {
|
|
2
|
+
context;
|
|
3
|
+
constructor(context) {
|
|
4
|
+
this.context = context;
|
|
5
|
+
}
|
|
6
|
+
configure(session, projectId, input) {
|
|
7
|
+
return this.context.api.request('PUT', `${this.base(session)}/projects/${projectId}/intake`, input);
|
|
8
|
+
}
|
|
9
|
+
rotateSecret(session, projectId) {
|
|
10
|
+
return this.context.api.request('POST', `${this.base(session)}/projects/${projectId}/intake/secret`);
|
|
11
|
+
}
|
|
12
|
+
exportContract(session, projectId) {
|
|
13
|
+
return this.context.api.get(`${this.base(session)}/projects/${projectId}/intake/export`);
|
|
14
|
+
}
|
|
15
|
+
reviewQueue(session, projectId) {
|
|
16
|
+
return this.context.api.get(`${this.base(session)}/projects/${projectId}/intake/requests`);
|
|
17
|
+
}
|
|
18
|
+
decide(session, projectId, taskId, input) {
|
|
19
|
+
return this.context.api.request('POST', `${this.base(session)}/projects/${projectId}/intake/requests/${taskId}/decision`, input);
|
|
20
|
+
}
|
|
21
|
+
async revoke(session, projectId) {
|
|
22
|
+
await this.context.api.delete(`${this.base(session)}/projects/${projectId}/intake`);
|
|
23
|
+
}
|
|
24
|
+
base(session) {
|
|
25
|
+
return `/v1/workspaces/${session.workspace.id}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -102,7 +102,7 @@ export class ReplicaWorkspaceService {
|
|
|
102
102
|
}
|
|
103
103
|
async members(session) {
|
|
104
104
|
const members = await this.context.repos.workspaces.listMembers(session.workspace.id);
|
|
105
|
-
return members.map(({ id, username, displayName, role }) => ({ id, username, displayName, role }));
|
|
105
|
+
return members.map(({ id, username, displayName, role, designated }) => ({ id, username, displayName, role, designated: role === 'owner' || Boolean(designated) }));
|
|
106
106
|
}
|
|
107
107
|
async findMember(session, username) {
|
|
108
108
|
const normalized = username.trim().replace(/^@/, '').toLowerCase();
|
|
@@ -116,6 +116,11 @@ export class ReplicaWorkspaceService {
|
|
|
116
116
|
await this.context.store.upsertMember(session.workspace.id, member);
|
|
117
117
|
return member;
|
|
118
118
|
}
|
|
119
|
+
async setDesignated(session, userId, designated) {
|
|
120
|
+
const members = await online('Updating workspace designates', () => this.context.api.request(designated ? 'POST' : 'DELETE', `/v1/workspaces/${session.workspace.id}/designates/${userId}`));
|
|
121
|
+
for (const member of members)
|
|
122
|
+
await this.context.store.upsertMember(session.workspace.id, member);
|
|
123
|
+
}
|
|
119
124
|
}
|
|
120
125
|
export class ReplicaProjectService {
|
|
121
126
|
context;
|
|
@@ -166,7 +171,7 @@ export class ReplicaProjectService {
|
|
|
166
171
|
return this.local.get(session, project.id);
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
|
-
const CHANGE_FIELDS = ['title', 'description', 'projectId', 'type', 'priority', 'status', 'assigneeId', 'requester', 'branch', 'baseBranch', 'branchStart'];
|
|
174
|
+
const CHANGE_FIELDS = ['title', 'description', 'projectId', 'type', 'priority', 'status', 'assigneeId', 'requester', 'remunerated', 'priceMinor', 'currencyCode', 'branch', 'baseBranch', 'branchStart'];
|
|
170
175
|
/**
|
|
171
176
|
* Tasks in remote mode: read and written in the local replica (instant, works
|
|
172
177
|
* offline), with every write queued as an operation for the server.
|
package/dist/data/sync/store.js
CHANGED
|
@@ -4,7 +4,7 @@ export const isChatOp = (type) => !type.startsWith('task.');
|
|
|
4
4
|
/** Task fields as stored; server views carry extra presentation fields we drop. */
|
|
5
5
|
const TASK_COLUMNS = [
|
|
6
6
|
'id', 'number', 'workspaceId', 'projectId', 'title', 'description', 'type', 'priority', 'status', 'assigneeId',
|
|
7
|
-
'creatorId', 'requester', 'branch', 'baseBranch', 'branchStart', 'createdAt', 'updatedAt', 'startedAt', 'completedAt', 'archivedAt',
|
|
7
|
+
'creatorId', 'requester', 'remunerated', 'priceMinor', 'currencyCode', 'externalApprovalStatus', 'branch', 'baseBranch', 'branchStart', 'createdAt', 'updatedAt', 'startedAt', 'completedAt', 'archivedAt',
|
|
8
8
|
];
|
|
9
9
|
const pickTask = (task) => Object.fromEntries(TASK_COLUMNS.map((column) => [column, task[column]]));
|
|
10
10
|
/**
|
|
@@ -30,8 +30,8 @@ export class ReplicaStore {
|
|
|
30
30
|
await this.upsertUser(member);
|
|
31
31
|
await this.db
|
|
32
32
|
.insert(workspaceMembers)
|
|
33
|
-
.values({ workspaceId, userId: member.id, role: member.role })
|
|
34
|
-
.onConflictDoUpdate({ target: [workspaceMembers.workspaceId, workspaceMembers.userId], set: { role: member.role } });
|
|
33
|
+
.values({ workspaceId, userId: member.id, role: member.role, designated: member.designated ?? member.role === 'owner' })
|
|
34
|
+
.onConflictDoUpdate({ target: [workspaceMembers.workspaceId, workspaceMembers.userId], set: { role: member.role, designated: member.designated ?? member.role === 'owner' } });
|
|
35
35
|
}
|
|
36
36
|
/** Server projects have no local path; the one this machine linked is kept. */
|
|
37
37
|
async upsertProject(project) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
ALTER TABLE tasks ADD COLUMN remunerated INTEGER NOT NULL DEFAULT 0 CHECK (remunerated IN (0, 1));--> statement-breakpoint
|
|
2
|
+
ALTER TABLE tasks ADD COLUMN price_minor INTEGER CHECK (price_minor IS NULL OR price_minor >= 0);--> statement-breakpoint
|
|
3
|
+
ALTER TABLE tasks ADD COLUMN currency_code TEXT CHECK (currency_code IS NULL OR currency_code GLOB '[A-Z][A-Z][A-Z]');
|
|
4
|
+
--> statement-breakpoint
|
|
5
|
+
ALTER TABLE workspace_members ADD COLUMN designated INTEGER NOT NULL DEFAULT 0 CHECK (designated IN (0, 1));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
2
|
+
CREATE TABLE `__new_tasks` (
|
|
3
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
4
|
+
`number` integer NOT NULL,
|
|
5
|
+
`workspace_id` text NOT NULL,
|
|
6
|
+
`project_id` text,
|
|
7
|
+
`title` text NOT NULL,
|
|
8
|
+
`description` text,
|
|
9
|
+
`type` text NOT NULL,
|
|
10
|
+
`priority` text NOT NULL,
|
|
11
|
+
`status` text NOT NULL,
|
|
12
|
+
`assignee_id` text,
|
|
13
|
+
`creator_id` text NOT NULL,
|
|
14
|
+
`requester` text,
|
|
15
|
+
`remunerated` integer DEFAULT false NOT NULL,
|
|
16
|
+
`price_minor` integer,
|
|
17
|
+
`currency_code` text,
|
|
18
|
+
`branch` text,
|
|
19
|
+
`base_branch` text,
|
|
20
|
+
`branch_start` text,
|
|
21
|
+
`created_at` integer NOT NULL,
|
|
22
|
+
`updated_at` integer NOT NULL,
|
|
23
|
+
`started_at` integer,
|
|
24
|
+
`completed_at` integer,
|
|
25
|
+
`archived_at` integer,
|
|
26
|
+
FOREIGN KEY (`workspace_id`) REFERENCES `workspaces`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
27
|
+
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE set null,
|
|
28
|
+
FOREIGN KEY (`assignee_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null,
|
|
29
|
+
FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
|
|
30
|
+
CONSTRAINT "tasks_type" CHECK(type IN ('bug', 'feature', 'improvement', 'maintenance', 'infra', 'refactor', 'research', 'chore')),
|
|
31
|
+
CONSTRAINT "tasks_priority" CHECK(priority IN ('none', 'low', 'medium', 'high', 'urgent')),
|
|
32
|
+
CONSTRAINT "tasks_status" CHECK(status IN ('backlog', 'todo', 'in_progress', 'review', 'blocked', 'done', 'cancelled')),
|
|
33
|
+
CONSTRAINT "tasks_price_nonnegative" CHECK(price_minor IS NULL OR price_minor >= 0),
|
|
34
|
+
CONSTRAINT "tasks_remuneration_fields" CHECK((remunerated = 0 AND price_minor IS NULL AND currency_code IS NULL) OR (remunerated = 1 AND price_minor IS NOT NULL AND currency_code IS NOT NULL))
|
|
35
|
+
);
|
|
36
|
+
--> statement-breakpoint
|
|
37
|
+
INSERT INTO `__new_tasks`("id", "number", "workspace_id", "project_id", "title", "description", "type", "priority", "status", "assignee_id", "creator_id", "requester", "remunerated", "price_minor", "currency_code", "branch", "base_branch", "branch_start", "created_at", "updated_at", "started_at", "completed_at", "archived_at") SELECT "id", "number", "workspace_id", "project_id", "title", "description", "type", "priority", "status", "assignee_id", "creator_id", "requester", "remunerated", "price_minor", "currency_code", "branch", "base_branch", "branch_start", "created_at", "updated_at", "started_at", "completed_at", "archived_at" FROM `tasks`;--> statement-breakpoint
|
|
38
|
+
DROP TABLE `tasks`;--> statement-breakpoint
|
|
39
|
+
ALTER TABLE `__new_tasks` RENAME TO `tasks`;--> statement-breakpoint
|
|
40
|
+
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
|
41
|
+
CREATE UNIQUE INDEX `tasks_workspace_number` ON `tasks` (`workspace_id`,`number`);--> statement-breakpoint
|
|
42
|
+
CREATE INDEX `tasks_workspace_status` ON `tasks` (`workspace_id`,`status`);--> statement-breakpoint
|
|
43
|
+
CREATE INDEX `tasks_assignee` ON `tasks` (`assignee_id`);--> statement-breakpoint
|
|
44
|
+
CREATE INDEX `tasks_project` ON `tasks` (`project_id`);--> statement-breakpoint
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `tasks` ADD `external_approval_status` text;
|