UNPKG

alepha

Version:

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

1,561 lines (1,258 loc) 78.6 kB
import { Alepha, jsonSchemaToZod, z } from "alepha"; import { DateTimeProvider } from "alepha/datetime"; import { AlephaOrmPostgres } from "alepha/orm/postgres"; import { currentTenantAtom } from "alepha/security"; import { describe, expect, it } from "vitest"; import { $parameter, AlephaApiParameters, ParameterProvider, } from "../index.ts"; const featureSchema = z.object({ enableBeta: z.boolean(), maxUploadSize: z.number(), }); describe("$parameter", () => { it("should initialize with default value", async () => { class AppConfig { features = $parameter({ name: "app.features.flags", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); expect(await config.features.get()).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); }); it("should set and persist parameter", async () => { class AppConfig { features = $parameter({ name: "app.features.flags", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); await config.features.set({ enableBeta: true, maxUploadSize: 20971520, }); expect(await config.features.get()).toEqual({ enableBeta: true, maxUploadSize: 20971520, }); // Verify persisted to database const provider = alepha.inject(ParameterProvider); const history = await provider.getHistory("app.features.flags"); expect(history.length).toBe(1); expect(history[0].content).toEqual({ enableBeta: true, maxUploadSize: 20971520, }); }); it("should load from database on start", async () => { class AppConfig { features = $parameter({ name: "app.features.load", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); // First, seed the database with a value different from default const provider = alepha.inject(ParameterProvider); await provider.save( "app.features.load", { enableBeta: true, maxUploadSize: 5242880 }, "test-hash", ); // Manually reload the parameter primitive const config = alepha.inject(AppConfig); await config.features.reload(); // Should have loaded from database, not default expect(await config.features.get()).toEqual({ enableBeta: true, maxUploadSize: 5242880, }); }); it("should maintain version history", async () => { class AppConfig { features = $parameter({ name: "app.features.history", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); // Make multiple changes await config.features.set({ enableBeta: true, maxUploadSize: 1 }); await config.features.set({ enableBeta: false, maxUploadSize: 2 }); await config.features.set({ enableBeta: true, maxUploadSize: 3 }); const history = await config.features.getHistory(); expect(history.length).toBe(3); expect(history[0].version).toBe(3); expect(history[1].version).toBe(2); expect(history[2].version).toBe(1); }); it("should support rollback to previous version", async () => { class AppConfig { features = $parameter({ name: "app.features.rollback", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); await config.features.set({ enableBeta: true, maxUploadSize: 100 }); await config.features.set({ enableBeta: false, maxUploadSize: 200 }); await config.features.set({ enableBeta: true, maxUploadSize: 300 }); // Rollback to version 1 await config.features.rollback(1); expect(await config.features.get()).toEqual({ enableBeta: true, maxUploadSize: 100, }); // Should have created a new version (4) const history = await config.features.getHistory(); expect(history.length).toBe(4); }); it("should support change description", async () => { class AppConfig { features = $parameter({ name: "app.features.description", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); await config.features.set( { enableBeta: true, maxUploadSize: 20971520 }, { changeDescription: "Enable beta features for testing" }, ); const history = await config.features.getHistory(); expect(history[0].changeDescription).toBe( "Enable beta features for testing", ); }); it("should support user tracking", async () => { class AppConfig { features = $parameter({ name: "app.features.user", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const userId = "550e8400-e29b-41d4-a716-446655440000"; await config.features.set( { enableBeta: true, maxUploadSize: 20971520 }, { user: { id: userId, email: "admin@example.com", name: "Admin User", }, }, ); const history = await config.features.getHistory(); expect(history[0].creatorId).toBe(userId); expect(history[0].creatorName).toBe("Admin User"); }); it("should subscribe and unsubscribe to changes", async () => { class AppConfig { features = $parameter({ name: "app.features.sub", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const received: unknown[] = []; const unsub = config.features.sub((value) => { received.push(value); }); await config.features.set({ enableBeta: true, maxUploadSize: 1 }); expect(received.length).toBe(1); expect(received[0]).toEqual({ enableBeta: true, maxUploadSize: 1 }); // Unsubscribe unsub(); await config.features.set({ enableBeta: false, maxUploadSize: 2 }); // Should not receive after unsubscribe expect(received.length).toBe(1); }); it("should promote cached next to current when time passes", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); class AppConfig { features = $parameter({ name: "app.features.timetravel", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const dateTime = alepha.inject(DateTimeProvider); // Set current value await config.features.set({ enableBeta: false, maxUploadSize: 100 }); // Schedule a future value +1h from now const futureDate = new Date( dateTime.now().toDate().getTime() + 60 * 60 * 1000, ); await config.features.set( { enableBeta: true, maxUploadSize: 999 }, { activationDate: futureDate }, ); // Reload to pick up the next version await config.features.reload(); // Currently should still be the old value expect(await config.features.get()).toEqual({ enableBeta: false, maxUploadSize: 100, }); // Travel past the activation date dateTime.travel(2, "hours"); // Now get() should promote the next to current expect(await config.features.get()).toEqual({ enableBeta: true, maxUploadSize: 999, }); }); }); describe("ParameterProvider", () => { it("should build parameter tree from names", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("app.features.flags", { enabled: true }, "hash1"); await provider.save("app.features.limits", { max: 100 }, "hash2"); await provider.save("app.pricing.tiers", { basic: 10 }, "hash3"); await provider.save("system.logging", { level: "info" }, "hash4"); const tree = await provider.getParameterTree(); expect(tree.length).toBe(2); // app, system expect(tree[0].name).toBe("app"); expect(tree[0].isLeaf).toBe(false); expect(tree[0].children.length).toBe(2); // features, pricing const features = tree[0].children.find((c) => c.name === "features"); expect(features?.children.length).toBe(2); // flags, limits }); it("should calculate statuses correctly", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); // Create first version (current — immediate activation) const v1 = await provider.save("test.status.param", { value: 1 }, "hash"); expect(v1.status).toBe("current"); // Create second version (becomes current, first becomes expired) const v2 = await provider.save("test.status.param", { value: 2 }, "hash"); expect(v2.status).toBe("current"); // Verify calculated statuses const history = await provider.getHistory("test.status.param"); const withStatuses = provider.calculateStatuses(history); const v1Status = withStatuses.find((v) => v.version === 1); const v2Status = withStatuses.find((v) => v.version === 2); expect(v1Status?.status).toBe("expired"); expect(v2Status?.status).toBe("current"); }); it("should calculate future and next statuses", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); const dateTime = alepha.inject(DateTimeProvider); // Create current version await provider.save("test.future.param", { value: 1 }, "hash"); // Create future versions const futureDate1 = new Date( dateTime.now().toDate().getTime() + 60 * 60 * 1000, ); // +1h const futureDate2 = new Date( dateTime.now().toDate().getTime() + 2 * 60 * 60 * 1000, ); // +2h await provider.save("test.future.param", { value: 2 }, "hash", { activationDate: futureDate1, }); await provider.save("test.future.param", { value: 3 }, "hash", { activationDate: futureDate2, }); const history = await provider.getHistory("test.future.param"); const withStatuses = provider.calculateStatuses(history); const v1 = withStatuses.find((v) => v.version === 1); const v2 = withStatuses.find((v) => v.version === 2); const v3 = withStatuses.find((v) => v.version === 3); expect(v1?.status).toBe("current"); expect(v2?.status).toBe("next"); expect(v3?.status).toBe("future"); }); it("should detect schema migration", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("migration.param", { v: 1 }, "hash-v1"); const v2 = await provider.save( "migration.param", { v: 2, extra: true }, "hash-v2", ); expect(v2.migrationLog).toContain("Schema changed"); expect(v2.migrationLog).toContain("hash-v1"); expect(v2.migrationLog).toContain("hash-v2"); }); it("should store previous content for rollback", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("previous.param", { old: true }, "hash"); const v2 = await provider.save("previous.param", { new: true }, "hash"); expect(v2.previousContent).toEqual({ old: true }); }); it("should get all parameter names", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("names.alpha", { a: 1 }, "h1"); await provider.save("names.beta", { b: 1 }, "h2"); await provider.save("names.alpha", { a: 2 }, "h1"); // Second version const names = await provider.getParameterNames(); expect(names).toContain("names.alpha"); expect(names).toContain("names.beta"); }); it("should load current and next from database", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); const dateTime = alepha.inject(DateTimeProvider); // Create current and future versions await provider.save("test.loadcn.param", { value: "current" }, "hash"); const futureDate = new Date( dateTime.now().toDate().getTime() + 60 * 60 * 1000, ); await provider.save("test.loadcn.param", { value: "next" }, "hash", { activationDate: futureDate, }); const { current, next } = await provider.loadCurrentAndNext("test.loadcn.param"); expect(current).not.toBeNull(); expect(current?.content).toEqual({ value: "current" }); expect(next).not.toBeNull(); expect(next?.content).toEqual({ value: "next" }); }); }); describe("Cross-instance sync", () => { it("should pick up changes via load() when database is updated externally", async () => { class AppConfig { features = $parameter({ name: "app.features.sync", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Should start with default expect(await config.features.get()).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); // Simulate another instance writing to DB (bypass primitive, use provider directly) await provider.save( "app.features.sync", { enableBeta: true, maxUploadSize: 20971520 }, "external-hash", ); // Primitive still has cached default (hasn't reloaded) expect(config.features.cachedCurrentContent).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); // Call load() to pick up the database change await config.features.load(); // Should now have the updated value expect(await config.features.get()).toEqual({ enableBeta: true, maxUploadSize: 20971520, }); }); it("should ignore sync messages from self via instanceId", async () => { class AppConfig { features = $parameter({ name: "app.features.selfignore", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const provider = alepha.inject(ParameterProvider); // Get the provider's instanceId (protected but accessible for testing) const providerAny = provider as any; const selfInstanceId = providerAny.instanceId; // Simulate receiving a change notification from self (should be ignored) const selfPayload = { name: "app.features.selfignore", instanceId: selfInstanceId, }; // Call the handler directly await providerAny.handleChangeNotification(selfPayload); // Value should NOT have changed (self-messages are ignored) const config = alepha.inject(AppConfig); expect(await config.features.get()).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); // Simulate receiving a change notification from another instance // First, seed a value in the database so load() finds something await provider.save( "app.features.selfignore", { enableBeta: true, maxUploadSize: 999 }, "hash", ); const otherPayload = { name: "app.features.selfignore", instanceId: "other-instance-id", }; await providerAny.handleChangeNotification(otherPayload); // Value should now be updated (loaded from DB) expect(await config.features.get()).toEqual({ enableBeta: true, maxUploadSize: 999, }); }); }); describe("Schema migration", () => { it("should keep valid value when schema is compatible", async () => { const schema = z.object({ name: z.text(), age: z.number().optional(), }); class AppConfig { param = $parameter({ name: "migrate.compatible", schema, default: { name: "default" }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with a value that has a different schema hash await provider.save("migrate.compatible", { name: "jack" }, "old-hash"); // Reload — the value is still valid against the new schema await config.param.reload(); expect(await config.param.get()).toEqual({ name: "jack" }); // No new version should be created (still just 1) const history = await provider.getHistory("migrate.compatible"); expect(history.length).toBe(1); }); it("should merge with defaults when adding a required field", async () => { const schema = z.object({ name: z.text(), age: z.number(), }); class AppConfig { param = $parameter({ name: "migrate.add.field", schema, default: { name: "default", age: 25 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with a value missing the "age" field, different schema hash await provider.save("migrate.add.field", { name: "jack" }, "old-hash"); // Reload — value is invalid (missing required "age"), should merge with defaults await config.param.reload(); expect(await config.param.get()).toEqual({ name: "jack", age: 25 }); // A new version should be auto-created const history = await provider.getHistory("migrate.add.field"); expect(history.length).toBe(2); expect(history[0].changeDescription).toContain("Auto-migrated"); expect(history[0].changeDescription).toContain("merged with defaults"); }); it("should reset to defaults when merge still invalid", async () => { const schema = z.object({ items: z.array( z.object({ x: z.number(), y: z.number(), }), ), }); class AppConfig { param = $parameter({ name: "migrate.reset.defaults", schema, default: { items: [{ x: 0, y: 0 }] }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with invalid nested value, different schema hash await provider.save( "migrate.reset.defaults", { items: [{ x: 1 }] }, "old-hash", ); // Reload — merge still invalid (nested object missing "y"), should reset to defaults await config.param.reload(); expect(await config.param.get()).toEqual({ items: [{ x: 0, y: 0 }] }); // A new version should be auto-created const history = await provider.getHistory("migrate.reset.defaults"); expect(history.length).toBe(2); expect(history[0].changeDescription).toContain("reset to defaults"); }); it("should use migrate function when provided", async () => { const schema = z.object({ name: z.text(), age: z.number(), }); class AppConfig { param = $parameter({ name: "migrate.function", schema, default: { name: "default", age: 0 }, migrate: (old: unknown) => { const prev = old as Record<string, unknown>; return { name: String(prev.firstName ?? "unknown"), age: 30, }; }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with old-format value, different schema hash await provider.save("migrate.function", { firstName: "jack" }, "old-hash"); // Reload — migrate function should transform the value await config.param.reload(); expect(await config.param.get()).toEqual({ name: "jack", age: 30 }); // A new version should be auto-created const history = await provider.getHistory("migrate.function"); expect(history.length).toBe(2); }); it("should fall through to merge when migrate returns invalid value", async () => { const schema = z.object({ name: z.text(), age: z.number(), }); class AppConfig { param = $parameter({ name: "migrate.fallthrough.merge", schema, default: { name: "default", age: 42 }, migrate: (_old: unknown) => { // Returns invalid value (missing required "age") return { name: "migrated" } as any; }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with value missing "age", different schema hash await provider.save( "migrate.fallthrough.merge", { name: "jack" }, "old-hash", ); // Reload — migrate returns invalid, falls through to merge with defaults await config.param.reload(); expect(await config.param.get()).toEqual({ name: "jack", age: 42 }); }); it("should not migrate when schema hash matches", async () => { const schema = z.object({ name: z.text(), age: z.number(), }); class AppConfig { param = $parameter({ name: "migrate.hash.match", schema, default: { name: "default", age: 0 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Get the actual schema hash the provider calculated const providerAny = provider as any; const schemaHash = providerAny.schemaHashes.get("migrate.hash.match"); // Seed database with the SAME schema hash — no migration needed await provider.save( "migrate.hash.match", { name: "jack", age: 99 }, schemaHash, ); // Reload — hash matches, no migration await config.param.reload(); expect(await config.param.get()).toEqual({ name: "jack", age: 99 }); // No new version created const history = await provider.getHistory("migrate.hash.match"); expect(history.length).toBe(1); }); it("should handle type change by resetting to defaults", async () => { const schema = z.object({ name: z.number(), }); class AppConfig { param = $parameter({ name: "migrate.type.change", schema, default: { name: 42 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with wrong type (string instead of number), different hash await provider.save("migrate.type.change", { name: "jack" }, "old-hash"); // Reload — merge produces { name: "jack" } which is still invalid (wrong type) // Falls to default await config.param.reload(); expect(await config.param.get()).toEqual({ name: 42 }); const history = await provider.getHistory("migrate.type.change"); expect(history.length).toBe(2); expect(history[0].changeDescription).toContain("reset to defaults"); }); it("should handle removed fields gracefully", async () => { const schema = z.object({ name: z.text(), }); class AppConfig { param = $parameter({ name: "migrate.removed.fields", schema, default: { name: "default" }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with extra fields that no longer exist in schema await provider.save( "migrate.removed.fields", { name: "jack", age: 30 }, "old-hash", ); // Reload — value should still work, "name" is present and valid await config.param.reload(); const value = await config.param.get(); expect((value as any).name).toBe("jack"); }); it("should not migrate when there is no DB value", async () => { const schema = z.object({ name: z.text(), age: z.number(), }); class AppConfig { param = $parameter({ name: "migrate.no.db.value", schema, default: { name: "default", age: 0 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); // No seeding — no DB value exists await config.param.reload(); // Should use default expect(await config.param.get()).toEqual({ name: "default", age: 0 }); }); it("should handle migrate function that throws", async () => { const schema = z.object({ name: z.text(), age: z.number(), }); class AppConfig { param = $parameter({ name: "migrate.throws", schema, default: { name: "default", age: 0 }, migrate: (_old: unknown) => { throw new Error("migration failed"); }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); // Seed database with value missing "age", different schema hash await provider.save("migrate.throws", { name: "jack" }, "old-hash"); // Reload — migrate throws, falls through to merge with defaults await config.param.reload(); expect(await config.param.get()).toEqual({ name: "jack", age: 0 }); }); it("should notify subscribers after auto-migration", async () => { const schema = z.object({ name: z.text(), age: z.number(), }); class Config { param = $parameter({ name: "migrate.notify.sub", schema, default: { name: "default", age: 0 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(Config); await alepha.start(); const provider = alepha.inject(ParameterProvider); const config = alepha.inject(Config); // First, set a valid value so cachedCurrent has a previous value await config.param.set({ name: "jack", age: 30 }); // Subscribe to changes const received: unknown[] = []; config.param.sub((v) => received.push(v)); // Now save a value with old schema hash to trigger migration // Reset migrationChecked to allow re-migration. The value caches are now // keyed `${org}:${name}` — no org atom in this test → the `~global` org. (provider as any).migrationChecked.delete("~global:migrate.notify.sub"); await provider.save( "migrate.notify.sub", { name: "alice" }, "old-hash-notify", ); // Reload triggers migration (merge adds default age) await config.param.reload(); // Subscriber should have been notified with the migrated value expect(received.length).toBeGreaterThanOrEqual(1); const lastReceived = received[received.length - 1] as any; expect(lastReceived.name).toBe("alice"); expect(lastReceived.age).toBe(0); }); it("should handle completely new shape by resetting to defaults", async () => { const schema = z.object({ width: z.number(), height: z.number(), }); class Config { param = $parameter({ name: "migrate.new.shape", schema, default: { width: 100, height: 200 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(Config); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save( "migrate.new.shape", { color: "red", opacity: 0.5 }, "old-hash", ); const config = alepha.inject(Config); await config.param.reload(); // Old shape has no overlapping fields — merge produces { width: 100, height: 200, color: "red", opacity: 0.5 } // After pickSchemaKeys: { width: 100, height: 200 } — which is valid! This is the merge path. // OR: merge invalid -> falls to defaults. Either way, result should have width/height. const value = await config.param.get(); expect(value).toEqual({ width: 100, height: 200 }); }); it("should only migrate once per lifecycle in Node.js", async () => { const schema = z.object({ name: z.text(), count: z.number(), }); class Config { param = $parameter({ name: "migrate.once", schema, default: { name: "default", count: 0 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(Config); await alepha.start(); const provider = alepha.inject(ParameterProvider); const config = alepha.inject(Config); // Save with old hash to trigger migration await provider.save("migrate.once", { name: "v1" }, "old-hash-once"); // First reload triggers migration await config.param.reload(); expect(await config.param.get()).toEqual({ name: "v1", count: 0 }); const historyAfterFirst = await provider.getHistory("migrate.once"); const versionCountAfterFirst = historyAfterFirst.length; // Save another value with old hash await provider.save("migrate.once", { name: "v2" }, "old-hash-once-2"); // Second reload should NOT trigger migration (migrationChecked = true) await config.param.reload(); // Value should be the raw DB value (v2 without count), since migration is skipped // The loaded value from DB is { name: "v2" } which becomes cachedCurrent const historyAfterSecond = await provider.getHistory("migrate.once"); // No new migration version should have been created expect(historyAfterSecond.length).toBe(versionCountAfterFirst + 1); // only the provider.save we just did }); }); describe("getParameterNames (distinct query)", () => { it("should return unique names even with multiple versions", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); // Create multiple versions of the same parameter await provider.save("distinct.alpha", { a: 1 }, "h1"); await provider.save("distinct.alpha", { a: 2 }, "h1"); await provider.save("distinct.alpha", { a: 3 }, "h1"); await provider.save("distinct.beta", { b: 1 }, "h2"); const names = await provider.getParameterNames(); // Should deduplicate — "distinct.alpha" appears once despite 3 versions const alphaCount = names.filter((n) => n === "distinct.alpha").length; expect(alphaCount).toBe(1); expect(names).toContain("distinct.alpha"); expect(names).toContain("distinct.beta"); }); it("should return names in sorted order", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("sorted.charlie", { c: 1 }, "h"); await provider.save("sorted.alpha", { a: 1 }, "h"); await provider.save("sorted.bravo", { b: 1 }, "h"); const names = await provider.getParameterNames(); const relevant = names.filter((n) => n.startsWith("sorted.")); expect(relevant).toEqual([ "sorted.alpha", "sorted.bravo", "sorted.charlie", ]); }); }); describe("getVersion (direct lookup)", () => { it("should return a specific version without loading all history", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("version.lookup", { v: 1 }, "h"); await provider.save("version.lookup", { v: 2 }, "h"); await provider.save("version.lookup", { v: 3 }, "h"); const v2 = await provider.getVersion("version.lookup", 2); expect(v2).not.toBeNull(); expect(v2!.version).toBe(2); expect(v2!.content).toEqual({ v: 2 }); }); it("should return null for non-existent version", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("version.missing", { v: 1 }, "h"); const missing = await provider.getVersion("version.missing", 99); expect(missing).toBeNull(); }); it("should allow calculating status for a single version", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("version.status", { v: 1 }, "h"); await provider.save("version.status", { v: 2 }, "h"); const v1 = await provider.getVersion("version.status", 1); const [withStatus] = provider.calculateStatuses([v1!]); // A single past version in isolation is both the latest activated and only one expect(withStatus.status).toBe("current"); expect(withStatus.version).toBe(1); }); }); describe("save (status calculation without re-fetch)", () => { it("should return correct status for immediate save", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); const v1 = await provider.save("save.status.imm", { v: 1 }, "h"); expect(v1.status).toBe("current"); const v2 = await provider.save("save.status.imm", { v: 2 }, "h"); expect(v2.status).toBe("current"); }); it("should return correct status for scheduled save", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); const dateTime = alepha.inject(DateTimeProvider); // Create current version const v1 = await provider.save("save.status.sched", { v: 1 }, "h"); expect(v1.status).toBe("current"); // Create future version const futureDate = new Date( dateTime.now().toDate().getTime() + 60 * 60 * 1000, ); const v2 = await provider.save("save.status.sched", { v: 2 }, "h", { activationDate: futureDate, }); expect(v2.status).toBe("next"); }); it("should mark older versions as expired after new immediate save", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("save.status.expired", { v: 1 }, "h"); await provider.save("save.status.expired", { v: 2 }, "h"); const v3 = await provider.save("save.status.expired", { v: 3 }, "h"); // v3 should be current expect(v3.status).toBe("current"); // Verify via full history const history = await provider.getHistory("save.status.expired"); const withStatuses = provider.calculateStatuses(history); expect(withStatuses.find((v) => v.version === 1)?.status).toBe("expired"); expect(withStatuses.find((v) => v.version === 2)?.status).toBe("expired"); expect(withStatuses.find((v) => v.version === 3)?.status).toBe("current"); }); it("should resolve empty schema hash from registered primitive", async () => { class AppConfig { features = $parameter({ name: "save.hash.resolve", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const provider = alepha.inject(ParameterProvider); // Save with empty schema hash (simulates admin UI) const result = await provider.save( "save.hash.resolve", { enableBeta: true, maxUploadSize: 100 }, "", ); // Should have resolved the hash from the registered primitive expect(result.schemaHash).not.toBe(""); // Save another version — should NOT log a schema migration since hash matches const v2 = await provider.save( "save.hash.resolve", { enableBeta: false, maxUploadSize: 200 }, "", ); expect(v2.migrationLog).toBeUndefined(); }); }); // --------------------------------------------------------------------------- // 1. rollback() error path // --------------------------------------------------------------------------- describe("rollback error path", () => { it("should throw AlephaError when rolling back to non-existent version", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("rollback.err", { v: 1 }, "h"); await expect(provider.rollback("rollback.err", 99)).rejects.toThrowError( /Parameter version not found: rollback\.err@99/, ); }); it("should throw AlephaError when rolling back a parameter that does not exist", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await expect( provider.rollback("nonexistent.param", 1), ).rejects.toThrowError(/Parameter version not found/); }); }); // --------------------------------------------------------------------------- // 2. getCurrentWithDefault() // --------------------------------------------------------------------------- describe("getCurrentWithDefault", () => { it("should return nulls for unregistered parameter", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); const result = await provider.getCurrentWithDefault("unregistered.param"); expect(result.current).toBeNull(); expect(result.next).toBeNull(); expect(result.defaultValue).toBeNull(); expect(result.currentValue).toBeNull(); expect(result.schema).toBeNull(); }); it("should lazy-seed v1 from defaults when primitive registered but no DB value", async () => { class AppConfig { features = $parameter({ name: "gwd.default.only", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const provider = alepha.inject(ParameterProvider); const result = await provider.getCurrentWithDefault("gwd.default.only"); // Auto-seed: getCurrentWithDefault materializes v1 from the compiled // defaults when nothing is in the DB yet, so the admin UI always has a // concrete current row to edit / roll back / compare against. expect(result.current).not.toBeNull(); expect(result.current!.version).toBe(1); expect(result.current!.status).toBe("current"); expect(result.current!.content).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); expect(result.current!.changeDescription).toBe( "Auto-seeded from compiled defaults", ); expect(result.next).toBeNull(); expect(result.defaultValue).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); expect(result.currentValue).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); expect(result.schema).not.toBeNull(); // Idempotent: a second call returns the same row without re-creating. const second = await provider.getCurrentWithDefault("gwd.default.only"); expect(second.current!.id).toBe(result.current!.id); expect(second.current!.version).toBe(1); }); it("should return current, next, and defaults when all exist", async () => { class AppConfig { features = $parameter({ name: "gwd.full", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); const dateTime = alepha.inject(DateTimeProvider); // Set current value await config.features.set({ enableBeta: true, maxUploadSize: 100 }); // Schedule future value const futureDate = new Date( dateTime.now().toDate().getTime() + 60 * 60 * 1000, ); await config.features.set( { enableBeta: false, maxUploadSize: 999 }, { activationDate: futureDate }, ); // Reload to pick up the next version in cache await config.features.reload(); const result = await provider.getCurrentWithDefault("gwd.full"); expect(result.current).not.toBeNull(); expect(result.current!.status).toBe("current"); expect(result.current!.content).toEqual({ enableBeta: true, maxUploadSize: 100, }); expect(result.next).not.toBeNull(); expect(result.next!.status).toBe("next"); expect(result.next!.content).toEqual({ enableBeta: false, maxUploadSize: 999, }); expect(result.defaultValue).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); expect(result.schema).not.toBeNull(); }); it("should return the schema as JSON Schema, not a raw ZodObject", async () => { // Regression: the parameter's `schema` is a live `ZodObject`. It must be // serialized to JSON Schema before transport — otherwise the action's // `schema: z.json()` response validation rejects it ("expected record, // received ZodObject at /schema"), 500-ing the admin Parameters UI when an // item is opened. The UI rebuilds its form by round-tripping the value back // through `jsonSchemaToZod` (the inverse of `z.toJSONSchema`). class AppConfig { features = $parameter({ name: "gwd.schema.json", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const provider = alepha.inject(ParameterProvider); const result = await provider.getCurrentWithDefault("gwd.schema.json"); // A plain JSON-Schema record describing the parameter shape — never the // Zod instance (which would have `_zod`/`def`, not `type`/`properties`). expect(z.schema.isObject(result.schema as never)).toBe(false); expect((result.schema as { type?: string }).type).toBe("object"); expect( Object.keys((result.schema as { properties?: object }).properties ?? {}), ).toEqual(expect.arrayContaining(["enableBeta", "maxUploadSize"])); // Round-trips back to a working Zod schema for the admin form. const rebuilt = jsonSchemaToZod(result.schema); expect( rebuilt.safeParse({ enableBeta: true, maxUploadSize: 42 }).success, ).toBe(true); }); }); // --------------------------------------------------------------------------- // 3. getCurrentValue() // --------------------------------------------------------------------------- describe("getCurrentValue", () => { it("should return null for unregistered parameter", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); expect(provider.getCurrentValue("unregistered.param")).toBeNull(); }); it("should return default value with isDefault=true when no DB value", async () => { class AppConfig { features = $parameter({ name: "gcv.default", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const provider = alepha.inject(ParameterProvider); const result = provider.getCurrentValue("gcv.default"); expect(result).not.toBeNull(); expect(result!.isDefault).toBe(true); expect(result!.content).toEqual({ enableBeta: false, maxUploadSize: 10485760, }); }); it("should return cached value with isDefault=false after set", async () => { class AppConfig { features = $parameter({ name: "gcv.cached", schema: featureSchema, default: { enableBeta: false, maxUploadSize: 10485760 }, }); } const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); alepha.with(AppConfig); await alepha.start(); const config = alepha.inject(AppConfig); const provider = alepha.inject(ParameterProvider); await config.features.set({ enableBeta: true, maxUploadSize: 42 }); const result = provider.getCurrentValue("gcv.cached"); expect(result).not.toBeNull(); expect(result!.isDefault).toBe(false); expect(result!.content).toEqual({ enableBeta: true, maxUploadSize: 42 }); }); }); // --------------------------------------------------------------------------- // 4. activateNow controller logic (tested via provider since controller // needs HTTP layer — we test the equivalent logic directly) // --------------------------------------------------------------------------- describe("activateNow logic", () => { it("should return current version as-is when already current", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provider = alepha.inject(ParameterProvider); await provider.save("activate.current", { v: 1 }, "h"); await provider.save("activate.current", { v: 2 }, "h"); const history = await provider.getHistory("activate.current"); const withStatuses = provider.calculateStatuses(history); const current = withStatuses.find((v) => v.status === "current")!; // Already current — no new version needed expect(current.version).toBe(2); expect(current.status).toBe("current"); }); it("should reject activating an expired version", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); alepha.with(AlephaApiParameters); await alepha.start(); const provid