alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
137 lines (119 loc) • 3.81 kB
text/typescript
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;
}
}