Release 0.5.9
This commit is contained in:
1 parent
2851c7d936
commit
c3033479b5
32 files changed
+1598
-50
No files matched your search
@@ -21,6 +21,19 @@ export type PieceSourceInput = {
|
||||
timecodeSeconds: number | null;
|
||||
};
|
||||
|
||||
export type PieceToolInput = {
|
||||
toolId: string;
|
||||
workUrl: string;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type PieceTool = PieceToolInput & {
|
||||
id: string;
|
||||
name: string;
|
||||
startUrl: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export type PieceInput = {
|
||||
title: string;
|
||||
outputTitle: string;
|
||||
@@ -48,6 +61,7 @@ export type PieceInput = {
|
||||
topicIds: string[];
|
||||
categoryIds: string[];
|
||||
sources: PieceSourceInput[];
|
||||
tools?: PieceToolInput[];
|
||||
};
|
||||
|
||||
export type PieceListItem = {
|
||||
@@ -63,15 +77,18 @@ export type PieceListItem = {
|
||||
topics: { id: string; name: string }[];
|
||||
sourceCount: number;
|
||||
mediaId: string | null;
|
||||
isFavorite: boolean;
|
||||
};
|
||||
|
||||
export type PieceEditor = Omit<PieceInput, 'status'> & {
|
||||
export type PieceEditor = Omit<PieceInput, 'status' | 'tools'> & {
|
||||
id: string;
|
||||
status: PieceStatus;
|
||||
revision: number;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
media: PieceMedia[];
|
||||
isFavorite: boolean;
|
||||
tools: PieceTool[];
|
||||
};
|
||||
|
||||
export type PieceMedia = {
|
||||
@@ -123,6 +140,32 @@ function validUrl(value: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function cleanPieceTools(values: PieceToolInput[] = []): PieceToolInput[] {
|
||||
if (values.length > 20) throw new Error('INVALID_TOOLS');
|
||||
const ids = uniqueIds(values.map((value) => value.toolId));
|
||||
if (ids.length !== values.length) throw new Error('INVALID_TOOLS');
|
||||
if (ids.length) {
|
||||
const placeholders = ids.map(() => '?').join(', ');
|
||||
const row = getDatabase()
|
||||
.prepare(`SELECT count(*) AS count FROM tools WHERE id IN (${placeholders})`)
|
||||
.get(...ids) as { count: number };
|
||||
if (row.count !== ids.length) throw new Error('INVALID_TOOLS');
|
||||
}
|
||||
return values.map((value) => {
|
||||
const workUrl = cleanText(value.workUrl, 2_000, 'tools');
|
||||
if (workUrl) {
|
||||
if (!validUrl(workUrl)) throw new Error('INVALID_TOOLS');
|
||||
const parsed = new URL(workUrl);
|
||||
if (parsed.username || parsed.password) throw new Error('INVALID_TOOLS');
|
||||
}
|
||||
return {
|
||||
toolId: value.toolId,
|
||||
workUrl,
|
||||
note: cleanText(value.note, 2_000, 'tools')
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function requireTaxonomyIds(table: 'formats' | 'topics' | 'section_categories', ids: string[]) {
|
||||
if (!ids.length) return;
|
||||
const placeholders = ids.map(() => '?').join(', ');
|
||||
@@ -213,7 +256,8 @@ function validatePieceInput(input: PieceInput): PieceInput {
|
||||
presentationMetadataFetchedAtUtc: input.presentationMetadataFetchedAtUtc || null,
|
||||
topicIds,
|
||||
categoryIds,
|
||||
sources
|
||||
sources,
|
||||
tools: cleanPieceTools(input.tools)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -244,6 +288,7 @@ type PieceListRow = {
|
||||
format: string;
|
||||
source_count: number;
|
||||
media_id: string | null;
|
||||
is_favorite: number;
|
||||
};
|
||||
|
||||
export function listPieces(filters: {
|
||||
@@ -288,13 +333,14 @@ export function listPieces(filters: {
|
||||
const database = getDatabase();
|
||||
const rows = database
|
||||
.prepare(
|
||||
`SELECT p.id, p.title, p.summary, p.status, p.revision, p.duration_seconds,
|
||||
`SELECT p.id, p.title, p.summary, p.status, p.revision, p.duration_seconds, p.is_favorite,
|
||||
p.format_id, COALESCE(f.name, 'Ohne Format') AS format,
|
||||
(SELECT count(*) FROM piece_sources ps WHERE ps.piece_id = p.id) AS source_count,
|
||||
(SELECT pm.id FROM piece_media pm WHERE pm.piece_id = p.id ORDER BY pm.position LIMIT 1) AS media_id
|
||||
FROM pieces p LEFT JOIN formats f ON f.id = p.format_id
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
ORDER BY CASE p.status WHEN 'ready' THEN 0 WHEN 'preparing' THEN 1 WHEN 'inbox' THEN 2 ELSE 3 END,
|
||||
ORDER BY p.is_favorite DESC,
|
||||
CASE p.status WHEN 'ready' THEN 0 WHEN 'preparing' THEN 1 WHEN 'inbox' THEN 2 ELSE 3 END,
|
||||
p.updated_at_utc DESC, p.title`
|
||||
)
|
||||
.all(...parameters) as PieceListRow[];
|
||||
@@ -319,7 +365,8 @@ export function listPieces(filters: {
|
||||
categories: categoryStatement.all(row.id) as PieceListItem['categories'],
|
||||
topics: topicStatement.all(row.id) as PieceListItem['topics'],
|
||||
sourceCount: row.source_count,
|
||||
mediaId: row.media_id
|
||||
mediaId: row.media_id,
|
||||
isFavorite: row.is_favorite === 1
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -351,6 +398,7 @@ type PieceRow = {
|
||||
revision: number;
|
||||
created_at_utc: string;
|
||||
updated_at_utc: string;
|
||||
is_favorite: number;
|
||||
};
|
||||
|
||||
export function getPiece(id: string): PieceEditor | null {
|
||||
@@ -363,7 +411,7 @@ export function getPiece(id: string): PieceEditor | null {
|
||||
presentation_image_url, presentation_site_name, presentation_provider,
|
||||
presentation_provider_id, presentation_title_manual, presentation_description_manual,
|
||||
presentation_image_manual, presentation_metadata_fetched_at_utc,
|
||||
revision, created_at_utc, updated_at_utc
|
||||
revision, created_at_utc, updated_at_utc, is_favorite
|
||||
FROM pieces WHERE id = ?`
|
||||
)
|
||||
.get(id) as PieceRow | undefined;
|
||||
@@ -396,6 +444,7 @@ export function getPiece(id: string): PieceEditor | null {
|
||||
revision: row.revision,
|
||||
createdAtUtc: row.created_at_utc,
|
||||
updatedAtUtc: row.updated_at_utc,
|
||||
isFavorite: row.is_favorite === 1,
|
||||
topicIds: (
|
||||
database.prepare('SELECT topic_id FROM piece_topics WHERE piece_id = ?').all(id) as {
|
||||
topic_id: string;
|
||||
@@ -438,6 +487,31 @@ export function getPiece(id: string): PieceEditor | null {
|
||||
byteSize: media.byte_size,
|
||||
altText: media.alt_text,
|
||||
position: media.position
|
||||
})),
|
||||
tools: (
|
||||
database
|
||||
.prepare(
|
||||
`SELECT pt.id, pt.tool_id, pt.work_url, pt.note, t.name, t.start_url, t.is_active
|
||||
FROM piece_tools pt JOIN tools t ON t.id = pt.tool_id
|
||||
WHERE pt.piece_id = ? ORDER BY pt.position`
|
||||
)
|
||||
.all(id) as {
|
||||
id: string;
|
||||
tool_id: string;
|
||||
work_url: string;
|
||||
note: string;
|
||||
name: string;
|
||||
start_url: string;
|
||||
is_active: number;
|
||||
}[]
|
||||
).map((tool) => ({
|
||||
id: tool.id,
|
||||
toolId: tool.tool_id,
|
||||
workUrl: tool.work_url,
|
||||
note: tool.note,
|
||||
name: tool.name,
|
||||
startUrl: tool.start_url,
|
||||
isActive: tool.is_active === 1
|
||||
}))
|
||||
};
|
||||
}
|
||||
@@ -460,6 +534,21 @@ function replaceRelations(pieceId: string, input: PieceInput) {
|
||||
input.sources.forEach((source, position) =>
|
||||
addSource.run(randomUUID(), pieceId, source.label, source.url, source.timecodeSeconds, position)
|
||||
);
|
||||
database.prepare('DELETE FROM piece_tools WHERE piece_id = ?').run(pieceId);
|
||||
const addTool = database.prepare(
|
||||
`INSERT INTO piece_tools (id, piece_id, tool_id, work_url, note, position)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
(input.tools ?? []).forEach((tool, position) =>
|
||||
addTool.run(randomUUID(), pieceId, tool.toolId, tool.workUrl, tool.note, position)
|
||||
);
|
||||
}
|
||||
|
||||
export function setPieceFavorite(id: string, favorite: boolean) {
|
||||
const result = getDatabase()
|
||||
.prepare('UPDATE pieces SET is_favorite = ? WHERE id = ?')
|
||||
.run(favorite ? 1 : 0, id);
|
||||
if (result.changes !== 1) throw new Error('NOT_FOUND');
|
||||
}
|
||||
|
||||
export function savePiece(id: string, expectedRevision: number, rawInput: PieceInput) {
|
||||
|
||||
Reference in new issue
Block a user