feat: add org_config loader with caching and defaults
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
import { db } from './db';
|
||||||
|
import { orgConfig } from './schema';
|
||||||
|
|
||||||
|
export interface OrgBranding {
|
||||||
|
primaryColor: string;
|
||||||
|
primaryColorLight: string;
|
||||||
|
primaryColorDark: string;
|
||||||
|
accentColor: string;
|
||||||
|
accentColorLight: string;
|
||||||
|
accentColorDark: string;
|
||||||
|
logoUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgInfo {
|
||||||
|
name: string;
|
||||||
|
tagline: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
facebookUrl: string;
|
||||||
|
instagramUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgLocale {
|
||||||
|
language: string;
|
||||||
|
currency: string;
|
||||||
|
dateFormat: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgApplications {
|
||||||
|
adoptionEnabled: boolean;
|
||||||
|
fosterEnabled: boolean;
|
||||||
|
volunteerEnabled: boolean;
|
||||||
|
surrenderEnabled: boolean;
|
||||||
|
adoptionFee: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgConfig {
|
||||||
|
branding: OrgBranding;
|
||||||
|
orgInfo: OrgInfo;
|
||||||
|
locale: OrgLocale;
|
||||||
|
applications: OrgApplications;
|
||||||
|
setupComplete: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULTS: OrgConfig = {
|
||||||
|
branding: {
|
||||||
|
primaryColor: '#0d9488',
|
||||||
|
primaryColorLight: '#14b8a6',
|
||||||
|
primaryColorDark: '#0f766e',
|
||||||
|
accentColor: '#f59e0b',
|
||||||
|
accentColorLight: '#fbbf24',
|
||||||
|
accentColorDark: '#d97706',
|
||||||
|
logoUrl: ''
|
||||||
|
},
|
||||||
|
orgInfo: {
|
||||||
|
name: 'My Rescue',
|
||||||
|
tagline: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
facebookUrl: '',
|
||||||
|
instagramUrl: ''
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
language: 'en',
|
||||||
|
currency: 'USD',
|
||||||
|
dateFormat: 'MM/DD/YYYY'
|
||||||
|
},
|
||||||
|
applications: {
|
||||||
|
adoptionEnabled: true,
|
||||||
|
fosterEnabled: true,
|
||||||
|
volunteerEnabled: true,
|
||||||
|
surrenderEnabled: true,
|
||||||
|
adoptionFee: 250
|
||||||
|
},
|
||||||
|
setupComplete: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY_MAP: Record<string, keyof OrgConfig> = {
|
||||||
|
branding: 'branding',
|
||||||
|
org_info: 'orgInfo',
|
||||||
|
locale: 'locale',
|
||||||
|
applications: 'applications',
|
||||||
|
setup_complete: 'setupComplete'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseOrgConfig(rows: { key: string; value: unknown }[]): OrgConfig {
|
||||||
|
const config = structuredClone(DEFAULTS);
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const configKey = KEY_MAP[row.key];
|
||||||
|
if (!configKey) continue;
|
||||||
|
|
||||||
|
if (configKey === 'setupComplete') {
|
||||||
|
config.setupComplete = Boolean(row.value);
|
||||||
|
} else {
|
||||||
|
config[configKey] = {
|
||||||
|
...config[configKey],
|
||||||
|
...(row.value as Record<string, unknown>)
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedConfig: OrgConfig | null = null;
|
||||||
|
let cacheTime = 0;
|
||||||
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
export async function getOrgConfig(): Promise<OrgConfig> {
|
||||||
|
if (cachedConfig && Date.now() - cacheTime < CACHE_TTL) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db.select().from(orgConfig);
|
||||||
|
cachedConfig = parseOrgConfig(rows);
|
||||||
|
cacheTime = Date.now();
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidateConfigCache(): void {
|
||||||
|
cachedConfig = null;
|
||||||
|
cacheTime = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parseOrgConfig, type OrgConfig } from '$lib/server/config';
|
||||||
|
|
||||||
|
describe('config', () => {
|
||||||
|
it('parses org config rows into structured object', () => {
|
||||||
|
const rows = [
|
||||||
|
{ key: 'branding', value: { primaryColor: '#0d9488', accentColor: '#f59e0b', logoUrl: '' } },
|
||||||
|
{ key: 'org_info', value: { name: 'Test Rescue', city: 'Sioux Falls', state: 'SD' } },
|
||||||
|
{ key: 'locale', value: { language: 'en', currency: 'USD', dateFormat: 'MM/DD/YYYY' } }
|
||||||
|
];
|
||||||
|
|
||||||
|
const config = parseOrgConfig(rows);
|
||||||
|
expect(config.branding.primaryColor).toBe('#0d9488');
|
||||||
|
expect(config.orgInfo.name).toBe('Test Rescue');
|
||||||
|
expect(config.locale.language).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides defaults for missing keys', () => {
|
||||||
|
const config = parseOrgConfig([]);
|
||||||
|
expect(config.branding.primaryColor).toBe('#0d9488');
|
||||||
|
expect(config.orgInfo.name).toBe('My Rescue');
|
||||||
|
expect(config.locale.language).toBe('en');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user