Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

3.5 kB Ruw bestand
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 };
}