alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,561 lines (1,258 loc) • 78.6 kB
text/typescript
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