bopodev-db 0.1.27 → 0.1.29

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.
@@ -0,0 +1,389 @@
1
+ CREATE TABLE "companies" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "name" text NOT NULL,
4
+ "mission" text,
5
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
6
+ );
7
+ --> statement-breakpoint
8
+ CREATE TABLE "projects" (
9
+ "id" text PRIMARY KEY NOT NULL,
10
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
11
+ "name" text NOT NULL,
12
+ "description" text,
13
+ "status" text DEFAULT 'planned' NOT NULL,
14
+ "planned_start_at" timestamp,
15
+ "monthly_budget_usd" numeric(12, 4) DEFAULT 100 NOT NULL,
16
+ "used_budget_usd" numeric(12, 4) DEFAULT 0 NOT NULL,
17
+ "budget_window_start_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
18
+ "execution_workspace_policy" text,
19
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
20
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE TABLE "project_workspaces" (
24
+ "id" text PRIMARY KEY NOT NULL,
25
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
26
+ "project_id" text NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE,
27
+ "name" text NOT NULL,
28
+ "cwd" text,
29
+ "repo_url" text,
30
+ "repo_ref" text,
31
+ "is_primary" boolean DEFAULT false NOT NULL,
32
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
33
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
34
+ );
35
+ --> statement-breakpoint
36
+ CREATE TABLE "goals" (
37
+ "id" text PRIMARY KEY NOT NULL,
38
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
39
+ "project_id" text REFERENCES "projects"("id") ON DELETE SET NULL,
40
+ "parent_goal_id" text,
41
+ "level" text NOT NULL,
42
+ "title" text NOT NULL,
43
+ "description" text,
44
+ "status" text DEFAULT 'draft' NOT NULL,
45
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
46
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
47
+ );
48
+ --> statement-breakpoint
49
+ CREATE TABLE "agents" (
50
+ "id" text PRIMARY KEY NOT NULL,
51
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
52
+ "manager_agent_id" text,
53
+ "role" text NOT NULL,
54
+ "role_key" text,
55
+ "title" text,
56
+ "name" text NOT NULL,
57
+ "provider_type" text NOT NULL,
58
+ "status" text DEFAULT 'idle' NOT NULL,
59
+ "heartbeat_cron" text NOT NULL,
60
+ "monthly_budget_usd" numeric(12, 4) DEFAULT 0 NOT NULL,
61
+ "used_budget_usd" numeric(12, 4) DEFAULT 0 NOT NULL,
62
+ "token_usage" integer DEFAULT 0 NOT NULL,
63
+ "can_hire_agents" boolean DEFAULT false NOT NULL,
64
+ "avatar_seed" text DEFAULT '' NOT NULL,
65
+ "runtime_command" text,
66
+ "runtime_args_json" text DEFAULT '[]' NOT NULL,
67
+ "runtime_cwd" text,
68
+ "runtime_env_json" text DEFAULT '{}' NOT NULL,
69
+ "runtime_model" text,
70
+ "runtime_thinking_effort" text DEFAULT 'auto' NOT NULL,
71
+ "bootstrap_prompt" text,
72
+ "runtime_timeout_sec" integer DEFAULT 0 NOT NULL,
73
+ "interrupt_grace_sec" integer DEFAULT 15 NOT NULL,
74
+ "run_policy_json" text DEFAULT '{}' NOT NULL,
75
+ "state_blob" text DEFAULT '{}' NOT NULL,
76
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
77
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
78
+ );
79
+ --> statement-breakpoint
80
+ CREATE TABLE "issues" (
81
+ "id" text PRIMARY KEY NOT NULL,
82
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
83
+ "project_id" text NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE,
84
+ "parent_issue_id" text,
85
+ "title" text NOT NULL,
86
+ "body" text,
87
+ "status" text DEFAULT 'todo' NOT NULL,
88
+ "priority" text DEFAULT 'none' NOT NULL,
89
+ "assignee_agent_id" text,
90
+ "labels_json" text DEFAULT '[]' NOT NULL,
91
+ "tags_json" text DEFAULT '[]' NOT NULL,
92
+ "is_claimed" boolean DEFAULT false NOT NULL,
93
+ "claimed_by_heartbeat_run_id" text,
94
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
95
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
96
+ );
97
+ --> statement-breakpoint
98
+ CREATE TABLE "issue_comments" (
99
+ "id" text PRIMARY KEY NOT NULL,
100
+ "issue_id" text NOT NULL REFERENCES "issues"("id") ON DELETE CASCADE,
101
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
102
+ "author_type" text NOT NULL,
103
+ "author_id" text,
104
+ "recipients_json" text DEFAULT '[]' NOT NULL,
105
+ "run_id" text,
106
+ "body" text NOT NULL,
107
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
108
+ );
109
+ --> statement-breakpoint
110
+ CREATE TABLE "issue_attachments" (
111
+ "id" text PRIMARY KEY NOT NULL,
112
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
113
+ "issue_id" text NOT NULL REFERENCES "issues"("id") ON DELETE CASCADE,
114
+ "project_id" text NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE,
115
+ "file_name" text NOT NULL,
116
+ "mime_type" text,
117
+ "file_size_bytes" integer NOT NULL,
118
+ "relative_path" text NOT NULL,
119
+ "uploaded_by_actor_type" text DEFAULT 'human' NOT NULL,
120
+ "uploaded_by_actor_id" text,
121
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
122
+ );
123
+ --> statement-breakpoint
124
+ CREATE TABLE "activity_logs" (
125
+ "id" text PRIMARY KEY NOT NULL,
126
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
127
+ "issue_id" text REFERENCES "issues"("id") ON DELETE SET NULL,
128
+ "actor_type" text NOT NULL,
129
+ "actor_id" text,
130
+ "event_type" text NOT NULL,
131
+ "payload_json" text DEFAULT '{}' NOT NULL,
132
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
133
+ );
134
+ --> statement-breakpoint
135
+ CREATE TABLE "heartbeat_runs" (
136
+ "id" text PRIMARY KEY NOT NULL,
137
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
138
+ "agent_id" text NOT NULL REFERENCES "agents"("id") ON DELETE CASCADE,
139
+ "status" text DEFAULT 'started' NOT NULL,
140
+ "started_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
141
+ "finished_at" timestamp,
142
+ "message" text
143
+ );
144
+ --> statement-breakpoint
145
+ CREATE TABLE "heartbeat_run_queue" (
146
+ "id" text PRIMARY KEY NOT NULL,
147
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
148
+ "agent_id" text NOT NULL REFERENCES "agents"("id") ON DELETE CASCADE,
149
+ "job_type" text NOT NULL,
150
+ "payload_json" text DEFAULT '{}' NOT NULL,
151
+ "status" text DEFAULT 'pending' NOT NULL,
152
+ "priority" integer DEFAULT 100 NOT NULL,
153
+ "idempotency_key" text,
154
+ "available_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
155
+ "attempt_count" integer DEFAULT 0 NOT NULL,
156
+ "max_attempts" integer DEFAULT 10 NOT NULL,
157
+ "last_error" text,
158
+ "started_at" timestamp,
159
+ "finished_at" timestamp,
160
+ "heartbeat_run_id" text REFERENCES "heartbeat_runs"("id") ON DELETE SET NULL,
161
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
162
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
163
+ );
164
+ --> statement-breakpoint
165
+ CREATE TABLE "heartbeat_run_messages" (
166
+ "id" text PRIMARY KEY NOT NULL,
167
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
168
+ "run_id" text NOT NULL REFERENCES "heartbeat_runs"("id") ON DELETE CASCADE,
169
+ "sequence" integer NOT NULL,
170
+ "kind" text NOT NULL,
171
+ "label" text,
172
+ "text" text,
173
+ "payload_json" text,
174
+ "signal_level" text,
175
+ "group_key" text,
176
+ "source" text,
177
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
178
+ );
179
+ --> statement-breakpoint
180
+ CREATE TABLE "approval_requests" (
181
+ "id" text PRIMARY KEY NOT NULL,
182
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
183
+ "requested_by_agent_id" text,
184
+ "action" text NOT NULL,
185
+ "payload_json" text DEFAULT '{}' NOT NULL,
186
+ "status" text DEFAULT 'pending' NOT NULL,
187
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
188
+ "resolved_at" timestamp
189
+ );
190
+ --> statement-breakpoint
191
+ CREATE TABLE "approval_inbox_states" (
192
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
193
+ "actor_id" text NOT NULL,
194
+ "approval_id" text NOT NULL REFERENCES "approval_requests"("id") ON DELETE CASCADE,
195
+ "seen_at" timestamp,
196
+ "dismissed_at" timestamp,
197
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
198
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
199
+ PRIMARY KEY ("company_id", "actor_id", "approval_id")
200
+ );
201
+ --> statement-breakpoint
202
+ CREATE TABLE "attention_inbox_states" (
203
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
204
+ "actor_id" text NOT NULL,
205
+ "item_key" text NOT NULL,
206
+ "seen_at" timestamp,
207
+ "acknowledged_at" timestamp,
208
+ "dismissed_at" timestamp,
209
+ "resolved_at" timestamp,
210
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
211
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
212
+ PRIMARY KEY ("company_id", "actor_id", "item_key")
213
+ );
214
+ --> statement-breakpoint
215
+ CREATE TABLE "cost_ledger" (
216
+ "id" text PRIMARY KEY NOT NULL,
217
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
218
+ "run_id" text REFERENCES "heartbeat_runs"("id") ON DELETE SET NULL,
219
+ "project_id" text REFERENCES "projects"("id") ON DELETE SET NULL,
220
+ "issue_id" text REFERENCES "issues"("id") ON DELETE SET NULL,
221
+ "agent_id" text REFERENCES "agents"("id") ON DELETE SET NULL,
222
+ "provider_type" text NOT NULL,
223
+ "runtime_model_id" text,
224
+ "pricing_provider_type" text,
225
+ "pricing_model_id" text,
226
+ "pricing_source" text,
227
+ "token_input" integer DEFAULT 0 NOT NULL,
228
+ "token_output" integer DEFAULT 0 NOT NULL,
229
+ "usd_cost" numeric(12, 6) DEFAULT 0 NOT NULL,
230
+ "usd_cost_status" text,
231
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
232
+ );
233
+ --> statement-breakpoint
234
+ DROP TABLE IF EXISTS "model_pricing";
235
+ --> statement-breakpoint
236
+ CREATE TABLE "audit_events" (
237
+ "id" text PRIMARY KEY NOT NULL,
238
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
239
+ "actor_type" text NOT NULL,
240
+ "actor_id" text,
241
+ "event_type" text NOT NULL,
242
+ "entity_type" text NOT NULL,
243
+ "entity_id" text NOT NULL,
244
+ "correlation_id" text,
245
+ "payload_json" text DEFAULT '{}' NOT NULL,
246
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
247
+ );
248
+ --> statement-breakpoint
249
+ CREATE TABLE "plugins" (
250
+ "id" text PRIMARY KEY NOT NULL,
251
+ "name" text NOT NULL,
252
+ "version" text NOT NULL,
253
+ "kind" text NOT NULL,
254
+ "runtime_type" text NOT NULL,
255
+ "runtime_entrypoint" text NOT NULL,
256
+ "hooks_json" text DEFAULT '[]' NOT NULL,
257
+ "capabilities_json" text DEFAULT '[]' NOT NULL,
258
+ "manifest_json" text DEFAULT '{}' NOT NULL,
259
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
260
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
261
+ );
262
+ --> statement-breakpoint
263
+ CREATE TABLE "templates" (
264
+ "id" text PRIMARY KEY NOT NULL,
265
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
266
+ "slug" text NOT NULL,
267
+ "name" text NOT NULL,
268
+ "description" text,
269
+ "current_version" text DEFAULT '1.0.0' NOT NULL,
270
+ "status" text DEFAULT 'draft' NOT NULL,
271
+ "visibility" text DEFAULT 'company' NOT NULL,
272
+ "variables_json" text DEFAULT '[]' NOT NULL,
273
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
274
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
275
+ );
276
+ --> statement-breakpoint
277
+ CREATE TABLE "template_versions" (
278
+ "id" text PRIMARY KEY NOT NULL,
279
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
280
+ "template_id" text NOT NULL REFERENCES "templates"("id") ON DELETE CASCADE,
281
+ "version" text NOT NULL,
282
+ "manifest_json" text DEFAULT '{}' NOT NULL,
283
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
284
+ );
285
+ --> statement-breakpoint
286
+ CREATE TABLE "template_installs" (
287
+ "id" text PRIMARY KEY NOT NULL,
288
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
289
+ "template_id" text REFERENCES "templates"("id") ON DELETE SET NULL,
290
+ "template_version_id" text REFERENCES "template_versions"("id") ON DELETE SET NULL,
291
+ "status" text DEFAULT 'applied' NOT NULL,
292
+ "summary_json" text DEFAULT '{}' NOT NULL,
293
+ "variables_json" text DEFAULT '{}' NOT NULL,
294
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
295
+ );
296
+ --> statement-breakpoint
297
+ CREATE TABLE "plugin_configs" (
298
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
299
+ "plugin_id" text NOT NULL REFERENCES "plugins"("id") ON DELETE CASCADE,
300
+ "enabled" boolean DEFAULT false NOT NULL,
301
+ "priority" integer DEFAULT 100 NOT NULL,
302
+ "config_json" text DEFAULT '{}' NOT NULL,
303
+ "granted_capabilities_json" text DEFAULT '[]' NOT NULL,
304
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
305
+ "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
306
+ PRIMARY KEY ("company_id", "plugin_id")
307
+ );
308
+ --> statement-breakpoint
309
+ CREATE TABLE "plugin_runs" (
310
+ "id" text PRIMARY KEY NOT NULL,
311
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
312
+ "run_id" text REFERENCES "heartbeat_runs"("id") ON DELETE CASCADE,
313
+ "plugin_id" text NOT NULL REFERENCES "plugins"("id") ON DELETE CASCADE,
314
+ "hook" text NOT NULL,
315
+ "status" text NOT NULL,
316
+ "duration_ms" integer DEFAULT 0 NOT NULL,
317
+ "error" text,
318
+ "diagnostics_json" text DEFAULT '{}' NOT NULL,
319
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
320
+ );
321
+ --> statement-breakpoint
322
+ CREATE TABLE "agent_issue_labels" (
323
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
324
+ "issue_id" text NOT NULL REFERENCES "issues"("id") ON DELETE CASCADE,
325
+ "label" text NOT NULL,
326
+ PRIMARY KEY ("company_id", "issue_id", "label")
327
+ );
328
+ --> statement-breakpoint
329
+ CREATE INDEX "idx_project_workspaces_company_project"
330
+ ON "project_workspaces" ("company_id", "project_id", "is_primary", "created_at");
331
+ --> statement-breakpoint
332
+ CREATE INDEX "idx_issues_company_status"
333
+ ON "issues" ("company_id", "status", "updated_at");
334
+ --> statement-breakpoint
335
+ CREATE INDEX "idx_issues_assignee_claim_priority"
336
+ ON "issues" ("company_id", "assignee_agent_id", "is_claimed", "status", "priority", "updated_at");
337
+ --> statement-breakpoint
338
+ CREATE INDEX "idx_issue_attachments_company_issue"
339
+ ON "issue_attachments" ("company_id", "issue_id", "created_at");
340
+ --> statement-breakpoint
341
+ CREATE INDEX "idx_issue_attachments_company_project"
342
+ ON "issue_attachments" ("company_id", "project_id", "created_at");
343
+ --> statement-breakpoint
344
+ CREATE INDEX "idx_audit_events_company_created"
345
+ ON "audit_events" ("company_id", "created_at");
346
+ --> statement-breakpoint
347
+ CREATE INDEX "idx_cost_ledger_company_created"
348
+ ON "cost_ledger" ("company_id", "created_at");
349
+ --> statement-breakpoint
350
+ CREATE UNIQUE INDEX "idx_heartbeat_runs_single_started"
351
+ ON "heartbeat_runs" ("company_id", "agent_id")
352
+ WHERE "status" = 'started';
353
+ --> statement-breakpoint
354
+ CREATE INDEX "idx_heartbeat_run_queue_status_available_priority"
355
+ ON "heartbeat_run_queue" ("company_id", "status", "available_at", "priority", "created_at");
356
+ --> statement-breakpoint
357
+ CREATE INDEX "idx_heartbeat_run_queue_agent_status"
358
+ ON "heartbeat_run_queue" ("company_id", "agent_id", "status", "available_at", "created_at");
359
+ --> statement-breakpoint
360
+ CREATE UNIQUE INDEX "idx_heartbeat_run_queue_idempotency"
361
+ ON "heartbeat_run_queue" ("company_id", "agent_id", "idempotency_key")
362
+ WHERE "idempotency_key" IS NOT NULL AND btrim("idempotency_key") <> '';
363
+ --> statement-breakpoint
364
+ CREATE INDEX "idx_heartbeat_run_messages_company_run_sequence"
365
+ ON "heartbeat_run_messages" ("company_id", "run_id", "sequence");
366
+ --> statement-breakpoint
367
+ CREATE INDEX "idx_heartbeat_run_messages_company_created"
368
+ ON "heartbeat_run_messages" ("company_id", "created_at");
369
+ --> statement-breakpoint
370
+ CREATE INDEX "idx_approval_inbox_states_company_actor_updated"
371
+ ON "approval_inbox_states" ("company_id", "actor_id", "updated_at");
372
+ --> statement-breakpoint
373
+ CREATE INDEX "idx_attention_inbox_states_company_actor_updated"
374
+ ON "attention_inbox_states" ("company_id", "actor_id", "updated_at");
375
+ --> statement-breakpoint
376
+ CREATE INDEX "idx_plugin_configs_company_enabled_priority"
377
+ ON "plugin_configs" ("company_id", "enabled", "priority", "plugin_id");
378
+ --> statement-breakpoint
379
+ CREATE INDEX "idx_plugin_runs_company_created"
380
+ ON "plugin_runs" ("company_id", "created_at");
381
+ --> statement-breakpoint
382
+ CREATE UNIQUE INDEX "idx_templates_company_slug"
383
+ ON "templates" ("company_id", "slug");
384
+ --> statement-breakpoint
385
+ CREATE INDEX "idx_template_versions_company_template_created"
386
+ ON "template_versions" ("company_id", "template_id", "created_at");
387
+ --> statement-breakpoint
388
+ CREATE INDEX "idx_template_installs_company_created"
389
+ ON "template_installs" ("company_id", "created_at");
@@ -0,0 +1 @@
1
+ ALTER TABLE "issues" ADD COLUMN "external_link" text;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "goals" ADD COLUMN "owner_agent_id" text REFERENCES "agents"("id") ON DELETE SET NULL;
2
+ ALTER TABLE "issues" ADD COLUMN "goal_id" text REFERENCES "goals"("id") ON DELETE SET NULL;
@@ -0,0 +1,12 @@
1
+ CREATE TABLE "issue_goals" (
2
+ "issue_id" text NOT NULL REFERENCES "issues"("id") ON DELETE CASCADE,
3
+ "goal_id" text NOT NULL REFERENCES "goals"("id") ON DELETE CASCADE,
4
+ "company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
5
+ "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
6
+ PRIMARY KEY ("issue_id", "goal_id")
7
+ );
8
+
9
+ INSERT INTO "issue_goals" ("issue_id", "goal_id", "company_id")
10
+ SELECT "id", "goal_id", "company_id" FROM "issues" WHERE "goal_id" IS NOT NULL;
11
+
12
+ ALTER TABLE "issues" DROP COLUMN "goal_id";
@@ -0,0 +1,34 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1742500000000,
9
+ "tag": "0000_initial",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1742600000000,
16
+ "tag": "0001_issues_external_link",
17
+ "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1742700000000,
23
+ "tag": "0002_issues_goal_goals_owner_agent",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "7",
29
+ "when": 1742800000000,
30
+ "tag": "0003_issue_goals_junction",
31
+ "breakpoints": true
32
+ }
33
+ ]
34
+ }
package/src/ping.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { sql } from "drizzle-orm";
2
+ import type { BopoDb } from "./client";
3
+
4
+ /** Cheap connection liveness check (no table scan). Kept as SQL because Drizzle has no relational API for a table-free SELECT. */
5
+ export async function pingDatabase(db: BopoDb): Promise<void> {
6
+ await db.execute(sql`SELECT 1`);
7
+ }
@@ -0,0 +1,41 @@
1
+ import { desc, eq } from "drizzle-orm";
2
+ import { nanoid } from "nanoid";
3
+ import type { BopoDb } from "../client";
4
+ import { companies } from "../schema";
5
+ import { compactUpdate } from "./helpers";
6
+
7
+ export async function createCompany(db: BopoDb, input: { name: string; mission?: string | null }) {
8
+ const id = nanoid(12);
9
+ await db.insert(companies).values({
10
+ id,
11
+ name: input.name,
12
+ mission: input.mission ?? null
13
+ });
14
+ return { id, ...input };
15
+ }
16
+
17
+ export async function listCompanies(db: BopoDb) {
18
+ return db.select().from(companies).orderBy(desc(companies.createdAt));
19
+ }
20
+
21
+ export async function getCompany(db: BopoDb, id: string) {
22
+ const [row] = await db.select().from(companies).where(eq(companies.id, id)).limit(1);
23
+ return row ?? null;
24
+ }
25
+
26
+ export async function updateCompany(
27
+ db: BopoDb,
28
+ input: { id: string; name?: string; mission?: string | null }
29
+ ) {
30
+ const [company] = await db
31
+ .update(companies)
32
+ .set(compactUpdate({ name: input.name, mission: input.mission }))
33
+ .where(eq(companies.id, input.id))
34
+ .returning();
35
+ return company ?? null;
36
+ }
37
+
38
+ export async function deleteCompany(db: BopoDb, id: string) {
39
+ const [deletedCompany] = await db.delete(companies).where(eq(companies.id, id)).returning({ id: companies.id });
40
+ return Boolean(deletedCompany);
41
+ }
@@ -0,0 +1,104 @@
1
+ import { and, eq } from "drizzle-orm";
2
+ import type { BopoDb } from "../client";
3
+ import { agents, goals, issues, projects, templates } from "../schema";
4
+
5
+ export class RepositoryValidationError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = "RepositoryValidationError";
9
+ }
10
+ }
11
+
12
+ export async function assertProjectBelongsToCompany(db: BopoDb, companyId: string, projectId: string) {
13
+ const [project] = await db
14
+ .select({ id: projects.id })
15
+ .from(projects)
16
+ .where(and(eq(projects.companyId, companyId), eq(projects.id, projectId)))
17
+ .limit(1);
18
+ if (!project) {
19
+ throw new RepositoryValidationError("Project not found for company.");
20
+ }
21
+ }
22
+
23
+ export async function assertIssueBelongsToCompany(db: BopoDb, companyId: string, issueId: string) {
24
+ const [issue] = await db
25
+ .select({ id: issues.id })
26
+ .from(issues)
27
+ .where(and(eq(issues.companyId, companyId), eq(issues.id, issueId)))
28
+ .limit(1);
29
+ if (!issue) {
30
+ throw new RepositoryValidationError("Issue not found for company.");
31
+ }
32
+ }
33
+
34
+ export async function assertGoalBelongsToCompany(db: BopoDb, companyId: string, goalId: string) {
35
+ const [goal] = await db
36
+ .select({ id: goals.id })
37
+ .from(goals)
38
+ .where(and(eq(goals.companyId, companyId), eq(goals.id, goalId)))
39
+ .limit(1);
40
+ if (!goal) {
41
+ throw new RepositoryValidationError("Parent goal not found for company.");
42
+ }
43
+ }
44
+
45
+ /** Ensures a goal can be linked to an issue: same company; project-scoped goals must match the issue's project. */
46
+ export async function assertIssueGoalAssignable(
47
+ db: BopoDb,
48
+ companyId: string,
49
+ issueProjectId: string,
50
+ goalId: string | null | undefined
51
+ ) {
52
+ if (!goalId) {
53
+ return;
54
+ }
55
+ const [goal] = await db
56
+ .select({ id: goals.id, projectId: goals.projectId })
57
+ .from(goals)
58
+ .where(and(eq(goals.companyId, companyId), eq(goals.id, goalId)))
59
+ .limit(1);
60
+ if (!goal) {
61
+ throw new RepositoryValidationError("Goal not found for company.");
62
+ }
63
+ if (goal.projectId && goal.projectId !== issueProjectId) {
64
+ throw new RepositoryValidationError("Goal is scoped to a different project than this issue.");
65
+ }
66
+ }
67
+
68
+ /** Validates each goal can be linked to an issue (same company; project goals must match issue project). */
69
+ export async function assertIssueGoalsAssignable(
70
+ db: BopoDb,
71
+ companyId: string,
72
+ issueProjectId: string,
73
+ goalIds: string[]
74
+ ) {
75
+ for (const goalId of goalIds) {
76
+ await assertIssueGoalAssignable(db, companyId, issueProjectId, goalId);
77
+ }
78
+ }
79
+
80
+ export async function assertAgentBelongsToCompany(db: BopoDb, companyId: string, agentId: string) {
81
+ const [agent] = await db
82
+ .select({ id: agents.id })
83
+ .from(agents)
84
+ .where(and(eq(agents.companyId, companyId), eq(agents.id, agentId)))
85
+ .limit(1);
86
+ if (!agent) {
87
+ throw new RepositoryValidationError("Agent not found for company.");
88
+ }
89
+ }
90
+
91
+ export async function assertTemplateBelongsToCompany(db: BopoDb, companyId: string, templateId: string) {
92
+ const [template] = await db
93
+ .select({ id: templates.id })
94
+ .from(templates)
95
+ .where(and(eq(templates.companyId, companyId), eq(templates.id, templateId)))
96
+ .limit(1);
97
+ if (!template) {
98
+ throw new RepositoryValidationError("Template not found for company.");
99
+ }
100
+ }
101
+
102
+ export function compactUpdate<T extends Record<string, unknown>>(input: T) {
103
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
104
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./helpers";
2
+ export * from "./companies";
3
+ export * from "./legacy";