Files
cobble/src/functions/backend/db/tasks/create-task.ts
T
tbenandClaude Opus 5.5 623112fd19 Give every task a bot: drop shared workspace tasks
A workspace task with no bot was seen by every bot working there. Now a
task can't be saved without a bot, a worker reaches only its own (the
workspace admin and the coordinator still reach all), the workspace page
picks who has a new task, and a bot leaving a workspace or deleted passes
its tasks there to the workspace's admin bot, else the coordinator (also
at boot for rows without a bot).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:26:44 +01:00

95 lines
3.3 KiB
TypeScript

import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_TASKS, BunSQLiteTables } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import type { TaskStatus } from "@/src/dict/task-dict";
import type { TaskDetail } from "@/src/types";
import AppError from "@/src/utils/app-error";
import grabCrewRole from "../access/grab-crew-role";
import grabTask from "./grab-task";
import assertBotWorksThere from "./assert-bot-works-there";
import taskScopeValues from "./task-scope-values";
type Params = {
/**
* The user whose run or request creates it.
*/
user_id: number | string;
bot_id?: number | string | null;
workspace_id?: number | string | null;
title: string;
description?: string;
status?: TaskStatus;
/**
* The bot creating it with its tool; none for a user's manual add.
*/
created_by_bot_id?: number | string | null;
};
/**
* Adds a task to a bot's own list or a workspace's, there for one of its
* bots (`bot_id` too): every task belongs to a bot. A user adds one to a bot they chat with or
* a workspace they're in; a bot's run adds one to its own list or to a
* workspace it works in (its tool checks that). A workspace's task can
* only go to a bot working there. Caps a list at `TaskMaxPerScope` open and
* finished tasks.
*/
export default async function createTask({
user_id,
bot_id,
workspace_id,
title,
description,
status,
created_by_bot_id,
}: Params): Promise<TaskDetail> {
const scope = taskScopeValues({ bot_id, workspace_id });
const role = await grabCrewRole({ user_id });
if (!created_by_bot_id && role == "member") {
throw new AppError(`Only an admin can add a task.`);
}
if (scope.bot_id && scope.workspace_id) {
await assertBotWorksThere({ bot_id: scope.bot_id, workspace_id: scope.workspace_id });
}
const count = Number(
(
await BunSQLite.sql<{ n: number }>({
sql: `SELECT COUNT(*) AS n FROM tasks WHERE ${
scope.workspace_id ? `workspace_id = ?` : `bot_id = ? AND workspace_id IS NULL`
}`,
values: [Number(scope.workspace_id || scope.bot_id)],
})
).payload?.[0]?.n || 0,
);
if (count >= AppData.TaskMaxPerScope) {
throw new AppError(`That list is full (${AppData.TaskMaxPerScope} tasks). Finish or delete one first.`);
}
const res = await BunSQLite.insert<BUN_SQLITE_COBBLE_TASKS, (typeof BunSQLiteTables)[number]>({
table: "tasks",
data: [
{
bot_id: scope.bot_id || undefined,
workspace_id: scope.workspace_id || undefined,
title: title.trim().slice(0, AppData.TaskTitleMaxLength),
description: description || undefined,
status: status || "todo",
origin: created_by_bot_id ? "bot" : "user",
created_by_bot_id: created_by_bot_id ? Number(created_by_bot_id) : undefined,
created_by_user_id: Number(user_id),
},
],
});
const task_id = res.singleRes?.id || res.postInsertReturn?.insertId;
if (!res.success || !task_id) {
throw new Error(`Couldn't save the task: ${res.msg}`);
}
return await grabTask({ user_id, task_id });
}