table-metadata.ts1.9 KBView on GitHub
/**
 * Reading `TableDocumentMetadata` off a document row.
 *
 * `tableStatsHook` writes `{ kind: 'table', schemaVersion, columnCount, rowCount }` into
 * `documents.metadata` on every save, precisely so list views, the file tree and
 * `list-documents` can show "38 rows × 6 columns" WITHOUT loading content
 * (apps/mail/docs/table-documents.md §3.2 step 13). Every client surface types that column
 * as `unknown`, so the narrowing lives here rather than being re-guessed per call site.
 */

import type { TableDocumentMetadata } from '@zero/server/table';

export function readTableMetadata(metadata: unknown): TableDocumentMetadata | null {
  if (typeof metadata !== 'object' || metadata === null) return null;
  const candidate = metadata as { kind?: unknown; rowCount?: unknown; columnCount?: unknown };
  if (candidate.kind !== 'table') return null;
  if (typeof candidate.rowCount !== 'number' || typeof candidate.columnCount !== 'number') {
    return null;
  }
  return metadata as TableDocumentMetadata;
}

/**
 * "38 rows × 6 columns" — the one phrasing for a table's shape.
 *
 * Split out from `formatTableSummary` because the counts also arrive from places that hold no
 * metadata object: `documents.importTable` returns them directly, and a size a user just
 * created must read identically to the same size read back off the row later.
 */
export function formatRowsByColumns(rowCount: number, columnCount: number): string {
  const rows = `${rowCount} ${rowCount === 1 ? 'row' : 'rows'}`;
  const columns = `${columnCount} ${columnCount === 1 ? 'column' : 'columns'}`;
  return `${rows} × ${columns}`;
}

/** The same, off a `documents.metadata`. Null when the row carries no table stats yet. */
export function formatTableSummary(metadata: unknown): string | null {
  const stats = readTableMetadata(metadata);
  if (!stats) return null;
  return formatRowsByColumns(stats.rowCount, stats.columnCount);
}