feat: tag management

This commit is contained in:
2026-02-11 14:30:57 +01:00
parent 6b9aa3fb1e
commit 325114681f
17 changed files with 2529 additions and 3 deletions

View File

@@ -184,6 +184,18 @@ export class DatabaseConnection {
CREATE INDEX IF NOT EXISTS idx_post_links_source ON post_links(source_post_id);
CREATE INDEX IF NOT EXISTS idx_post_links_target ON post_links(target_post_id);
CREATE UNIQUE INDEX IF NOT EXISTS posts_project_slug_idx ON posts(project_id, slug);
CREATE TABLE IF NOT EXISTS tags (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
name TEXT NOT NULL,
color TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_tags_project_id ON tags(project_id);
CREATE UNIQUE INDEX IF NOT EXISTS tags_project_name_idx ON tags(project_id, name);
`);
// Check if project_id column exists in posts table, add if missing (migration)
@@ -368,6 +380,27 @@ export class DatabaseConnection {
console.log('FTS table migrated - rebuild index required');
}
// Migration: Ensure tags table exists (for databases created before tags feature)
const tagsTableExists = await this.localClient.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='tags'"
);
if (tagsTableExists.rows.length === 0) {
console.log('Creating tags table...');
await this.localClient.execute(`
CREATE TABLE tags (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
name TEXT NOT NULL,
color TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
await this.localClient.execute('CREATE INDEX idx_tags_project_id ON tags(project_id)');
await this.localClient.execute('CREATE UNIQUE INDEX tags_project_name_idx ON tags(project_id, name)');
console.log('Tags table created successfully');
}
// Create default project if none exists
const existingProjects = await this.localClient.execute('SELECT COUNT(*) as count FROM projects');
if (existingProjects.rows[0] && (existingProjects.rows[0].count as number) === 0) {

View File

@@ -95,6 +95,19 @@ export const postLinks = sqliteTable('post_links', {
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
// Tags table - stores tag metadata with optional colors
export const tags = sqliteTable('tags', {
id: text('id').primaryKey(),
projectId: text('project_id').notNull(),
name: text('name').notNull(),
color: text('color'), // Optional hex color like #ff0000
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
}, (table) => ({
// Composite unique index: tag name must be unique within each project
projectNameIdx: uniqueIndex('tags_project_name_idx').on(table.projectId, table.name),
}));
// Types for TypeScript
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;
@@ -108,3 +121,5 @@ export type Setting = typeof settings.$inferSelect;
export type NewSetting = typeof settings.$inferInsert;
export type PostLink = typeof postLinks.$inferSelect;
export type NewPostLink = typeof postLinks.$inferInsert;
export type Tag = typeof tags.$inferSelect;
export type NewTag = typeof tags.$inferInsert;