Compare commits

..

5 Commits

Author SHA1 Message Date
dade012888 Merge pull request 'hotfix/payload-banner' (#28) from hotfix/payload-banner into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #28
2026-03-25 17:57:49 +07:00
4001aec6ef ♻️ refactor: refine payload before sending to frontend
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 47s
2026-03-25 17:55:48 +07:00
dd70f9f9d4 ♻️ refactor: restructure banner select payload 2026-03-25 17:49:16 +07:00
26909154ab Merge pull request 'prune-banner' (#27) from prune-banner into main
All checks were successful
Sync to GitHub / sync (push) Successful in 9s
Reviewed-on: #27
2026-03-25 12:52:55 +07:00
7f6b1373f4 💥 breaking: update endpoint to support new banner schema
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 1m42s
2026-03-25 12:39:23 +07:00
5 changed files with 61 additions and 63 deletions

View File

@ -20,6 +20,25 @@ export const findAllActiveHeroBannerRepository = async () => {
startDate: "asc", startDate: "asc",
}, },
], ],
select: {
orderPriority: true,
imageUrl: true,
media: {
select: {
id: true,
title: true,
slug: true,
pictureLarge: true,
synopsis: true,
genres: {
select: {
slug: true,
name: true,
},
},
},
},
},
}); });
} catch (error) { } catch (error) {
throw new AppError(500, "Failed to fetch active hero banners", error); throw new AppError(500, "Failed to fetch active hero banners", error);

View File

@ -8,26 +8,31 @@ import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllAc
export const getActiveHeroBannerService = async () => { export const getActiveHeroBannerService = async () => {
try { try {
// Check if Hero Banner is enabled in system preferences // Check if Hero Banner is enabled in system preferences
const isHeroBannerEnabled = await findSystemPreferenceService( const isHeroBannerEnabled = await findSystemPreferenceService("HERO_BANNER_ENABLED", "boolean");
"HERO_BANNER_ENABLED", if (!isHeroBannerEnabled) throw new AppError(403, "Hero Banner is disabled");
"boolean",
);
if (!isHeroBannerEnabled)
throw new AppError(403, "Hero Banner is disabled");
// Try to get active banners from Redis cache // Try to get active banners from Redis cache
const cachedBanners = await redis.get( const cachedBanners = await redis.get(`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`);
`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`,
);
if (cachedBanners) return JSON.parse(cachedBanners); if (cachedBanners) return JSON.parse(cachedBanners);
// If not in cache, fetch from database and cache the result // If not in cache, fetch from database and cache the result
const activeBanners = await findAllActiveHeroBannerRepository(); const activeBanners = await findAllActiveHeroBannerRepository();
const constructedBanners = activeBanners.map((banner) => ({
id: banner.media.id,
title: banner.media.title,
slug: banner.media.slug,
imageUrl: banner.imageUrl || banner.media.pictureLarge,
synopsis: banner.media.synopsis,
genres: banner.media.genres.map((genre) => ({
slug: genre.slug,
name: genre.name,
})),
}));
await redis.set( await redis.set(
`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`, `${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`,
JSON.stringify(activeBanners), JSON.stringify(constructedBanners),
); );
return activeBanners; return constructedBanners;
} catch (error) { } catch (error) {
ErrorForwarder(error); ErrorForwarder(error);
} }

View File

@ -1,12 +1,12 @@
import { Prisma } from "@prisma/client";
import { AppError } from "../../../helpers/error/instances/app"; import { AppError } from "../../../helpers/error/instances/app";
import { prisma } from "../../../utils/databases/prisma/connection"; import { prisma } from "../../../utils/databases/prisma/connection";
import { generateUUIDv7 } from "../../../helpers/databases/uuidv7"; import { generateUUIDv7 } from "../../../helpers/databases/uuidv7";
import { SystemAccountId } from "../../../config/account/system"; import { SystemAccountId } from "../../../config/account/system";
import { Static } from "elysia";
import { createHeroBannerSchema } from "../schemas/createHeroBanner.schema";
import { Prisma } from "@prisma/client";
export const insertHeroBannerRepository = async ( export const insertHeroBannerRepository = async (payload: Static<typeof createHeroBannerSchema.body>) => {
payload: Omit<Prisma.HeroBannerCreateInput, "id" | "createdBy">,
) => {
try { try {
return await prisma.heroBanner.create({ return await prisma.heroBanner.create({
data: { data: {
@ -16,6 +16,9 @@ export const insertHeroBannerRepository = async (
}, },
}); });
} catch (error) { } catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
throw new AppError(400, "A hero banner with the order priority already exists", error);
}
throw new AppError(500, "Failed to insert hero banner", error); throw new AppError(500, "Failed to insert hero banner", error);
} }
}; };

View File

@ -3,45 +3,18 @@ import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const createHeroBannerSchema = { export const createHeroBannerSchema = {
body: t.Object({ body: t.Object({
isClickable: t.Optional( orderPriority: t.Optional(
t.Boolean({ t.Number({ description: "The priority order of the hero banner. Lower numbers indicate higher priority." }),
description: "Indicates whether the hero banner is clickable",
}),
),
title: t.Optional(
t.String({
description: "The title of the hero banner",
}),
),
tags: t.Array(t.String(), {
description: "An array of tags associated with the hero banner",
}),
description: t.Optional(
t.String({
description: "A brief description of the hero banner",
}),
),
buttonContent: t.Optional(
t.String({
description: "The text content of the button on the hero banner",
}),
),
buttonLink: t.Optional(
t.String({
description: "The URL that the button on the hero banner links to",
}),
), ),
mediaId: t.String({ description: "The ID of the media associated with the hero banner" }),
imageUrl: t.Optional( imageUrl: t.Optional(
t.String({ t.String({
description: "The URL of the image used in the hero banner", description:
"The URL of the image used in the hero banner. If not provided, a thumbnail image of the media will be used.",
}), }),
), ),
startDate: t.String({ startDate: t.Date({ description: "The start date for the hero banner in ISO 8601 format" }),
description: "The start date for the hero banner in ISO 8601 format", endDate: t.Date({ description: "The end date for the hero banner in ISO 8601 format" }),
}),
endDate: t.String({
description: "The end date for the hero banner in ISO 8601 format",
}),
}), }),
detail: { detail: {
summary: "Create a new hero banner", summary: "Create a new hero banner",
@ -64,17 +37,16 @@ export const createHeroBannerSchema = {
"The created hero banner object. This field is returned only if the environment is running in development mode.", "The created hero banner object. This field is returned only if the environment is running in development mode.",
properties: { properties: {
id: { type: "string", description: "The ID of the created hero banner" }, id: { type: "string", description: "The ID of the created hero banner" },
isClickable: { type: "boolean", description: "Indicates whether the hero banner is clickable" }, orderPriority: {
title: { type: "string", description: "The title of the hero banner" }, type: "number",
tags: { description: "The priority order of the hero banner. Lower numbers indicate higher priority.",
type: "array", },
items: { type: "string" }, mediaId: { type: "string", description: "The ID of the media associated with the hero banner" },
description: "An array of tags associated with the hero banner", imageUrl: {
type: "string",
description:
"The URL of the image used in the hero banner. If not provided, a thumbnail image of the media will be used.",
}, },
description: { type: "string", description: "A brief description of the hero banner" },
buttonContent: { type: "string", description: "The text content of the button on the hero banner" },
buttonLink: { type: "string", description: "The URL that the button on the hero banner links to" },
imageUrl: { type: "string", description: "The URL of the image used in the hero banner" },
startDate: { startDate: {
type: "string", type: "string",
format: "date-time", format: "date-time",

View File

@ -1,10 +1,9 @@
import { Static } from "elysia";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { CreateHeroBannerRequestBody } from "../../controllers/createHeroBanner.controller";
import { insertHeroBannerRepository } from "../../repositories/insertHeroBanner.repository"; import { insertHeroBannerRepository } from "../../repositories/insertHeroBanner.repository";
import { createHeroBannerSchema } from "../../schemas/createHeroBanner.schema";
export const createHeroBannerService = async ( export const createHeroBannerService = async (payload: Static<typeof createHeroBannerSchema.body>) => {
payload: CreateHeroBannerRequestBody,
) => {
try { try {
return await insertHeroBannerRepository(payload); return await insertHeroBannerRepository(payload);
} catch (error) { } catch (error) {