app/db/schema.ts
363 lines and 49 functions in DrDustinEdwards/carrel, at commit d5d5e82 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| <anonymous:29> | 29 to 29 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:30> | 30 to 30 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:33> | 33 to 33 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:46> | 46 to 46 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:58> | 58 to 58 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:64> | 64 to 64 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:66> | 66 to 66 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:71> | 71 to 71 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:76> | 76 to 76 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:78> | 78 to 78 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:95> | 95 to 95 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:97> | 97 to 97 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:123> | 123 to 123 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:125> | 125 to 125 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:135> | 135 to 135 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:137> | 137 to 137 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:148> | 148 to 148 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:157> | 157 to 157 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:169> | 169 to 169 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:177> | 177 to 177 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:184> | 184 to 184 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:186> | 186 to 186 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:201> | 201 to 201 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:206> | 206 to 206 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:212> | 212 to 212 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:213> | 213 to 213 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:219> | 219 to 219 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:225> | 225 to 225 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:226> | 226 to 226 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:230> | 230 to 230 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:240> | 240 to 240 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:250> | 250 to 250 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:268> | 268 to 268 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:270> | 270 to 270 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:280> | 280 to 280 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:290> | 290 to 290 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:300> | 300 to 300 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:323> | 323 to 323 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:324> | 324 to 324 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:331> | 331 to 331 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:337> | 337 to 337 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:338> | 338 to 338 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:344> | 344 to 344 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:358> | 358 to 358 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:363> | 363 to 363 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:368> | 368 to 368 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:374> | 374 to 374 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:388> | 388 to 388 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:395> | 395 to 395 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by app/lib/ai.server.ts, app/lib/binder.server.ts, app/lib/book-history.server.ts, app/lib/books.server.ts, app/lib/content.server.ts, app/lib/google/drive.server.ts, app/lib/google/oauth.server.ts, app/lib/google/refresh.server.ts, app/lib/google/search-console.server.ts, app/lib/health.server.ts, app/lib/inbox-report.server.ts, app/lib/index.server.ts, app/lib/legal.server.ts, app/lib/mcp/call-log.ts, app/lib/media.server.ts, app/lib/mentions.server.ts, app/lib/people-admin.server.ts, app/lib/people.server.ts, app/lib/projects.server.ts, app/lib/refresh.server.ts, app/lib/social/queue.server.ts, app/lib/voice.server.ts, app/lib/waiting.server.ts, app/lib/writing.server.ts.
Source
// Mirrors drizzle/0001_init.sql to 0018_voice.sql, which own the shape. Change both in the
// same commit. The FTS5 table site_items_fts has no mirror: only index.server.ts touches it, in SQL.
import { sql } from "drizzle-orm";
import { integer, primaryKey, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const people = sqliteTable("people", {
id: integer("id").primaryKey(),
email: text("email").notNull().unique(),
name: text("name").notNull().default(""),
isOwner: integer("is_owner", { mode: "boolean" }).notNull().default(false),
isReviewer: integer("is_reviewer", { mode: "boolean" }).notNull().default(false),
disabledAt: text("disabled_at"),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const projects = sqliteTable("projects", {
id: integer("id").primaryKey(),
slug: text("slug").notNull().unique(),
name: text("name").notNull(),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
site: text("site"),
book: text("book"),
});
export const projectMembers = sqliteTable(
"project_members",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
personId: integer("person_id").notNull().references(() => people.id, { onDelete: "cascade" }),
role: text("role", { enum: ["reader", "editor"] }).notNull(),
},
(t) => [primaryKey({ columns: [t.projectId, t.personId] })],
);
export const healthState = sqliteTable("health_state", {
checkName: text("check_name").primaryKey(),
ok: integer("ok", { mode: "boolean" }).notNull(),
detail: text("detail").notNull(),
changedAt: text("changed_at").notNull(),
});
export const siteItems = sqliteTable(
"site_items",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
itemId: text("item_id").notNull(),
kind: text("kind").notNull(),
title: text("title").notNull(),
status: text("status", { enum: ["draft", "scheduled", "published"] }).notNull(),
path: text("path"),
publishAt: text("publish_at"),
publishedAt: text("published_at"),
updatedAt: text("updated_at"),
bodyUpdatedAt: text("body_updated_at"),
syncedAt: text("synced_at").notNull(),
},
(t) => [primaryKey({ columns: [t.projectId, t.itemId] })],
);
export const drafts = sqliteTable(
"drafts",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
itemId: text("item_id").notNull(),
personId: integer("person_id").notNull().references(() => people.id, { onDelete: "cascade" }),
source: text("source").notNull(),
baseVersion: text("base_version"),
updatedAt: text("updated_at").notNull(),
},
(t) => [primaryKey({ columns: [t.projectId, t.itemId, t.personId] })],
);
export const changes = sqliteTable("changes", {
id: text("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
itemId: text("item_id").notNull(),
personId: integer("person_id").notNull().references(() => people.id),
// Media actions since migration 0007: item_id is then the site's media id, and a file has no version.
// content-delete since migration 0009: item_id is the post's id, version_before the version deleted, version_after null.
// media-alt, media-tags, media-trash and media-restore since migration 0010: version_before and version_after are the file's metadata versions.
action: text("action", { enum: ["save", "publish", "schedule", "unpublish", "media-upload", "media-delete", "content-delete", "media-alt", "media-tags", "media-trash", "media-restore"] }).notNull(),
versionBefore: text("version_before"),
versionAfter: text("version_after"),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
client: text("client"),
});
// Webmention decisions since migration 0011 (a table of its own: the changes table holds posts and
// files). mention_id is the site's id for the mention and is null for a sweep; status_after is null
// for a delete and a sweep; change_id is the id Carrel sent the site, so the two histories join.
const MENTION_STATUS = ["unverified", "pending", "approved", "rejected", "failed"] as const;
export const mentionDecisions = sqliteTable("mention_decisions", {
id: integer("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
mentionId: text("mention_id"),
personId: integer("person_id").notNull().references(() => people.id),
action: text("action", { enum: ["approve", "reject", "delete", "sweep"] }).notNull(),
statusBefore: text("status_before", { enum: MENTION_STATUS }),
statusAfter: text("status_after", { enum: MENTION_STATUS }),
changeId: text("change_id").notNull().unique(),
client: text("client"),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
// The MCP call log (0012_mcp_calls.sql): one row per tool call, append-only (triggers refuse UPDATE
// and DELETE). No foreign key on the person, so the record outlives them.
export const mcpCalls = sqliteTable("mcp_calls", {
id: integer("id").primaryKey(),
personId: integer("person_id").notNull(),
caller: text("caller").notNull(),
client: text("client").notNull(),
tool: text("tool").notNull(),
arguments: text("arguments").notNull(),
argumentsTruncated: integer("arguments_truncated", { mode: "boolean" }).notNull().default(false),
outcome: text("outcome", { enum: ["ok", "refused"] }).notNull(),
result: text("result").notNull(),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const aiDrafts = sqliteTable("ai_drafts", {
id: integer("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
itemId: text("item_id").notNull(),
personId: integer("person_id").notNull().references(() => people.id, { onDelete: "cascade" }),
client: text("client").notNull(),
source: text("source").notNull(),
baseVersion: text("base_version"),
note: text("note").notNull().default(""),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const aiPublications = sqliteTable("ai_publications", {
changeId: text("change_id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
itemId: text("item_id").notNull(),
personId: integer("person_id").notNull().references(() => people.id),
client: text("client").notNull(),
version: text("version").notNull(),
publishedAt: text("published_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
emailedAt: text("emailed_at"),
emailError: text("email_error"),
});
export const bookFiles = sqliteTable(
"book_files",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
path: text("path").notNull(),
kind: text("kind", { enum: ["scene", "character", "place", "rule", "outline", "book", "note"] }).notNull(),
sha: text("sha").notNull(),
source: text("source").notNull(),
meta: text("meta").notNull().default("{}"),
words: integer("words").notNull().default(0),
syncedAt: text("synced_at").notNull(),
},
(t) => [primaryKey({ columns: [t.projectId, t.path] })],
);
export const novelsShared = sqliteTable("novels_shared", {
path: text("path").primaryKey(),
sha: text("sha").notNull(),
source: text("source").notNull(),
syncedAt: text("synced_at").notNull(),
});
export const findings = sqliteTable("findings", {
id: integer("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
path: text("path").notNull(),
checkName: text("check_name").notNull(),
message: text("message").notNull(),
line: integer("line"),
excerpt: text("excerpt"),
fingerprint: text("fingerprint").notNull(),
status: text("status", { enum: ["open", "dismissed"] }).notNull().default("open"),
dismissedBy: integer("dismissed_by").references(() => people.id),
dismissedAt: text("dismissed_at"),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const authorship = sqliteTable("authorship", {
id: text("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
path: text("path").notNull(),
personId: integer("person_id").notNull().references(() => people.id),
client: text("client"),
wordsAdded: integer("words_added").notNull(),
wordsRemoved: integer("words_removed").notNull(),
versionBefore: text("version_before"),
versionAfter: text("version_after").notNull(),
commitSha: text("commit_sha").notNull(),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
/** What the save counts toward the day: typed words only. Empty before 0012 and for an AI client's save. */
wordsTyped: integer("words_typed"),
});
export const statusLabels = sqliteTable(
"status_labels",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
position: integer("position").notNull(),
label: text("label").notNull(),
color: integer("color").notNull(),
},
(t) => [primaryKey({ columns: [t.projectId, t.position] })],
);
export const writingGoals = sqliteTable(
"writing_goals",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
personId: integer("person_id").notNull().references(() => people.id, { onDelete: "cascade" }),
mode: text("mode", { enum: ["daily", "deadline"] }).notNull().default("daily"),
dailyTarget: integer("daily_target"),
writingDays: text("writing_days").notNull().default("0123456"),
allowNegative: integer("allow_negative", { mode: "boolean" }).notNull().default(false),
},
(t) => [primaryKey({ columns: [t.projectId, t.personId] })],
);
export const untypedWords = sqliteTable(
"untyped_words",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
personId: integer("person_id").notNull().references(() => people.id, { onDelete: "cascade" }),
path: text("path").notNull(),
words: integer("words").notNull().default(0),
},
(t) => [primaryKey({ columns: [t.projectId, t.personId, t.path] })],
);
export const googleKeysSeen = sqliteTable("google_keys_seen", {
privateKeyId: text("private_key_id").primaryKey(),
clientEmail: text("client_email").notNull(),
firstSeenAt: text("first_seen_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const googleTokens = sqliteTable("google_tokens", {
personId: integer("person_id").primaryKey().references(() => people.id, { onDelete: "cascade" }),
refreshToken: text("refresh_token").notNull(),
iv: text("iv").notNull(),
scope: text("scope").notNull(),
connectedAt: text("connected_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
lastError: text("last_error"),
});
export const googleOauthStates = sqliteTable("google_oauth_states", {
state: text("state").primaryKey(),
personId: integer("person_id").notNull().references(() => people.id, { onDelete: "cascade" }),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const manuscripts = sqliteTable("manuscripts", {
fileId: text("file_id").primaryKey(),
name: text("name").notNull(),
mimeType: text("mime_type").notNull(),
webViewLink: text("web_view_link").notNull(),
folder: text("folder").notNull().default(""),
people: text("people").notNull().default("[]"),
createdTime: text("created_time"),
modifiedTime: text("modified_time"),
syncedAt: text("synced_at").notNull(),
});
export const sentDocs = sqliteTable("sent_docs", {
id: integer("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
itemId: text("item_id").notNull(),
personId: integer("person_id").notNull().references(() => people.id, { onDelete: "cascade" }),
docId: text("doc_id").notNull(),
docUrl: text("doc_url").notNull(),
frontmatter: text("frontmatter").notNull().default(""),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const searchConsolePages = sqliteTable(
"search_console_pages",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
page: text("page").notNull(),
clicks: integer("clicks").notNull(),
impressions: integer("impressions").notNull(),
ctr: real("ctr").notNull(),
position: real("position").notNull(),
startDate: text("start_date").notNull(),
endDate: text("end_date").notNull(),
fetchedAt: text("fetched_at").notNull(),
},
(t) => [primaryKey({ columns: [t.projectId, t.page] })],
);
export const socialAccounts = sqliteTable("social_accounts", {
id: integer("id").primaryKey(),
key: text("key").notNull().unique(),
name: text("name").notNull(),
platform: text("platform", { enum: ["bluesky", "x"] }).notNull(),
kind: text("kind", { enum: ["personal", "brand"] }).notNull(),
handle: text("handle").notNull(),
projectId: integer("project_id").references(() => projects.id, { onDelete: "set null" }),
mode: text("mode", { enum: ["approval", "auto"] }).notNull().default("approval"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
dailyCap: integer("daily_cap").notNull().default(3),
minGapMinutes: integer("min_gap_minutes").notNull().default(60),
monthlyBudgetMills: integer("monthly_budget_mills").notNull().default(0),
template: text("template").notNull().default(""),
voiceGuide: text("voice_guide").notNull().default(""),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const socialRoutineRuns = sqliteTable("social_routine_runs", {
id: integer("id").primaryKey(),
kind: text("kind", { enum: ["batch", "nightly"] }).notNull(),
requestedAt: text("requested_at").notNull(),
events: text("events").notNull(),
status: text("status", { enum: ["fired", "failed"] }).notNull(),
sessionUrl: text("session_url"),
error: text("error"),
});
export const socialEvents = sqliteTable("social_events", {
id: integer("id").primaryKey(),
accountId: integer("account_id").notNull().references(() => socialAccounts.id, { onDelete: "cascade" }),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
itemId: text("item_id").notNull(),
title: text("title").notNull().default(""),
url: text("url").notNull().default(""),
summary: text("summary").notNull().default(""),
publishedAt: text("published_at"),
state: text("state", { enum: ["waiting", "routine", "covered", "templated", "closed"] }).notNull().default("waiting"),
routineRunId: integer("routine_run_id").references(() => socialRoutineRuns.id),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const socialPosts = sqliteTable("social_posts", {
id: integer("id").primaryKey(),
accountId: integer("account_id").notNull().references(() => socialAccounts.id, { onDelete: "cascade" }),
eventId: integer("event_id").notNull().references(() => socialEvents.id, { onDelete: "cascade" }),
text: text("text").notNull(),
source: text("source", { enum: ["predrafted", "routine", "template"] }).notNull(),
status: text("status", { enum: ["drafted", "held", "awaiting", "queued", "sent", "by-hand", "failed", "rejected"] }).notNull(),
lint: text("lint").notNull().default("[]"),
createdBy: text("created_by").notNull(),
approvedBy: integer("approved_by").references(() => people.id),
costMills: integer("cost_mills").notNull().default(0),
platformPostId: text("platform_post_id"),
error: text("error"),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
sentAt: text("sent_at"),
acknowledgedAt: text("acknowledged_at"),
});
export const legalSections = sqliteTable("legal_sections", {
key: text("key").primaryKey(),
title: text("title").notNull(),
body: text("body").notNull(),
updatedAt: text("updated_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
updatedBy: integer("updated_by").notNull().references(() => people.id),
});
export const bookMoves = sqliteTable("book_moves", {
id: integer("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
changeId: text("change_id").notNull(),
fromPath: text("from_path").notNull(),
toPath: text("to_path").notNull(),
commitSha: text("commit_sha").notNull(),
personId: integer("person_id").notNull().references(() => people.id),
createdAt: text("created_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
// The voice pack and the cuts learned from the author's edits (0018_voice.sql, voice.server.ts).
export const voicePacks = sqliteTable("voice_packs", {
projectId: integer("project_id").primaryKey().references(() => projects.id, { onDelete: "cascade" }),
guide: text("guide").notNull().default(""),
phrases: text("phrases").notNull().default("[]"),
allow: text("allow").notNull().default("[]"),
profile: text("profile"),
excerpts: text("excerpts").notNull().default("[]"),
posts: integer("posts").notNull().default(0),
builtAt: text("built_at"),
updatedAt: text("updated_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});
export const voiceCuts = sqliteTable(
"voice_cuts",
{
projectId: integer("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
phrase: text("phrase").notNull(),
items: text("items").notNull().default("[]"),
kept: integer("kept").notNull().default(0),
setAside: integer("set_aside", { mode: "boolean" }).notNull().default(false),
lastCutAt: text("last_cut_at").notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => [primaryKey({ columns: [t.projectId, t.phrase] })],
);