UNPKG

alepha

Version:

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

137 lines (119 loc) 3.81 kB
import { AlephaError } from "alepha"; /** * Multi-tenancy mode for an app's deployments. * * - `none` (default): single-instance app. Passing `--tenant` is an * error — guards a non-tenanted app (e.g. the SaaS console) from * accidentally getting a tenant prefix. * - `required`: every deploy targets a tenant. Omitting `--tenant` is an * error — kills the "forgot the flag → deployed to the apex" footgun. * - `optional`: both work — a base instance (no `--tenant`) and per-tenant * instances coexist (distinct names + hosts). */ export type Tenancy = "none" | "optional" | "required"; /** * Validate a `--tenant` value against the app's declared `tenancy` and * return the effective tenant (or `undefined` for a base/non-tenanted * deploy). Throws on any matrix violation so a misconfigured deploy fails * fast instead of landing on the wrong resource names / host. */ export function resolveTenant( tenancy: Tenancy | undefined, tenant: string | undefined, ): string | undefined { const mode = tenancy ?? "none"; if (tenant !== undefined && !/^[a-z0-9][a-z0-9-]*$/.test(tenant)) { throw new AlephaError( `Invalid --tenant "${tenant}": must be a slug (lowercase alphanumeric + dashes, e.g. "b14").`, ); } if (mode === "none") { if (tenant !== undefined) { throw new AlephaError( `This app is not tenanted (tenancy: "none") but --tenant "${tenant}" was given. ` + `Set tenancy: "optional" | "required" in platform() to deploy per-tenant.`, ); } return undefined; } if (mode === "required" && tenant === undefined) { throw new AlephaError( `This app requires a tenant (tenancy: "required"). Pass --tenant <slug>.`, ); } // required+value or optional(+/- value) — value may be undefined for the // optional base instance. return tenant; } /** * Resolve the host a deploy is served on. * * - `override` (V2 custom domains, e.g. `reservation.club-b14.fr`) wins * outright when supplied — not wired to a flag today, but the single * seam a future `--domain` / Rocket `config.hostname` plugs into. * - else a tenant becomes the leftmost DNS label: `b14` + `alepha.club` * → `b14.alepha.club`. * - else the base domain is used as-is. */ export function tenantDomain( base: string | undefined, tenant: string | undefined, override?: string, ): string | undefined { if (override) return override; if (!base) return base; return tenant ? `${tenant}.${base}` : base; } /** * Generates deterministic resource names for cloud deployments. * * Pattern: `<tenant>-<project>-<env>` (tenant segment omitted when the * deploy isn't tenanted). * * All segments are slugified (lowercase, alphanumeric + dashes, max 63 * chars). One app per workspace — see `alepha platform`. */ export class NamingService { public forContext( project: string, env: string, tenant?: string, ): NamingContext { const prefix = [tenant, project, env] .filter((segment): segment is string => !!segment) .map((segment) => this.slugify(segment)) .join("-"); return new NamingContext(prefix); } public slugify(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 63); } } export class NamingContext { protected readonly prefix: string; constructor(prefix: string) { this.prefix = prefix; } public worker(): string { return this.prefix; } public d1(): string { return this.prefix; } public hyperdrive(): string { return this.prefix; } public r2(): string { return this.prefix; } public kv(): string { return this.prefix; } public queue(): string { return this.prefix; } }