Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { slugifyWorkspaceName } from '@wellformed/shared';
import { config } from '../config.js';
import { adoptLegacyDatabase, type AdoptionResult } from '../db/adopt.js';
import {
closeControlDatabase,
openControlDatabase,
transactControl,
type ControlDb,
} from '../control/db.js';
import { createAccount, getAccountByEmail } from '../control/accounts.js';
import { listAllWorkspaces, type WorkspaceRecord } from '../control/workspaces.js';
import { hashPassword } from '../auth/password.js';
import type { DemoChoice } from '../db/demo.js';
import { availableSlug, provisionWorkspace } from './provision.js';
import { TenantRegistry } from './registry.js';
/**
* One installation: the control database, the pool of workspace databases, and
* whatever had to happen before either could be used.
*
* Both the server and the CLI open an installation, so the adoption of a
* pre-tenancy database happens exactly once no matter which runs first.
*/
export interface Installation {
control: ControlDb;
registry: TenantRegistry;
adoption: AdoptionResult;
close(): void;
}
export function openInstallation(
overrides: { controlDbPath?: string; tenantsDir?: string; adopt?: boolean } = {},
): Installation {
const control = openControlDatabase(overrides.controlDbPath ?? config.controlDbPath);
const registry = new TenantRegistry({
directory: overrides.tenantsDir ?? config.tenantsDir,
maxOpen: config.maxOpenWorkspaces,
idleMs: config.workspaceIdleMs,
});
const adoption =
overrides.adopt === false
? { adopted: false, accountsMoved: 0 }
: adoptLegacyDatabase(control, {
file: config.legacyDbPath,
slug: config.legacySlug,
name: config.legacyName,
});
return {
control,
registry,
adoption,
close: () => {
registry.closeAll();
closeControlDatabase(control);
},
};
}
export interface OwnerInput {
email: string;
password: string;
name?: string;
workspaceName?: string;
slug?: string;
/** What to build inside the workspace. Omitted means the full demo. */
demo?: DemoChoice;
}
/**
* Create an account and its first workspace without going through HTTP.
*
* The scripted equivalent of signing up: what a deploy script or a self-hoster
* with signups closed uses to get the first person in.
*/
export async function provisionOwner(
control: ControlDb,
registry: TenantRegistry,
input: OwnerInput,
): Promise<{ workspace: WorkspaceRecord; created: boolean }> {
const email = input.email.trim().toLowerCase();
const workspaceName = input.workspaceName ?? 'Workspace';
const existing = getAccountByEmail(control, email);
if (existing) {
const owned = listAllWorkspaces(control);
const already = owned.find((w) => w.createdBy === existing.id);
if (already) return { workspace: already, created: false };
}
const hash = await hashPassword(input.password);
const account =
existing ??
transactControl(control, () =>
createAccount(control, { email, name: input.name ?? 'Admin', passwordHash: hash }),
);
const slug = input.slug ?? availableSlug(control, slugifyWorkspaceName(workspaceName));
const workspace = provisionWorkspace(control, registry, {
slug,
name: workspaceName,
ownerId: account.id,
// The scripted road defaults to the full demo: an operator provisioning a
// first workspace wants something to point at, and can say --empty.
demo: input.demo ?? 'full',
});
return { workspace, created: true };
}