Compare commits

...

5 Commits

Author SHA1 Message Date
ebc143dc74 Merge pull request 'fix/hero-banner' (#22) from fix/hero-banner into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #22
2026-03-03 21:59:03 +07:00
5a7f9bbebe perf: use Redis for faster system preference check
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 52s
2026-03-03 21:49:32 +07:00
02ad14d382 feat: add helper to detect system preference 2026-03-03 21:47:07 +07:00
a6b2c77bd1 👔 feat: add option to disable banner 2026-03-03 21:25:59 +07:00
d858e54fe8 feat: add tags to banner 2026-03-03 16:56:58 +07:00
11 changed files with 135 additions and 2 deletions

View File

@ -498,6 +498,7 @@ Table hero_banner {
orderPriority Int [unique]
isClickable Boolean [not null, default: false]
title String
tags String[] [not null]
description String
buttonContent String
buttonLink String
@ -512,7 +513,7 @@ Table hero_banner {
Table system_preferences {
id String [pk]
key String [not null]
key String [unique, not null]
value String [not null]
description String [not null]
deletedAt DateTime

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "hero_banner" ADD COLUMN "tags" TEXT[];

View File

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[key]` on the table `system_preferences` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "system_preferences_key_key" ON "system_preferences"("key");

View File

@ -559,6 +559,7 @@ model HeroBanner {
orderPriority Int? @unique
isClickable Boolean @default(false)
title String? @db.VarChar(225)
tags String[]
description String? @db.Text
buttonContent String? @db.VarChar(100)
buttonLink String? @db.Text
@ -574,7 +575,7 @@ model HeroBanner {
model SystemPreference {
id String @id @db.Uuid
key String @db.VarChar(225)
key String @db.VarChar(225) @unique
value String @db.VarChar(225)
description String @db.Text
deletedAt DateTime?

View File

@ -1,4 +1,5 @@
import { prisma } from "../../src/utils/databases/prisma/connection";
import { systemPreferenceSeed } from "./systemPreference.seed";
import { userRoleSeed } from "./userRole.seed";
import { userSystemSeed } from "./userSystem.seed";
@ -8,6 +9,7 @@ async function main() {
const userSystemSeedResult = await userSystemSeed();
await userRoleSeed(userSystemSeedResult.id);
await systemPreferenceSeed();
console.log("🌳 All seeds completed");
}

View File

@ -0,0 +1,35 @@
import { Prisma } from "@prisma/client";
import { generateUUIDv7 } from "../../src/helpers/databases/uuidv7";
import { prisma } from "../../src/utils/databases/prisma/connection";
export const systemPreferenceSeed = async () => {
const preferences: Prisma.SystemPreferenceUpsertArgs["create"][] = [
{
id: generateUUIDv7(),
key: "REGISTRATION_ENABLED",
value: process.env.ENABLE_REGISTRATION === "true" ? "true" : "false",
description: "Enable or disable user registration",
},
{
id: generateUUIDv7(),
key: "HERO_BANNER_ENABLED",
value: process.env.ENABLE_HERO_BANNER === "true" ? "true" : "false",
description: "Enable or disable hero banner feature",
},
];
await prisma.$transaction(async (tx) => {
return await Promise.all(
preferences.map(
async (pref) =>
await tx.systemPreference.upsert({
where: {
key: pref.key,
},
update: pref,
create: pref,
}),
),
);
});
};

View File

@ -1,8 +1,17 @@
import { AppError } from "../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { findSystemPreferenceService } from "../../systemPreference/services/internal/findSystemPreference.service";
import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllActiveHeroBanner.repository";
export const getActiveHeroBannerService = async () => {
try {
const isHeroBannerEnabled = await findSystemPreferenceService(
"HERO_BANNER_ENABLED",
"boolean",
);
if (!isHeroBannerEnabled)
throw new AppError(403, "Hero Banner is disabled");
return await findAllActiveHeroBannerRepository();
} catch (error) {
ErrorForwarder(error);

View File

@ -5,6 +5,7 @@ import { createHeroBannerService } from "../services/http/createHeroBanner.servi
export interface CreateHeroBannerRequestBody {
isClickable?: boolean;
title?: string;
tags: string[];
description?: string;
buttonContent?: string;
buttonLink?: string;

View File

@ -0,0 +1,12 @@
import Elysia, { Context } from "elysia";
import { returnWriteResponse } from "../../helpers/callback/httpResponse";
export const systemPreferenceModule = new Elysia({
prefix: "/system-preference",
}).get("/", (ctx: Context) => {
return returnWriteResponse(
ctx.set,
200,
"System Preference module is up and running",
);
});

View File

@ -0,0 +1,47 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { redis } from "../../../../utils/databases/redis/connection";
import { findSystemPreferenceRepository } from "../repositories/findSystemPreference.repository";
export const findSystemPreferenceService = async (
key: string,
type: "boolean" | "string" | "number" = "string",
) => {
try {
// First, check if the system preference is exists in redis cache
const cachedValue = await redis.get(
`${process.env.APP_NAME}:configs:${key}`,
);
if (!cachedValue) {
// If not exists in cache, fetch from database. If found, return the value and set it to cache, if not found, throw an error
const systemPreference = await findSystemPreferenceRepository(key);
if (!systemPreference)
throw new AppError(404, "System preference not found");
// and set it to cache for future requests
await redis.set(
`${process.env.APP_NAME}:configs:${key}`,
systemPreference.value,
);
// Return the value from database
return parseValue(systemPreference.value, type);
} else {
return parseValue(cachedValue, type);
}
} catch (error) {
ErrorForwarder(error, 500, "Failed to find system preference");
}
};
const parseValue = (value: string, type: "boolean" | "string" | "number") => {
switch (type) {
case "boolean":
return value === "true";
case "number":
return Number(value);
default:
return value;
}
};

View File

@ -0,0 +1,15 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { prisma } from "../../../../utils/databases/prisma/connection";
export const findSystemPreferenceRepository = async (key: string) => {
try {
return await prisma.systemPreference.findUnique({
where: {
key,
deletedAt: null,
},
});
} catch (error) {
throw new AppError(500, "Failed to find system preference", error);
}
};