diff --git a/packages/server/src/db/migrate.ts b/packages/server/src/db/migrate.ts index 4866fb9a..920732f3 100644 --- a/packages/server/src/db/migrate.ts +++ b/packages/server/src/db/migrate.ts @@ -368,6 +368,9 @@ export function runMigrations(db: Database.Database): void { // ─── Clean up stale attachment records for profile images ─────────────── migrateCleanupProfileAttachmentRecords(db); + // ─── Add category_overrides table ──────────────────────────────────────── + migrateCategoryOverrides(db); + console.log('Migrations complete.'); } @@ -1003,6 +1006,26 @@ function migrateCleanupProfileAttachmentRecords(db: Database.Database): void { db.prepare('UPDATE instance_settings SET profile_attachments_cleaned = 1 WHERE id = 1').run(); } +function migrateCategoryOverrides(db: Database.Database): void { + const exists = db.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='category_overrides'" + ).get(); + if (exists) return; + + console.log('Migrating: Adding category_overrides table...'); + db.exec(` + CREATE TABLE IF NOT EXISTS category_overrides ( + category_id TEXT NOT NULL REFERENCES channel_categories(id) ON DELETE CASCADE, + target_type TEXT NOT NULL, + target_id TEXT NOT NULL, + allow TEXT NOT NULL DEFAULT '0', + deny TEXT NOT NULL DEFAULT '0', + PRIMARY KEY (category_id, target_type, target_id) + ); + CREATE INDEX IF NOT EXISTS idx_category_overrides_category_id ON category_overrides(category_id); + `); +} + /** * Async backfill: generate thumbnails for all existing image attachments that * don't have one yet. Runs once after server startup, gated by a persistent diff --git a/packages/server/src/db/schema.ts b/packages/server/src/db/schema.ts index 8721d19c..85c351dd 100644 --- a/packages/server/src/db/schema.ts +++ b/packages/server/src/db/schema.ts @@ -202,6 +202,16 @@ export const channelOverrides = sqliteTable('channel_overrides', { pk: primaryKey({ columns: [table.channelId, table.targetType, table.targetId] }), })); +export const categoryOverrides = sqliteTable('category_overrides', { + categoryId: text('category_id').notNull().references(() => channelCategories.id, { onDelete: 'cascade' }), + targetType: text('target_type').notNull(), + targetId: text('target_id').notNull(), + allow: text('allow').notNull().default('0'), + deny: text('deny').notNull().default('0'), +}, (table) => ({ + pk: primaryKey({ columns: [table.categoryId, table.targetType, table.targetId] }), +})); + export const readStates = sqliteTable('read_states', { userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), channelId: text('channel_id').notNull(),