From d858e54fe86910887f5d9b1116e4ee5ab1d336d9 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Tue, 3 Mar 2026 16:56:58 +0700 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20tags=20to=20banne?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/dbml/schema.dbml | 1 + .../20260303063458_add_tags_in_hero_banner/migration.sql | 2 ++ prisma/schema.prisma | 1 + src/modules/internal/controllers/createHeroBanner.controller.ts | 1 + 4 files changed, 5 insertions(+) create mode 100644 prisma/migrations/20260303063458_add_tags_in_hero_banner/migration.sql diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 6996dca..c2ea06e 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -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 diff --git a/prisma/migrations/20260303063458_add_tags_in_hero_banner/migration.sql b/prisma/migrations/20260303063458_add_tags_in_hero_banner/migration.sql new file mode 100644 index 0000000..63764e1 --- /dev/null +++ b/prisma/migrations/20260303063458_add_tags_in_hero_banner/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "hero_banner" ADD COLUMN "tags" TEXT[]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4c9b2be..06d52de 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/src/modules/internal/controllers/createHeroBanner.controller.ts b/src/modules/internal/controllers/createHeroBanner.controller.ts index 9e8b18f..0c040bb 100644 --- a/src/modules/internal/controllers/createHeroBanner.controller.ts +++ b/src/modules/internal/controllers/createHeroBanner.controller.ts @@ -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; From a6b2c77bd15717bec18bd3cb98ab11bff2892231 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Tue, 3 Mar 2026 21:25:59 +0700 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=91=94=20feat:=20add=20option=20to=20?= =?UTF-8?q?disable=20banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/dbml/schema.dbml | 2 +- .../migration.sql | 8 +++++ prisma/schema.prisma | 2 +- prisma/seed/index.ts | 2 ++ prisma/seed/systemPreference.seed.ts | 35 +++++++++++++++++++ .../services/getActiveHeroBanner.service.ts | 5 +++ 6 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20260303101939_make_system_preference_id_unique/migration.sql create mode 100644 prisma/seed/systemPreference.seed.ts diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index c2ea06e..05bab02 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -513,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 diff --git a/prisma/migrations/20260303101939_make_system_preference_id_unique/migration.sql b/prisma/migrations/20260303101939_make_system_preference_id_unique/migration.sql new file mode 100644 index 0000000..f9cb54e --- /dev/null +++ b/prisma/migrations/20260303101939_make_system_preference_id_unique/migration.sql @@ -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"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 06d52de..a19bb18 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -575,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? diff --git a/prisma/seed/index.ts b/prisma/seed/index.ts index 7478bfb..cebe591 100644 --- a/prisma/seed/index.ts +++ b/prisma/seed/index.ts @@ -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"); } diff --git a/prisma/seed/systemPreference.seed.ts b/prisma/seed/systemPreference.seed.ts new file mode 100644 index 0000000..b7038bb --- /dev/null +++ b/prisma/seed/systemPreference.seed.ts @@ -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, + }), + ), + ); + }); +}; diff --git a/src/modules/heroBanner/services/getActiveHeroBanner.service.ts b/src/modules/heroBanner/services/getActiveHeroBanner.service.ts index b6093c9..135a7c6 100644 --- a/src/modules/heroBanner/services/getActiveHeroBanner.service.ts +++ b/src/modules/heroBanner/services/getActiveHeroBanner.service.ts @@ -1,8 +1,13 @@ +import { AppError } from "../../../helpers/error/instances/app"; import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllActiveHeroBanner.repository"; export const getActiveHeroBannerService = async () => { try { + const isHeroBannerEnabled = process.env.ENABLE_HERO_BANNER === "true"; + if (!isHeroBannerEnabled) + throw new AppError(403, "Hero Banner is disabled"); + return await findAllActiveHeroBannerRepository(); } catch (error) { ErrorForwarder(error); From 02ad14d38244584641bd04c1a542f17557cccd2c Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Tue, 3 Mar 2026 21:47:07 +0700 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20helper=20to=20det?= =?UTF-8?q?ect=20system=20preference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/systemPreference/index.tsx | 12 +++++ .../internal/findSystemPreference.service.ts | 47 +++++++++++++++++++ .../findSystemPreference.repository.ts | 15 ++++++ 3 files changed, 74 insertions(+) create mode 100644 src/modules/systemPreference/index.tsx create mode 100644 src/modules/systemPreference/services/internal/findSystemPreference.service.ts create mode 100644 src/modules/systemPreference/services/repositories/findSystemPreference.repository.ts diff --git a/src/modules/systemPreference/index.tsx b/src/modules/systemPreference/index.tsx new file mode 100644 index 0000000..b153922 --- /dev/null +++ b/src/modules/systemPreference/index.tsx @@ -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", + ); +}); diff --git a/src/modules/systemPreference/services/internal/findSystemPreference.service.ts b/src/modules/systemPreference/services/internal/findSystemPreference.service.ts new file mode 100644 index 0000000..6ba278e --- /dev/null +++ b/src/modules/systemPreference/services/internal/findSystemPreference.service.ts @@ -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; + } +}; diff --git a/src/modules/systemPreference/services/repositories/findSystemPreference.repository.ts b/src/modules/systemPreference/services/repositories/findSystemPreference.repository.ts new file mode 100644 index 0000000..6023cab --- /dev/null +++ b/src/modules/systemPreference/services/repositories/findSystemPreference.repository.ts @@ -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); + } +}; From 5a7f9bbebe54e52995de5d0119fc4be297818aac Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Tue, 3 Mar 2026 21:49:32 +0700 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9A=A1=20perf:=20use=20Redis=20for=20fas?= =?UTF-8?q?ter=20system=20preference=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../heroBanner/services/getActiveHeroBanner.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/heroBanner/services/getActiveHeroBanner.service.ts b/src/modules/heroBanner/services/getActiveHeroBanner.service.ts index 135a7c6..475c210 100644 --- a/src/modules/heroBanner/services/getActiveHeroBanner.service.ts +++ b/src/modules/heroBanner/services/getActiveHeroBanner.service.ts @@ -1,10 +1,14 @@ 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 = process.env.ENABLE_HERO_BANNER === "true"; + const isHeroBannerEnabled = await findSystemPreferenceService( + "HERO_BANNER_ENABLED", + "boolean", + ); if (!isHeroBannerEnabled) throw new AppError(403, "Hero Banner is disabled");