Add folder and sort order functionality to database schema and operations
This commit is contained in:
171
src/lib/db.ts
171
src/lib/db.ts
@@ -6,6 +6,17 @@ export type Text = {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
last_saved_version_id: string | null;
|
last_saved_version_id: string | null;
|
||||||
|
folder_id: string | null;
|
||||||
|
sort_order: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Folder = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
parent_id: string | null;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
sort_order: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TextVersion = {
|
export type TextVersion = {
|
||||||
@@ -26,6 +37,15 @@ export type TextDraft = {
|
|||||||
|
|
||||||
const MIGRATIONS = [
|
const MIGRATIONS = [
|
||||||
"PRAGMA foreign_keys = ON;",
|
"PRAGMA foreign_keys = ON;",
|
||||||
|
`CREATE TABLE IF NOT EXISTS folders(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
parent_id TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
sort_order INTEGER,
|
||||||
|
FOREIGN KEY(parent_id) REFERENCES folders(id)
|
||||||
|
);`,
|
||||||
`CREATE TABLE IF NOT EXISTS prompts(
|
`CREATE TABLE IF NOT EXISTS prompts(
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
@@ -50,7 +70,10 @@ const MIGRATIONS = [
|
|||||||
FOREIGN KEY(prompt_id) REFERENCES prompts(id) ON DELETE CASCADE
|
FOREIGN KEY(prompt_id) REFERENCES prompts(id) ON DELETE CASCADE
|
||||||
);`,
|
);`,
|
||||||
"CREATE INDEX IF NOT EXISTS idx_prompt_versions_prompt_time ON prompt_versions(prompt_id, created_at DESC);",
|
"CREATE INDEX IF NOT EXISTS idx_prompt_versions_prompt_time ON prompt_versions(prompt_id, created_at DESC);",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_prompts_updated ON prompts(updated_at DESC);"
|
"CREATE INDEX IF NOT EXISTS idx_prompts_updated ON prompts(updated_at DESC);",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id);",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_folders_updated ON folders(updated_at DESC);",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_prompts_folder ON prompts(folder_id);"
|
||||||
];
|
];
|
||||||
|
|
||||||
let dbPromise: Promise<Database> | null = null;
|
let dbPromise: Promise<Database> | null = null;
|
||||||
@@ -59,6 +82,21 @@ async function migrate(db: Database) {
|
|||||||
for (const statement of MIGRATIONS) {
|
for (const statement of MIGRATIONS) {
|
||||||
await db.execute(statement);
|
await db.execute(statement);
|
||||||
}
|
}
|
||||||
|
await ensureColumn(db, "prompts", "folder_id", "TEXT REFERENCES folders(id) ON DELETE SET NULL");
|
||||||
|
await ensureColumn(db, "prompts", "sort_order", "INTEGER");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureColumn(
|
||||||
|
db: Database,
|
||||||
|
table: string,
|
||||||
|
column: string,
|
||||||
|
definition: string
|
||||||
|
) {
|
||||||
|
const columns = await db.select<{ name: string }[]>(
|
||||||
|
`PRAGMA table_info(${table})`
|
||||||
|
);
|
||||||
|
if (columns.some((col) => col.name === column)) return;
|
||||||
|
await db.execute(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDb(): Promise<Database> {
|
export async function getDb(): Promise<Database> {
|
||||||
@@ -79,7 +117,11 @@ export async function initDb() {
|
|||||||
export async function listTexts(): Promise<Text[]> {
|
export async function listTexts(): Promise<Text[]> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const rows = await db.select<Text[]>(
|
const rows = await db.select<Text[]>(
|
||||||
"SELECT * FROM prompts ORDER BY updated_at DESC"
|
`SELECT * FROM prompts
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN sort_order IS NULL THEN 1 ELSE 0 END,
|
||||||
|
sort_order ASC,
|
||||||
|
updated_at DESC`
|
||||||
);
|
);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
@@ -97,7 +139,10 @@ export async function searchTexts(term: string): Promise<Text[]> {
|
|||||||
WHERE v.prompt_id = p.id
|
WHERE v.prompt_id = p.id
|
||||||
AND LOWER(v.body) LIKE $1
|
AND LOWER(v.body) LIKE $1
|
||||||
)
|
)
|
||||||
ORDER BY p.updated_at DESC`,
|
ORDER BY
|
||||||
|
CASE WHEN p.sort_order IS NULL THEN 1 ELSE 0 END,
|
||||||
|
p.sort_order ASC,
|
||||||
|
p.updated_at DESC`,
|
||||||
[normalized]
|
[normalized]
|
||||||
);
|
);
|
||||||
return rows;
|
return rows;
|
||||||
@@ -144,7 +189,9 @@ export async function getDraft(promptId: string): Promise<TextDraft | null> {
|
|||||||
|
|
||||||
export async function createText(
|
export async function createText(
|
||||||
title: string,
|
title: string,
|
||||||
body: string
|
body: string,
|
||||||
|
folderId: string | null = null,
|
||||||
|
sortOrder: number | null = null
|
||||||
): Promise<{ textId: string; versionId: string; createdAt: number }> {
|
): Promise<{ textId: string; versionId: string; createdAt: number }> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -152,8 +199,8 @@ export async function createText(
|
|||||||
const versionId = crypto.randomUUID();
|
const versionId = crypto.randomUUID();
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO prompts(id, title, created_at, updated_at, last_saved_version_id) VALUES($1, $2, $3, $4, $5)",
|
"INSERT INTO prompts(id, title, created_at, updated_at, last_saved_version_id, folder_id, sort_order) VALUES($1, $2, $3, $4, $5, $6, $7)",
|
||||||
[textId, title, now, now, versionId]
|
[textId, title, now, now, versionId, folderId, sortOrder]
|
||||||
);
|
);
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -166,6 +213,118 @@ export async function createText(
|
|||||||
return { textId, versionId, createdAt: now };
|
return { textId, versionId, createdAt: now };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listFolders(): Promise<Folder[]> {
|
||||||
|
const db = await getDb();
|
||||||
|
return db.select<Folder[]>(
|
||||||
|
`SELECT * FROM folders
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN sort_order IS NULL THEN 1 ELSE 0 END,
|
||||||
|
sort_order ASC,
|
||||||
|
updated_at DESC`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFolder(
|
||||||
|
name: string,
|
||||||
|
parentId: string | null = null,
|
||||||
|
sortOrder: number | null = null
|
||||||
|
): Promise<{ folderId: string; createdAt: number }> {
|
||||||
|
const db = await getDb();
|
||||||
|
const now = Date.now();
|
||||||
|
const folderId = crypto.randomUUID();
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO folders(id, name, parent_id, created_at, updated_at, sort_order) VALUES($1, $2, $3, $4, $5, $6)",
|
||||||
|
[folderId, name, parentId, now, now, sortOrder]
|
||||||
|
);
|
||||||
|
return { folderId, createdAt: now };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateFolderName(folderId: string, name: string) {
|
||||||
|
const db = await getDb();
|
||||||
|
const now = Date.now();
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE folders SET name = $1, updated_at = $2 WHERE id = $3",
|
||||||
|
[name, now, folderId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveFolder(
|
||||||
|
folderId: string,
|
||||||
|
parentId: string | null,
|
||||||
|
sortOrder: number | null = null
|
||||||
|
) {
|
||||||
|
const db = await getDb();
|
||||||
|
const now = Date.now();
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE folders SET parent_id = $1, sort_order = $2, updated_at = $3 WHERE id = $4",
|
||||||
|
[parentId, sortOrder, now, folderId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setFolderOrder(folderIds: string[]) {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.execute("BEGIN TRANSACTION");
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < folderIds.length; i += 1) {
|
||||||
|
await db.execute("UPDATE folders SET sort_order = $1 WHERE id = $2", [
|
||||||
|
i,
|
||||||
|
folderIds[i]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await db.execute("COMMIT");
|
||||||
|
} catch (error) {
|
||||||
|
await db.execute("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFolder(folderId: string) {
|
||||||
|
const db = await getDb();
|
||||||
|
const rows = await db.select<{ parent_id: string | null }[]>(
|
||||||
|
"SELECT parent_id FROM folders WHERE id = $1 LIMIT 1",
|
||||||
|
[folderId]
|
||||||
|
);
|
||||||
|
const parentId = rows[0]?.parent_id ?? null;
|
||||||
|
|
||||||
|
await db.execute("UPDATE folders SET parent_id = $1 WHERE parent_id = $2", [
|
||||||
|
parentId,
|
||||||
|
folderId
|
||||||
|
]);
|
||||||
|
await db.execute("UPDATE prompts SET folder_id = $1 WHERE folder_id = $2", [
|
||||||
|
parentId,
|
||||||
|
folderId
|
||||||
|
]);
|
||||||
|
await db.execute("DELETE FROM folders WHERE id = $1", [folderId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveTextToFolder(
|
||||||
|
textId: string,
|
||||||
|
folderId: string | null,
|
||||||
|
sortOrder: number | null = null
|
||||||
|
) {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE prompts SET folder_id = $1, sort_order = $2 WHERE id = $3",
|
||||||
|
[folderId, sortOrder, textId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setTextOrder(textIds: string[]) {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.execute("BEGIN TRANSACTION");
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < textIds.length; i += 1) {
|
||||||
|
await db.execute("UPDATE prompts SET sort_order = $1 WHERE id = $2", [
|
||||||
|
i,
|
||||||
|
textIds[i]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await db.execute("COMMIT");
|
||||||
|
} catch (error) {
|
||||||
|
await db.execute("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function saveManualVersion(
|
export async function saveManualVersion(
|
||||||
promptId: string,
|
promptId: string,
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user