Files
testapi/src/repository.ts

449 lines
10 KiB
TypeScript

import type { Database } from "bun:sqlite";
interface TaskFilters {
orgId?: string | null;
projectId?: string | null;
status?: string | null;
priority?: string | null;
assigneeUserId?: string | null;
}
function toCamelCaseTask(row: Record<string, unknown>) {
return {
id: row.id,
orgId: row.org_id,
projectId: row.project_id,
assigneeUserId: row.assignee_user_id,
title: row.title,
description: row.description,
status: row.status,
priority: row.priority,
storyPoints: row.story_points,
dueDate: row.due_date,
createdAt: row.created_at,
updatedAt: row.updated_at,
projectName: row.project_name,
projectKey: row.project_key,
assigneeName: row.assignee_name,
};
}
function getUser(db: Database, userId: string) {
return db
.query(`
SELECT
id,
org_id AS orgId,
full_name AS fullName,
email,
role,
timezone
FROM users
WHERE id = ?
`)
.get(userId);
}
function getProject(db: Database, projectId: string) {
return db
.query(`
SELECT
p.id,
p.org_id AS orgId,
p.key,
p.name,
p.status,
p.owner_user_id AS ownerUserId,
p.due_date AS dueDate,
u.full_name AS ownerName,
COUNT(t.id) AS taskCount
FROM projects p
LEFT JOIN users u ON u.id = p.owner_user_id
LEFT JOIN tasks t ON t.project_id = p.id
WHERE p.id = ?
GROUP BY p.id
`)
.get(projectId);
}
export function listOrganizations(db: Database) {
return db
.query(`
SELECT
o.id,
o.slug,
o.name,
o.plan,
o.industry,
o.created_at AS createdAt,
COUNT(DISTINCT p.id) AS projectCount,
COUNT(DISTINCT t.id) AS taskCount,
COUNT(DISTINCT CASE WHEN t.status != 'done' THEN t.id END) AS openTaskCount
FROM organizations o
LEFT JOIN projects p ON p.org_id = o.id
LEFT JOIN tasks t ON t.org_id = o.id
GROUP BY o.id
ORDER BY o.name
`)
.all();
}
export function getOrganizationSummary(db: Database, orgId: string) {
const org = db
.query(`
SELECT
o.id,
o.slug,
o.name,
o.plan,
o.industry,
o.created_at AS createdAt,
COUNT(DISTINCT u.id) AS userCount,
COUNT(DISTINCT p.id) AS projectCount,
COUNT(DISTINCT t.id) AS taskCount
FROM organizations o
LEFT JOIN users u ON u.org_id = o.id
LEFT JOIN projects p ON p.org_id = o.id
LEFT JOIN tasks t ON t.org_id = o.id
WHERE o.id = ?
GROUP BY o.id
`)
.get(orgId);
if (!org) {
return null;
}
const statusBreakdown = db
.query(`
SELECT status, COUNT(*) AS count
FROM tasks
WHERE org_id = ?
GROUP BY status
ORDER BY count DESC, status ASC
`)
.all(orgId);
return {
...org,
statusBreakdown,
};
}
export interface CreateOrganizationInput {
slug: string;
name: string;
plan: string;
industry: string;
}
export function createOrganization(db: Database, input: CreateOrganizationInput) {
const id = `org_${Math.random().toString(36).slice(2, 10)}`;
const now = new Date().toISOString();
db.query(`
INSERT INTO organizations (id, slug, name, plan, industry, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(id, input.slug, input.name, input.plan, input.industry, now);
return getOrganizationSummary(db, id);
}
export function listUsers(db: Database, orgId?: string | null) {
if (orgId) {
return db
.query(`
SELECT
id,
org_id AS orgId,
full_name AS fullName,
email,
role,
timezone
FROM users
WHERE org_id = ?
ORDER BY full_name
`)
.all(orgId);
}
return db
.query(`
SELECT
id,
org_id AS orgId,
full_name AS fullName,
email,
role,
timezone
FROM users
ORDER BY org_id, full_name
`)
.all();
}
export interface CreateUserInput {
orgId: string;
fullName: string;
email: string;
role: string;
timezone: string;
}
export function createUser(db: Database, input: CreateUserInput) {
const id = `user_${Math.random().toString(36).slice(2, 10)}`;
db.query(`
INSERT INTO users (id, org_id, full_name, email, role, timezone)
VALUES (?, ?, ?, ?, ?, ?)
`).run(id, input.orgId, input.fullName, input.email, input.role, input.timezone);
return getUser(db, id);
}
export function listProjects(db: Database, orgId?: string | null) {
const sql = `
SELECT
p.id,
p.org_id AS orgId,
p.key,
p.name,
p.status,
p.owner_user_id AS ownerUserId,
p.due_date AS dueDate,
u.full_name AS ownerName,
COUNT(t.id) AS taskCount
FROM projects p
LEFT JOIN users u ON u.id = p.owner_user_id
LEFT JOIN tasks t ON t.project_id = p.id
${orgId ? "WHERE p.org_id = ?" : ""}
GROUP BY p.id
ORDER BY p.name
`;
return orgId ? db.query(sql).all(orgId) : db.query(sql).all();
}
export interface CreateProjectInput {
orgId: string;
key: string;
name: string;
status: string;
ownerUserId: string;
dueDate?: string | null;
}
export function createProject(db: Database, input: CreateProjectInput) {
const id = `proj_${Math.random().toString(36).slice(2, 10)}`;
db.query(`
INSERT INTO projects (id, org_id, key, name, status, owner_user_id, due_date)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.key,
input.name,
input.status,
input.ownerUserId,
input.dueDate ?? null,
);
return getProject(db, id);
}
export function listTasks(db: Database, filters: TaskFilters = {}) {
const conditions: string[] = [];
const values: string[] = [];
if (filters.orgId) {
conditions.push("t.org_id = ?");
values.push(filters.orgId);
}
if (filters.projectId) {
conditions.push("t.project_id = ?");
values.push(filters.projectId);
}
if (filters.status) {
conditions.push("t.status = ?");
values.push(filters.status);
}
if (filters.priority) {
conditions.push("t.priority = ?");
values.push(filters.priority);
}
if (filters.assigneeUserId) {
conditions.push("t.assignee_user_id = ?");
values.push(filters.assigneeUserId);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const rows = db
.query(`
SELECT
t.*,
p.name AS project_name,
p.key AS project_key,
u.full_name AS assignee_name
FROM tasks t
JOIN projects p ON p.id = t.project_id
LEFT JOIN users u ON u.id = t.assignee_user_id
${whereClause}
ORDER BY
CASE t.priority
WHEN 'urgent' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
ELSE 4
END,
t.due_date ASC,
t.updated_at DESC
`)
.all(...values) as Record<string, unknown>[];
return rows.map(toCamelCaseTask);
}
export function getTaskDetail(db: Database, taskId: string) {
const row = db
.query(`
SELECT
t.*,
p.name AS project_name,
p.key AS project_key,
u.full_name AS assignee_name
FROM tasks t
JOIN projects p ON p.id = t.project_id
LEFT JOIN users u ON u.id = t.assignee_user_id
WHERE t.id = ?
`)
.get(taskId) as Record<string, unknown> | null;
if (!row) {
return null;
}
const labels = db
.query(`
SELECT l.id, l.name, l.color
FROM task_labels tl
JOIN labels l ON l.id = tl.label_id
WHERE tl.task_id = ?
ORDER BY l.name
`)
.all(taskId);
const comments = db
.query(`
SELECT
c.id,
c.body,
c.created_at AS createdAt,
c.author_user_id AS authorUserId,
u.full_name AS authorName
FROM comments c
JOIN users u ON u.id = c.author_user_id
WHERE c.task_id = ?
ORDER BY c.created_at ASC
`)
.all(taskId);
return {
...toCamelCaseTask(row),
labels,
comments,
};
}
export interface CreateTaskInput {
orgId: string;
projectId: string;
assigneeUserId?: string | null;
title: string;
description?: string;
status?: string;
priority?: string;
storyPoints?: number | null;
dueDate?: string | null;
}
export function createTask(db: Database, input: CreateTaskInput) {
const now = new Date().toISOString();
const id = `task_${Math.random().toString(36).slice(2, 10)}`;
db.query(`
INSERT INTO tasks (
id, org_id, project_id, assignee_user_id, title, description, status, priority, story_points, due_date, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.projectId,
input.assigneeUserId ?? null,
input.title,
input.description ?? "",
input.status ?? "todo",
input.priority ?? "medium",
input.storyPoints ?? null,
input.dueDate ?? null,
now,
now,
);
return getTaskDetail(db, id);
}
export interface UpdateTaskInput {
title?: string;
description?: string;
status?: string;
priority?: string;
assigneeUserId?: string | null;
storyPoints?: number | null;
dueDate?: string | null;
}
export function updateTask(db: Database, taskId: string, input: UpdateTaskInput) {
const existing = db.query("SELECT id FROM tasks WHERE id = ?").get(taskId);
if (!existing) {
return null;
}
const fields: string[] = [];
const values: Array<string | number | null> = [];
if (input.title !== undefined) {
fields.push("title = ?");
values.push(input.title);
}
if (input.description !== undefined) {
fields.push("description = ?");
values.push(input.description);
}
if (input.status !== undefined) {
fields.push("status = ?");
values.push(input.status);
}
if (input.priority !== undefined) {
fields.push("priority = ?");
values.push(input.priority);
}
if (input.assigneeUserId !== undefined) {
fields.push("assignee_user_id = ?");
values.push(input.assigneeUserId);
}
if (input.storyPoints !== undefined) {
fields.push("story_points = ?");
values.push(input.storyPoints);
}
if (input.dueDate !== undefined) {
fields.push("due_date = ?");
values.push(input.dueDate);
}
fields.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(taskId);
db.query(`UPDATE tasks SET ${fields.join(", ")} WHERE id = ?`).run(...values);
return getTaskDetail(db, taskId);
}