diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 6996dca..05bab02 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 @@ -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 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/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 4c9b2be..a19bb18 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 @@ -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? 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..475c210 100644 --- a/src/modules/heroBanner/services/getActiveHeroBanner.service.ts +++ b/src/modules/heroBanner/services/getActiveHeroBanner.service.ts @@ -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); 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; 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); + } +};