Spreadsheet-UX op een echte relationele engine

main

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

3.3 kB Ruw bestand
import { assertWorkspaceSlug, type Locale, type Role } from '@wellformed/shared';
import { conflict } from '../errors.js';
import type { DemoChoice } from '../db/demo.js';
import { seed } from '../db/seed.js';
import { transactControl, type ControlDb } from '../control/db.js';
import {
  createWorkspace,
  deleteWorkspace,
  getWorkspaceBySlug,
  setMembership,
  type WorkspaceRecord,
} from '../control/workspaces.js';
import type { TenantRegistry } from './registry.js';

export interface ProvisionInput {
  slug: string;
  name: string;
  /** The account that gets the `admin` membership. */
  ownerId: string;
  /**
   * What to build inside it: nothing, the demo's tables and views, or the
   * demo with its data too. Omitted means empty — starting from your own
   * spreadsheet is the expected road in.
   */
  demo?: DemoChoice;
  /**
   * The language the workspace opens in — usually the browser language of
   * whoever is creating it. Changeable afterwards in workspace settings.
   * The demo, when asked for, is built in this language.
   */
  locale?: Locale;
}

/**
 * Create a workspace: a control record, an admin membership, a database file
 * and, optionally, something to look at inside it.
 *
 * The control record and the file are created in that order but rolled back
 * together. A control row pointing at a database that failed to build would
 * hand its members a workspace that 500s on every request, which is worse than
 * the signup simply failing.
 */
export function provisionWorkspace(
  control: ControlDb,
  registry: TenantRegistry,
  input: ProvisionInput,
): WorkspaceRecord {
  assertWorkspaceSlug(input.slug);

  if (getWorkspaceBySlug(control, input.slug)) {
    throw conflict(`The workspace address "${input.slug}" is already taken.`);
  }

  const workspace = transactControl(control, () => {
    const created = createWorkspace(control, {
      slug: input.slug,
      name: input.name,
      createdBy: input.ownerId,
      locale: input.locale,
    });
    setMembership(control, created.id, input.ownerId, 'admin' satisfies Role);
    return created;
  });

  try {
    const db = registry.acquire(workspace);
    try {
      const demo = input.demo ?? 'none';
      if (demo !== 'none') {
        seed(db, { quiet: true, locale: workspace.locale, mode: demo });
      }
    } finally {
      registry.release(workspace.id);
    }
  } catch (error) {
    // Undo both halves, in the order that leaves nothing orphaned.
    try {
      registry.destroy(workspace);
    } finally {
      deleteWorkspace(control, workspace.id);
    }
    throw error;
  }

  return workspace;
}

/**
 * Suggest a free slug near `base`, so signup does not bounce someone over a
 * name collision they cannot see.
 */
export function availableSlug(control: ControlDb, base: string): string {
  if (!getWorkspaceBySlug(control, base)) {
    try {
      assertWorkspaceSlug(base);
      return base;
    } catch {
      // Fall through to the suffixed attempts below.
    }
  }
  for (let n = 2; n < 500; n++) {
    const candidate = `${base.slice(0, 36)}-${n}`;
    try {
      assertWorkspaceSlug(candidate);
    } catch {
      continue;
    }
    if (!getWorkspaceBySlug(control, candidate)) return candidate;
  }
  throw conflict('Could not find a free workspace address. Pick one yourself.');
}