@krizaka/settings
v0.5.2
Published
A Krizaka product's configuration as data: a typed registry (flags, integers, choices, subsets, bilingual texts), resolved user over workspace over global over default, changed by an administrator with an audit line for every change, the price catalogue (
Maintainers
Readme
@krizaka/settings
A product's configuration as data: a typed registry of settings (flags, integers, choices, subsets, bilingual texts), resolved user → workspace → global → default, changed by an administrator without a redeploy, every change leaving an audit line (who, what, when, before, after). A JSON-safe description of the registry is what an admin screen draws from.
import { emitMigration } from "@krizaka/db";
import { defineSettings, flag, integer, dbSettings, settingsSchema, GLOBAL, WORKSPACE_SCOPES } from "@krizaka/settings";
const settings = defineSettings({ "features.apiKeys": flag("features", true, WORKSPACE_SCOPES), "limits.maxFileMb": integer("limits", 10, { min: 1, max: 10, unit: "MB" }) } as const);
emitMigration(settingsSchema, 1, { tables: {} }); // commit the SQL once, in your migrations (idempotent)
const service = dbSettings(settings, { query, workspaceOf, clock: () => performance.now() });
await service.get("features.apiKeys", { userId }); // typed: boolean
await service.set({ id: adminId, label: email }, GLOBAL, "features.apiKeys", false, now);Schema. The tables are a @krizaka/db module (settings), version 1 byte-identical to the SQL this package shipped before, so a product that has them adopts with no DDL change: adoptMigration(settingsSchema, 1, context) writes the marker only, emitMigration(…) writes the SQL for a database that lacks them. The old migration function stays, @deprecated, for one minor. The price versions are the module settings_prices (pricesSchema).
What stays in the product: its registry (the keys, groups, defaults), the checks it makes at its own boundary (« the server refuses what is off »), who may be an administrator, the HTTP routes. The clock is passed in; nothing here reads it.
Prices — the catalogue (ADR-009)
A product's prices are data too: a typed catalogue of entries (product.plan, unit, amount in cents or « on quote », CAD,
before taxes, availability public · internal · founder-limited · preparing, named limits). The product's former pricing
module becomes the seed; its administrator changes an entry by scheduling a version from a date (now or later, never the
past), and every change writes an audit line in settings_audit (price.<product>.<plan>). A subscription records the version it
was sold at (versionId) and keeps it.
import { AVAILABILITY, definePrices, findTypedPrices, priceCatalogue, pricesMigration, priceStore, publicCatalogue, UNIT } from "@krizaka/settings";
const prices = definePrices([
{ product: "akinaza", plan: "starter", unit: UNIT.month, amountCents: 4500, availability: AVAILABILITY.founderLimited, limits: { founderCap: 20 } },
{ product: "akinaza", plan: "scale", unit: UNIT.month, amountCents: 9900, availability: AVAILABILITY.preparing },
]);
await db.query(pricesMigration()); // once, in your migrations (idempotent; includes the settings' tables)
const catalogue = priceCatalogue(prices, priceStore({ query }));
const shown = publicCatalogue(await catalogue.catalogue(now)); // a public page: never internal, « en préparation » without a figure
await catalogue.schedule({ id: adminId, label: email }, "akinaza.starter", { amountCents: 4900 }, effectiveFrom, now);
const sold = await catalogue.version(subscription.priceVersionId); // the price it was sold at, forever
expect(findTypedPrices(messages)).toEqual([]); // a product's test: no amount typed in a messagedescribePrices(catalogue, now) is the JSON-safe description an admin screen draws (with what is already scheduled);
memoryPriceStore() serves tests. The amount reaches the processor through @krizaka/payments, by the entry's key.
postgresSettingswas the first name ofdbSettings; it stays as a@deprecatedalias for one minor and leaves in the next major.
