feat(settings): add cached SettingsService (setting() equivalent)

This commit is contained in:
Simo committed 2026-06-27 14:31:35 +02:00
1 parent edbd5c89ba
commit c2366fe6c9
3 files changed
+83

No files matched your search

+3
View File
@@ -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";
@@ -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);
});
});
+41
View File
@@ -0,0 +1,41 @@
export interface SettingRow {
key: string;
value: string;
}
export type SettingsFetcher = () => Promise<SettingRow[]>;
/**
* 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<string, string> | null = null;
constructor(private readonly fetcher: SettingsFetcher) {}
private async load(): Promise<Map<string, string>> {
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<string | null> {
const map = await this.load();
return map.has(key) ? (map.get(key) as string) : def;
}
async getBool(key: string, def = false): Promise<boolean> {
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;
}
}