fix: use name+tmdbid primary key instead of auto incmremented id

This commit is contained in:
Sam Chau
2026-01-19 04:12:37 +10:30
parent 95930edc53
commit 4f565ebd6f
13 changed files with 87 additions and 85 deletions

View File

@@ -33,7 +33,7 @@ export async function getAllConditionsForEvaluation(
// Get all conditions for all formats
const conditions = await db
.selectFrom('custom_format_conditions')
.select(['id', 'custom_format_id', 'name', 'type', 'negate', 'required'])
.select(['id', 'custom_format_id', 'name', 'type', 'arr_type', 'negate', 'required'])
.execute();
if (conditions.length === 0) {
@@ -198,6 +198,7 @@ export async function getAllConditionsForEvaluation(
id: c.id,
name: c.name,
type: c.type,
arrType: c.arr_type as 'all' | 'radarr' | 'sonarr',
negate: c.negate === 1,
required: c.required === 1,
patterns: patternsMap.get(c.id),

View File

@@ -6,7 +6,8 @@ import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
export interface CreateTestReleaseInput {
entityId: number;
entityType: 'movie' | 'series';
entityTmdbId: number;
title: string;
size_bytes: number | null;
languages: string[];
@@ -31,7 +32,8 @@ export async function createRelease(options: CreateTestReleaseOptions) {
const insertRelease = db
.insertInto('test_releases')
.values({
test_entity_id: input.entityId,
entity_type: input.entityType,
entity_tmdb_id: input.entityTmdbId,
title: input.title,
size_bytes: input.size_bytes,
languages: JSON.stringify(input.languages),

View File

@@ -6,7 +6,8 @@ import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
export interface CreateTestReleasesInput {
entityId: number;
entityType: 'movie' | 'series';
entityTmdbId: number;
title: string;
size_bytes: number | null;
languages: string[];
@@ -37,14 +38,16 @@ export async function createReleases(options: CreateTestReleasesOptions) {
};
}
// Get the entity ID (all inputs should have the same entityId)
const entityId = inputs[0].entityId;
// Get the entity key (all inputs should have the same entity)
const entityType = inputs[0].entityType;
const entityTmdbId = inputs[0].entityTmdbId;
// Check for existing releases for this entity
const existingReleases = await db
.selectFrom('test_releases')
.select(['title'])
.where('test_entity_id', '=', entityId)
.where('entity_type', '=', entityType)
.where('entity_tmdb_id', '=', entityTmdbId)
.execute();
const existingTitles = new Set(existingReleases.map((r) => r.title));
@@ -69,7 +72,8 @@ export async function createReleases(options: CreateTestReleasesOptions) {
const insertRelease = db
.insertInto('test_releases')
.values({
test_entity_id: input.entityId,
entity_type: input.entityType,
entity_tmdb_id: input.entityTmdbId,
title: input.title,
size_bytes: input.size_bytes,
languages: JSON.stringify(input.languages),

View File

@@ -9,34 +9,42 @@ export interface DeleteTestEntityOptions {
databaseId: number;
cache: PCDCache;
layer: OperationLayer;
entityId: number;
entityType: 'movie' | 'series';
entityTmdbId: number;
entityTitle: string; // For metadata
}
/**
* Delete a test entity and its releases by writing an operation to the specified layer
* Uses stable composite key (type, tmdb_id) instead of auto-generated id
*/
export async function remove(options: DeleteTestEntityOptions) {
const { databaseId, cache, layer, entityId } = options;
const { databaseId, cache, layer, entityType, entityTmdbId, entityTitle } = options;
const db = cache.kb;
// Delete releases first (foreign key constraint)
// Delete releases first (uses composite FK)
const deleteReleases = db
.deleteFrom('test_releases')
.where('test_entity_id', '=', entityId)
.where('entity_type', '=', entityType)
.where('entity_tmdb_id', '=', entityTmdbId)
.compile();
// Delete the entity
const deleteEntity = db.deleteFrom('test_entities').where('id', '=', entityId).compile();
// Delete the entity using stable composite key
const deleteEntity = db
.deleteFrom('test_entities')
.where('type', '=', entityType)
.where('tmdb_id', '=', entityTmdbId)
.compile();
const result = await writeOperation({
databaseId,
layer,
description: `delete-test-entity-${entityId}`,
description: `delete-test-entity-${entityTitle.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`,
queries: [deleteReleases, deleteEntity],
metadata: {
operation: 'delete',
entity: 'test_entity',
name: `id:${entityId}`
name: entityTitle
}
});

View File

@@ -28,14 +28,13 @@ export async function list(cache: PCDCache) {
if (entities.length === 0) return [];
const entityIds = entities.map((e) => e.id);
// 2. Get all releases for all entities
const allReleases = await db
.selectFrom('test_releases')
.select([
'id',
'test_entity_id',
'entity_type',
'entity_tmdb_id',
'title',
'size_bytes',
'languages',
@@ -44,24 +43,25 @@ export async function list(cache: PCDCache) {
'created_at',
'updated_at'
])
.where('test_entity_id', 'in', entityIds)
.orderBy('test_entity_id')
.orderBy('entity_type')
.orderBy('entity_tmdb_id')
.orderBy('title')
.execute();
// Build releases map
const releasesMap = new Map<number, typeof allReleases>();
// Build releases map using composite key
const releasesMap = new Map<string, typeof allReleases>();
for (const release of allReleases) {
if (!releasesMap.has(release.test_entity_id)) {
releasesMap.set(release.test_entity_id, []);
const key = `${release.entity_type}-${release.entity_tmdb_id}`;
if (!releasesMap.has(key)) {
releasesMap.set(key, []);
}
releasesMap.get(release.test_entity_id)!.push(release);
releasesMap.get(key)!.push(release);
}
// Build the final result
return entities.map((entity) => ({
...entity,
releases: (releasesMap.get(entity.id) || []).map((r) => ({
releases: (releasesMap.get(`${entity.type}-${entity.tmdb_id}`) || []).map((r) => ({
id: r.id,
title: r.title,
size_bytes: r.size_bytes !== null ? Number(r.size_bytes) : null,

View File

@@ -221,7 +221,8 @@ export interface TestEntitiesTable {
export interface TestReleasesTable {
id: Generated<number>;
test_entity_id: number;
entity_type: 'movie' | 'series';
entity_tmdb_id: number;
title: string;
size_bytes: number | null;
languages: string; // JSON array

View File

@@ -237,36 +237,3 @@ export function groupSonarrReleases(
return groups;
}
// =============================================================================
// Test Release Conversion
// =============================================================================
/**
* Shape for PCD test release creation
*/
export interface TestReleaseInput {
entityId: number;
title: string;
size_bytes: number | null;
languages: string[];
indexers: string[];
flags: string[];
}
/**
* Convert grouped releases to test release inputs
*/
export function groupedReleasesToTestInputs(
groupedReleases: GroupedRelease[],
entityId: number
): TestReleaseInput[] {
return groupedReleases.map((r) => ({
entityId,
title: r.title,
size_bytes: r.size,
languages: r.languages,
indexers: r.indexers,
flags: r.flags
}));
}

View File

@@ -163,11 +163,17 @@ export const actions: Actions = {
}
const formData = await request.formData();
const entityId = parseInt(formData.get('entityId') as string, 10);
const entityType = formData.get('entityType') as 'movie' | 'series';
const entityTmdbId = parseInt(formData.get('entityTmdbId') as string, 10);
const entityTitle = formData.get('entityTitle') as string;
const layer = (formData.get('layer') as 'user' | 'base') || 'user';
if (isNaN(entityId)) {
return fail(400, { error: 'Invalid entity ID' });
if (!entityType || !['movie', 'series'].includes(entityType)) {
return fail(400, { error: 'Invalid entity type' });
}
if (isNaN(entityTmdbId)) {
return fail(400, { error: 'Invalid entity TMDB ID' });
}
if (layer === 'base' && !canWriteToBase(currentDatabaseId)) {
@@ -178,7 +184,9 @@ export const actions: Actions = {
databaseId: currentDatabaseId,
cache,
layer,
entityId
entityType,
entityTmdbId,
entityTitle: entityTitle || 'Unknown'
});
if (!result.success) {
@@ -214,7 +222,8 @@ export const actions: Actions = {
}
let release: {
entityId: number;
entityType: 'movie' | 'series';
entityTmdbId: number;
title: string;
size_bytes: number | null;
languages: string[];
@@ -377,7 +386,8 @@ export const actions: Actions = {
}
let releases: Array<{
entityId: number;
entityType: 'movie' | 'series';
entityTmdbId: number;
title: string;
size_bytes: number | null;
languages: string[];

View File

@@ -173,7 +173,8 @@
// Release modal state
let showReleaseModal = false;
let releaseModalMode: 'create' | 'edit' = 'create';
let releaseEntityId: number = 0;
let releaseEntityType: 'movie' | 'series' = 'movie';
let releaseEntityTmdbId: number = 0;
let currentRelease: TestRelease | null = null;
// Release delete modal state
@@ -284,15 +285,17 @@
}
// Release modal handlers
function handleAddRelease(e: CustomEvent<{ entityId: number }>) {
releaseEntityId = e.detail.entityId;
function handleAddRelease(e: CustomEvent<{ entityType: 'movie' | 'series'; entityTmdbId: number }>) {
releaseEntityType = e.detail.entityType;
releaseEntityTmdbId = e.detail.entityTmdbId;
releaseModalMode = 'create';
currentRelease = null;
showReleaseModal = true;
}
function handleEditRelease(e: CustomEvent<{ entityId: number; release: TestRelease }>) {
releaseEntityId = e.detail.entityId;
function handleEditRelease(e: CustomEvent<{ entityType: 'movie' | 'series'; entityTmdbId: number; release: TestRelease }>) {
releaseEntityType = e.detail.entityType;
releaseEntityTmdbId = e.detail.entityTmdbId;
releaseModalMode = 'edit';
currentRelease = e.detail.release;
showReleaseModal = true;
@@ -490,7 +493,8 @@
<ReleaseModal
bind:open={showReleaseModal}
mode={releaseModalMode}
entityId={releaseEntityId}
entityType={releaseEntityType}
entityTmdbId={releaseEntityTmdbId}
release={currentRelease}
canWriteToBase={data.canWriteToBase}
/>

View File

@@ -24,9 +24,9 @@
const dispatch = createEventDispatcher<{
confirmDelete: { entity: TestEntity; formRef: HTMLFormElement };
addRelease: { entityId: number };
addRelease: { entityType: 'movie' | 'series'; entityTmdbId: number };
importReleases: { entity: TestEntity };
editRelease: { entityId: number; release: TestRelease };
editRelease: { entityType: 'movie' | 'series'; entityTmdbId: number; release: TestRelease };
confirmDeleteRelease: { release: TestRelease; formRef: HTMLFormElement };
expand: { entity: TestEntity };
}>();
@@ -160,7 +160,9 @@
};
}}
>
<input type="hidden" name="entityId" value={row.id} />
<input type="hidden" name="entityType" value={row.type} />
<input type="hidden" name="entityTmdbId" value={row.tmdb_id} />
<input type="hidden" name="entityTitle" value={row.title} />
<input type="hidden" name="layer" value={deleteLayer} />
<TableActionButton
icon={Trash2}
@@ -187,8 +189,8 @@
</div>
{:else}
<ReleaseTable
entityId={row.id}
entityType={row.type}
entityTmdbId={row.tmdb_id}
releases={row.releases}
{evaluations}
{selectedProfileId}

View File

@@ -318,7 +318,8 @@
releases
.filter((r) => selectedReleases.has(r.title))
.map((r) => ({
entityId: entity?.id ?? 0,
entityType: entity?.type ?? 'movie',
entityTmdbId: entity?.tmdb_id ?? 0,
title: r.title,
size_bytes: r.size,
languages: r.languages,

View File

@@ -9,7 +9,8 @@
export let open = false;
export let mode: 'create' | 'edit' = 'create';
export let entityId: number;
export let entityType: 'movie' | 'series';
export let entityTmdbId: number;
export let release: {
id?: number;
title: string;
@@ -86,7 +87,8 @@
// Build JSON for form submission
$: releaseJson = JSON.stringify({
id: release?.id,
entityId,
entityType,
entityTmdbId,
title,
size_bytes: gbToBytes(sizeGb),
languages,

View File

@@ -14,8 +14,8 @@
type ReleaseEvaluation = components['schemas']['ReleaseEvaluation'];
export let entityId: number;
export let entityType: 'movie' | 'series';
export let entityTmdbId: number;
export let releases: TestRelease[];
export let evaluations: Record<number, ReleaseEvaluation>;
export let selectedProfileId: number | null;
@@ -57,8 +57,8 @@
}
const dispatch = createEventDispatcher<{
add: { entityId: number };
edit: { entityId: number; release: TestRelease };
add: { entityType: 'movie' | 'series'; entityTmdbId: number };
edit: { entityType: 'movie' | 'series'; entityTmdbId: number; release: TestRelease };
confirmDelete: { release: TestRelease; formRef: HTMLFormElement };
}>();
@@ -165,7 +165,7 @@
title="Edit release"
variant="accent"
size="sm"
on:click={() => dispatch('edit', { entityId, release })}
on:click={() => dispatch('edit', { entityType, entityTmdbId, release })}
/>
<form
id={releaseFormId}
@@ -276,7 +276,7 @@
<!-- Clickable add row -->
<button
type="button"
on:click={() => dispatch('add', { entityId })}
on:click={() => dispatch('add', { entityType, entityTmdbId })}
class="w-full rounded-lg border-2 border-dashed border-neutral-200 py-3 text-sm text-neutral-400 transition-colors hover:border-accent-300 hover:bg-accent-50/50 hover:text-accent-600 dark:border-neutral-700 dark:hover:border-accent-600 dark:hover:bg-accent-900/10 dark:hover:text-accent-400"
>
<span class="inline-flex items-center gap-1">