Migrated git root from W:/programming/Projects/ to W:/programming/Projects/Tubearr/. Previous history preserved in Tubearr-full-backup.bundle at parent directory. Completed milestones: M001 through M005 Active: M006/S02 (Add Channel UX)
193 lines
5.4 KiB
TypeScript
193 lines
5.4 KiB
TypeScript
import { eq, and, sql } from 'drizzle-orm';
|
|
import { type LibSQLDatabase } from 'drizzle-orm/libsql';
|
|
import type * as schema from '../schema/index';
|
|
import { channels, contentItems } from '../schema/index';
|
|
import type { Channel, Platform, MonitoringMode } from '../../types/index';
|
|
|
|
// ── Types ──
|
|
|
|
/** Fields needed to create a new channel (auto-generated fields excluded). */
|
|
export type CreateChannelData = Omit<
|
|
Channel,
|
|
'id' | 'createdAt' | 'updatedAt' | 'lastCheckedAt' | 'lastCheckStatus' | 'monitoringMode'
|
|
> & { monitoringMode?: Channel['monitoringMode'] };
|
|
|
|
/** Fields that can be updated on an existing channel. */
|
|
export type UpdateChannelData = Partial<
|
|
Pick<Channel, 'name' | 'checkInterval' | 'monitoringEnabled' | 'imageUrl' | 'formatProfileId' | 'lastCheckedAt' | 'lastCheckStatus' | 'monitoringMode'>
|
|
>;
|
|
|
|
type Db = LibSQLDatabase<typeof schema>;
|
|
|
|
// ── Repository Functions ──
|
|
|
|
/** Insert a new channel and return the created row. */
|
|
export async function createChannel(
|
|
db: Db,
|
|
data: CreateChannelData
|
|
): Promise<Channel> {
|
|
const result = await db
|
|
.insert(channels)
|
|
.values({
|
|
name: data.name,
|
|
platform: data.platform,
|
|
platformId: data.platformId,
|
|
url: data.url,
|
|
monitoringEnabled: data.monitoringEnabled,
|
|
checkInterval: data.checkInterval,
|
|
imageUrl: data.imageUrl,
|
|
metadata: data.metadata,
|
|
formatProfileId: data.formatProfileId,
|
|
monitoringMode: data.monitoringMode ?? 'all',
|
|
})
|
|
.returning();
|
|
|
|
return mapRow(result[0]);
|
|
}
|
|
|
|
/** Get a channel by ID. Returns null if not found. */
|
|
export async function getChannelById(
|
|
db: Db,
|
|
id: number
|
|
): Promise<Channel | null> {
|
|
const rows = await db
|
|
.select()
|
|
.from(channels)
|
|
.where(eq(channels.id, id))
|
|
.limit(1);
|
|
|
|
return rows.length > 0 ? mapRow(rows[0]) : null;
|
|
}
|
|
|
|
/** Get all channels, ordered by name. */
|
|
export async function getAllChannels(db: Db): Promise<Channel[]> {
|
|
const rows = await db.select().from(channels).orderBy(channels.name);
|
|
return rows.map(mapRow);
|
|
}
|
|
|
|
/** Get all channels with monitoring enabled. */
|
|
export async function getEnabledChannels(db: Db): Promise<Channel[]> {
|
|
const rows = await db
|
|
.select()
|
|
.from(channels)
|
|
.where(eq(channels.monitoringEnabled, true))
|
|
.orderBy(channels.name);
|
|
|
|
return rows.map(mapRow);
|
|
}
|
|
|
|
/** Update specific fields on a channel. Sets updatedAt to now. Returns updated row or null. */
|
|
export async function updateChannel(
|
|
db: Db,
|
|
id: number,
|
|
data: UpdateChannelData
|
|
): Promise<Channel | null> {
|
|
const result = await db
|
|
.update(channels)
|
|
.set({
|
|
...data,
|
|
updatedAt: sql`(datetime('now'))`,
|
|
})
|
|
.where(eq(channels.id, id))
|
|
.returning();
|
|
|
|
return result.length > 0 ? mapRow(result[0]) : null;
|
|
}
|
|
|
|
/**
|
|
* Change a channel's monitoring mode and cascade the `monitored` flag to all existing content items.
|
|
*
|
|
* Cascade logic:
|
|
* - 'all' or 'existing' → existing items become monitored (true)
|
|
* - 'future' or 'none' → existing items become unmonitored (false)
|
|
*
|
|
* Also syncs `monitoringEnabled`: mode !== 'none' → enabled (per D022).
|
|
* Returns updated channel or null if not found.
|
|
*/
|
|
export async function setMonitoringMode(
|
|
db: Db,
|
|
id: number,
|
|
mode: MonitoringMode
|
|
): Promise<Channel | null> {
|
|
// Step 1: Cascade monitored flag to all content items for this channel
|
|
const cascadeMonitored = mode === 'all' || mode === 'existing';
|
|
await db
|
|
.update(contentItems)
|
|
.set({
|
|
monitored: cascadeMonitored,
|
|
updatedAt: sql`(datetime('now'))`,
|
|
})
|
|
.where(eq(contentItems.channelId, id));
|
|
|
|
// Step 2: Update the channel's monitoringMode and monitoringEnabled
|
|
const result = await db
|
|
.update(channels)
|
|
.set({
|
|
monitoringMode: mode,
|
|
monitoringEnabled: mode !== 'none',
|
|
updatedAt: sql`(datetime('now'))`,
|
|
})
|
|
.where(eq(channels.id, id))
|
|
.returning();
|
|
|
|
return result.length > 0 ? mapRow(result[0]) : null;
|
|
}
|
|
|
|
/** Delete a channel by ID. Returns true if a row was deleted. */
|
|
export async function deleteChannel(
|
|
db: Db,
|
|
id: number
|
|
): Promise<boolean> {
|
|
const result = await db
|
|
.delete(channels)
|
|
.where(eq(channels.id, id))
|
|
.returning({ id: channels.id });
|
|
|
|
return result.length > 0;
|
|
}
|
|
|
|
/** Find a channel by platform and platformId (for duplicate detection). */
|
|
export async function getChannelByPlatformId(
|
|
db: Db,
|
|
platform: string,
|
|
platformId: string
|
|
): Promise<Channel | null> {
|
|
const rows = await db
|
|
.select()
|
|
.from(channels)
|
|
.where(
|
|
and(
|
|
eq(channels.platform, platform),
|
|
eq(channels.platformId, platformId)
|
|
)
|
|
)
|
|
.limit(1);
|
|
|
|
return rows.length > 0 ? mapRow(rows[0]) : null;
|
|
}
|
|
|
|
// ── Row Mapping ──
|
|
|
|
/**
|
|
* Map a raw Drizzle row to the Channel domain type.
|
|
* Ensures boolean and nullable fields are correctly typed.
|
|
*/
|
|
function mapRow(row: typeof channels.$inferSelect): Channel {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
platform: row.platform as Platform,
|
|
platformId: row.platformId,
|
|
url: row.url,
|
|
monitoringEnabled: row.monitoringEnabled,
|
|
checkInterval: row.checkInterval,
|
|
imageUrl: row.imageUrl,
|
|
metadata: row.metadata as Record<string, unknown> | null,
|
|
formatProfileId: row.formatProfileId,
|
|
monitoringMode: (row.monitoringMode ?? 'all') as Channel['monitoringMode'],
|
|
createdAt: row.createdAt,
|
|
updatedAt: row.updatedAt,
|
|
lastCheckedAt: row.lastCheckedAt,
|
|
lastCheckStatus: row.lastCheckStatus as Channel['lastCheckStatus'],
|
|
};
|
|
}
|