feat: make preview_image_url nullable across post-related DTOs and database schema
Some checks failed
Frontend CI / build (push) Failing after 56s

This commit is contained in:
SquidSpirit 2025-10-14 21:56:51 +08:00
parent df5bba022e
commit 4c4c5c357e
16 changed files with 56 additions and 41 deletions

View File

@ -12,8 +12,8 @@ pub struct CreatePostRequestDto {
pub content: String,
pub label_ids: Vec<i32>,
#[schema(format = Uri)]
pub preview_image_url: String,
#[schema(required, format = Uri)]
pub preview_image_url: Option<String>,
#[schema(required, format = DateTime)]
pub published_time: Option<String>,

View File

@ -14,7 +14,7 @@ pub struct PostInfoResponseDto {
pub labels: Vec<LabelResponseDto>,
#[schema(format = Uri)]
pub preview_image_url: String,
pub preview_image_url: Option<String>,
#[schema(format = DateTime)]
pub published_time: Option<String>,

View File

@ -12,8 +12,8 @@ pub struct UpdatePostRequestDto {
pub content: String,
pub label_ids: Vec<i32>,
#[schema(format = Uri)]
pub preview_image_url: String,
#[schema(required, format = Uri)]
pub preview_image_url: Option<String>,
#[schema(required, format = DateTime)]
pub published_time: Option<String>,

View File

@ -7,7 +7,7 @@ pub struct PostInfoMapper {
pub semantic_id: String,
pub title: String,
pub description: String,
pub preview_image_url: String,
pub preview_image_url: Option<String>,
pub published_time: Option<NaiveDateTime>,
pub labels: Vec<LabelMapper>,
}

View File

@ -10,7 +10,7 @@ pub struct PostInfo {
pub semantic_id: String,
pub title: String,
pub description: String,
pub preview_image_url: String,
pub preview_image_url: Option<String>,
pub labels: Vec<Label>,
pub published_time: Option<DateTime<Utc>>,
}

View File

@ -6,7 +6,7 @@ pub struct PostInfoWithLabelRecord {
pub semantic_id: String,
pub title: String,
pub description: String,
pub preview_image_url: String,
pub preview_image_url: Option<String>,
pub published_time: Option<NaiveDateTime>,
pub label_id: Option<i32>,

View File

@ -6,7 +6,7 @@ pub struct PostWithLabelRecord {
pub semantic_id: String,
pub title: String,
pub description: String,
pub preview_image_url: String,
pub preview_image_url: Option<String>,
pub content: String,
pub published_time: Option<NaiveDateTime>,

View File

@ -0,0 +1,2 @@
-- Revert post.preview_image_url to NOT NULL
ALTER TABLE "post" ALTER COLUMN "preview_image_url" SET NOT NULL;

View File

@ -0,0 +1,2 @@
-- Make post.preview_image_url nullable
ALTER TABLE "post" ALTER COLUMN "preview_image_url" DROP NOT NULL;

View File

@ -6,7 +6,7 @@ export class CreatePostRequestDto {
readonly description: string;
readonly content: string;
readonly labelIds: number[];
readonly previewImageUrl: URL;
readonly previewImageUrl: URL | null;
readonly publishedTime: Date | null;
private constructor(props: {
@ -15,7 +15,7 @@ export class CreatePostRequestDto {
description: string;
content: string;
labelIds: number[];
previewImageUrl: URL;
previewImageUrl: URL | null;
publishedTime: Date | null;
}) {
this.semanticId = props.semanticId;
@ -33,7 +33,7 @@ export class CreatePostRequestDto {
description: '',
content: '',
labelIds: [],
previewImageUrl: new URL('https://example.com'),
previewImageUrl: null,
publishedTime: null,
});
}

View File

@ -7,7 +7,7 @@ export const PostInfoResponseSchema = z.object({
semantic_id: z.string(),
title: z.string(),
description: z.string(),
preview_image_url: z.url(),
preview_image_url: z.url().nullable(),
labels: z.array(LabelResponseSchema),
published_time: z.iso.datetime({ offset: true }).nullable(),
});
@ -17,7 +17,7 @@ export class PostInfoResponseDto {
readonly semanticId: string;
readonly title: string;
readonly description: string;
readonly previewImageUrl: URL;
readonly previewImageUrl: URL | null;
readonly labels: readonly LabelResponseDto[];
readonly publishedTime: Date | null;
@ -26,7 +26,7 @@ export class PostInfoResponseDto {
semanticId: string;
title: string;
description: string;
previewImageUrl: URL;
previewImageUrl: URL | null;
labels: LabelResponseDto[];
publishedTime: Date | null;
}) {
@ -47,12 +47,17 @@ export class PostInfoResponseDto {
published_time = new Date(parsedJson.published_time);
}
let preview_image_url: URL | null = null;
if (parsedJson.preview_image_url !== null) {
preview_image_url = new URL(parsedJson.preview_image_url);
}
return new PostInfoResponseDto({
id: parsedJson.id,
semanticId: parsedJson.semantic_id,
title: parsedJson.title,
description: parsedJson.description,
previewImageUrl: new URL(parsedJson.preview_image_url),
previewImageUrl: preview_image_url,
labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)),
publishedTime: published_time,
});

View File

@ -9,7 +9,7 @@ export class PostInfoViewModel {
readonly semanticId: string;
readonly title: string;
readonly description: string;
readonly previewImageUrl: URL;
readonly previewImageUrl: URL | null;
readonly labels: readonly LabelViewModel[];
readonly publishedTime: Date | null;
@ -18,7 +18,7 @@ export class PostInfoViewModel {
semanticId: string;
title: string;
description: string;
previewImageUrl: URL;
previewImageUrl: URL | null;
labels: readonly LabelViewModel[];
publishedTime: Date | null;
}) {
@ -49,12 +49,17 @@ export class PostInfoViewModel {
publishedTime = new Date(props.publishedTime);
}
let previewImageUrl: URL | null = null;
if (props.previewImageUrl) {
previewImageUrl = new URL(props.previewImageUrl);
}
return new PostInfoViewModel({
id: props.id,
semanticId: props.semanticId,
title: props.title,
description: props.description,
previewImageUrl: new URL(props.previewImageUrl),
previewImageUrl: previewImageUrl,
labels: props.labels.map((label) => LabelViewModel.rehydrate(label)),
publishedTime: publishedTime,
});
@ -74,7 +79,7 @@ export class PostInfoViewModel {
semanticId: this.semanticId,
title: this.title,
description: this.description,
previewImageUrl: this.previewImageUrl.href,
previewImageUrl: this.previewImageUrl?.href ?? null,
labels: this.labels.map((label) => label.dehydrate()),
publishedTime: this.publishedTime?.getTime() ?? null,
};
@ -86,7 +91,7 @@ export interface DehydratedPostInfoProps {
semanticId: string;
title: string;
description: string;
previewImageUrl: string;
previewImageUrl: string | null;
labels: DehydratedLabelProps[];
publishedTime: number | null;
}

View File

@ -5,7 +5,7 @@ export class PostInfo {
readonly semanticId: string;
readonly title: string;
readonly description: string;
readonly previewImageUrl: URL;
readonly previewImageUrl: URL | null;
readonly labels: readonly Label[];
readonly publishedTime: Date | null;
@ -14,7 +14,7 @@ export class PostInfo {
semanticId: string;
title: string;
description: string;
previewImageUrl: URL;
previewImageUrl: URL | null;
labels: readonly Label[];
publishedTime: Date | null;
}) {

View File

@ -14,27 +14,28 @@
const md = markdownit();
const parsedContent = $derived(state.data?.content ? md.render(state.data.content) : '');
const postInfo = $derived(state.data?.info);
onMount(() => postBloc.dispatch({ event: PostEventType.PostLoadedEvent, id: id }));
</script>
<svelte:head>
<title>{generateTitle(state.data?.info.title)}</title>
{#if state.data}
<meta name="description" content={state.data.info.description} />
{#if state.data.info.isPublished}
<title>{generateTitle(postInfo?.title)}</title>
{#if postInfo}
<meta name="description" content={postInfo.description} />
{#if postInfo.isPublished}
<StructuredData
headline={state.data.info.title}
description={state.data.info.description}
datePublished={state.data.info.publishedTime!}
image={state.data.info.previewImageUrl}
headline={postInfo.title}
description={postInfo.description}
datePublished={postInfo.publishedTime!}
image={postInfo.previewImageUrl}
/>
{/if}
{/if}
</svelte:head>
<article class="content-container prose pb-10 prose-gray">
{#if state.data}
<PostContentHeader postInfo={state.data.info} />
<article class="content-container prose prose-gray pb-10">
{#if postInfo}
<PostContentHeader {postInfo} />
<div class="max-w-3xl">
<hr />
<SafeHtml html={parsedContent} />

View File

@ -7,11 +7,11 @@
let isImageLoading = $state(true);
let isImageError = $state(false);
function handleImageLoad() {
function onImageLoad() {
isImageLoading = false;
}
function handleImageError() {
function onImageError() {
isImageLoading = false;
isImageError = true;
}
@ -27,10 +27,10 @@
class="rounded-2xl object-cover transition-opacity duration-300
{isImageLoading ? 'opacity-0' : 'opacity-100'}
{isImageError ? 'hidden' : ''}"
src={postInfo.previewImageUrl.href}
src={postInfo.previewImageUrl?.href}
alt={postInfo.title}
onload={handleImageLoad}
onerror={handleImageError}
onload={onImageLoad}
onerror={onImageError}
/>
{#if isImageLoading || isImageError}
<div class="absolute inset-0 flex items-center justify-center bg-gray-200"></div>

View File

@ -10,7 +10,7 @@
headline: string;
description: string;
datePublished: Date;
image: URL;
image: URL | null;
} = $props();
const structuredData = $derived({
@ -19,7 +19,7 @@
headline: headline,
description: description,
datePublished: datePublished.toISOString(),
image: image.href,
image: image?.href,
});
const jsonLdScript = $derived(