UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

240 lines (213 loc) 6.15 kB
import { $inject, createPrimitive, KIND, Primitive, type Static, type TObject, } from "alepha"; import type { UserAccount } from "alepha/security"; import { ParameterProvider } from "../services/ParameterProvider.ts"; /** * Creates a versioned parameter primitive for managing application settings. * * Provides type-safe, versioned configuration with: * - Schema validation with auto-migration detection * - Default values for initial state * - Status derived from activationDate (no stored status) * - Database persistence with full version history * - Cross-instance notification via topic * - Tree view support via dot-notation naming (e.g., "app.features.flags") * - Async `.get()` with lazy loading (works in Node and Cloudflare Workers) * * @example * ```ts * class AppConfig { * features = $parameter({ * name: "app.features.flags", * schema: z.object({ * enableBeta: z.boolean(), * maxUploadSize: z.number() * }), * default: { enableBeta: false, maxUploadSize: 10485760 } * }); * * async checkBeta() { * const config = await this.features.get(); * return config.enableBeta; * } * * async enableBeta() { * await this.features.set({ enableBeta: true, maxUploadSize: 20971520 }); * } * } * ``` */ export interface ParameterPrimitiveOptions<T extends TObject> { /** * Parameter name using dot notation for tree hierarchy. * Examples: "app.features", "app.pricing.tiers", "system.limits" */ name?: string; /** * Human-readable description of the parameter. */ description?: string; /** * TypeBox schema defining the parameter structure. */ schema: T; /** * Default value used when no parameter exists in database. */ default: Static<T>; /** * Optional migration function for schema changes. * Receives the raw DB value and returns a transformed value matching the new schema. * Runs before validation — if the result is valid, it's used directly. * If not provided or returns an invalid value, falls through to merge/default cascade. */ migrate?: (old: unknown) => Static<T>; } export class ParameterPrimitive<T extends TObject> extends Primitive< ParameterPrimitiveOptions<T> > { protected readonly provider = $inject(ParameterProvider); /** * Parameter name (uses property key if not specified). */ public get name(): string { return ( this.options.name || `${this.config.service.name}.${this.config.propertyKey}` ); } /** * The TypeBox schema for this parameter. */ public get schema(): T { return this.options.schema; } /** * Get the cached current content, falling back to default. * Synchronous access for admin API. */ public get cachedCurrentContent(): Static<T> { return this.provider.getCachedCurrentContent(this.name) as Static<T>; } /** * Whether the parameter is using its default value (no DB value loaded). */ public get isUsingDefault(): boolean { return this.provider.isUsingDefault(this.name); } /** * Get the current parameter value asynchronously. * Lazy-loads from database on first call. * Checks if a cached next version has become current. */ public get(): Promise<Static<T>> { return this.provider.get(this.name) as Promise<Static<T>>; } /** * Load current and next values from database. */ public async load(): Promise<void> { await this.provider.load(this.name); } /** * Set a new parameter value. * * @param value - The new parameter value * @param options - Optional settings (activation date, creator info, etc.) */ public async set( value: Static<T>, options: SetParameterOptions = {}, ): Promise<void> { await this.provider.set(this.name, value, { activationDate: options.activationDate, changeDescription: options.changeDescription, tags: options.tags, creatorId: options.user?.id, creatorName: options.user?.name ?? options.user?.email, }); } /** * Subscribe to parameter changes. * Returns an unsubscribe function. */ public sub(fn: (curr: Static<T>) => void): () => void { return this.provider.sub(this.name, fn as (v: unknown) => void); } /** * Reload parameter from database. * Called when sync notification received or for manual refresh. */ public async reload(): Promise<void> { await this.provider.load(this.name); } /** * Get version history for this parameter. */ public async getHistory(options?: { limit?: number; offset?: number }) { return this.provider.getHistory(this.name, options); } /** * Get a specific version of this parameter. */ public async getVersion(version: number) { return this.provider.getVersion(this.name, version); } /** * Delete all versions of this parameter. */ public async delete(): Promise<void> { await this.provider.delete(this.name); } /** * Rollback to a specific version. */ public async rollback( version: number, options?: SetParameterOptions, ): Promise<void> { await this.provider.rollback(this.name, version, { changeDescription: options?.changeDescription, creatorId: options?.user?.id, creatorName: options?.user?.name ?? options?.user?.email, }); await this.provider.load(this.name); } /** * Called after primitive creation to register with provider. */ protected onInit(): void { this.provider.register(this); } } export const $parameter = <T extends TObject>( options: ParameterPrimitiveOptions<T>, ) => { return createPrimitive(ParameterPrimitive<T>, options); }; $parameter[KIND] = ParameterPrimitive; export interface SetParameterOptions { /** * User making the change (for audit trail). */ user?: Pick<UserAccount, "id" | "email" | "name">; /** * When this parameter should become active. * Default is immediate (now). */ activationDate?: Date; /** * Description of the change. */ changeDescription?: string; /** * Tags for filtering/categorization. */ tags?: string[]; }