From c2366fe6c9e06005df94b37e3308acb86cd8db87 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 27 Jun 2026 14:31:35 +0200 Subject: [PATCH] feat(settings): add cached SettingsService (setting() equivalent) --- packages/settings/src/index.ts | 3 ++ .../settings/src/settings-service.test.ts | 39 ++++++++++++++++++ packages/settings/src/settings-service.ts | 41 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 packages/settings/src/index.ts create mode 100644 packages/settings/src/settings-service.test.ts create mode 100644 packages/settings/src/settings-service.ts diff --git a/packages/settings/src/index.ts b/packages/settings/src/index.ts new file mode 100644 index 00000000..fa8cec89 --- /dev/null +++ b/packages/settings/src/index.ts @@ -0,0 +1,3 @@ +export { CurrencyType, currencyLabel } from "./currency-type.js"; +export { SettingsService } from "./settings-service.js"; +export type { SettingRow, SettingsFetcher } from "./settings-service.js"; diff --git a/packages/settings/src/settings-service.test.ts b/packages/settings/src/settings-service.test.ts new file mode 100644 index 00000000..bb35078c --- /dev/null +++ b/packages/settings/src/settings-service.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, vi } from "vitest"; +import { SettingsService } from "./settings-service.js"; + +const rows = [ + { key: "hotel_name", value: "AtomHotel" }, + { key: "maintenance_enabled", value: "1" }, + { key: "radio_enabled", value: "0" }, +]; + +describe("SettingsService", () => { + it("returns a value by key", async () => { + const svc = new SettingsService(async () => rows); + expect(await svc.get("hotel_name")).toBe("AtomHotel"); + }); + + it("returns the default when key is missing", async () => { + const svc = new SettingsService(async () => rows); + expect(await svc.get("missing", "fallback")).toBe("fallback"); + expect(await svc.get("missing")).toBeNull(); + }); + + it("coerces '1'/'0' string booleans", async () => { + const svc = new SettingsService(async () => rows); + expect(await svc.getBool("maintenance_enabled")).toBe(true); + expect(await svc.getBool("radio_enabled")).toBe(false); + expect(await svc.getBool("missing", true)).toBe(true); + }); + + it("fetches only once and caches until cleared", async () => { + const fetcher = vi.fn(async () => rows); + const svc = new SettingsService(fetcher); + await svc.get("hotel_name"); + await svc.get("radio_enabled"); + expect(fetcher).toHaveBeenCalledTimes(1); + svc.clear(); + await svc.get("hotel_name"); + expect(fetcher).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/settings/src/settings-service.ts b/packages/settings/src/settings-service.ts new file mode 100644 index 00000000..64b360e7 --- /dev/null +++ b/packages/settings/src/settings-service.ts @@ -0,0 +1,41 @@ +export interface SettingRow { + key: string; + value: string; +} + +export type SettingsFetcher = () => Promise; + +/** + * Mirrors Laravel's App\Services\SettingsService: loads the whole + * website_settings table into a key->value map, caches it in-process, + * and reads with a default fallback. Booleans are stored as '1'/'0'. + */ +export class SettingsService { + private cache: Map | null = null; + + constructor(private readonly fetcher: SettingsFetcher) {} + + private async load(): Promise> { + if (this.cache === null) { + const rows = await this.fetcher(); + this.cache = new Map(rows.map((r) => [r.key, r.value])); + } + return this.cache; + } + + async get(key: string, def: string | null = null): Promise { + const map = await this.load(); + return map.has(key) ? (map.get(key) as string) : def; + } + + async getBool(key: string, def = false): Promise { + const map = await this.load(); + if (!map.has(key)) return def; + const v = (map.get(key) as string).toLowerCase(); + return v === "1" || v === "true"; + } + + clear(): void { + this.cache = null; + } +}