114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
/**
|
|
* Universal declarative creation enumerator.
|
|
*
|
|
* Rather than write a bespoke enumerator per rApp, each module declares a
|
|
* `CreationSpec` that names its Automerge doc patterns, the collection keys
|
|
* holding records, and a few per-collection accessors. One generic engine
|
|
* walks the docs and emits creations for all modules uniformly.
|
|
*
|
|
* Adding a new module to rPast = ~10 lines in `creation-specs.ts`.
|
|
*/
|
|
|
|
import { loadDoc, listDocIds } from './doc-loader';
|
|
import type { Creation, CreationEnumerator } from './creations';
|
|
|
|
export interface CreationCollection {
|
|
/** Top-level key in the doc (e.g. "events", "tasks", "notes"). */
|
|
path: string;
|
|
/** Short record-type tag, e.g. "event", "task", "note". */
|
|
recordType: string;
|
|
/** Field on the record holding the creation timestamp. Defaults to "createdAt". */
|
|
timestampField?: string;
|
|
/** Pick the display title. Default: record.title || record.name || record.path || id. */
|
|
title?: (record: Record<string, unknown>, id: string) => string;
|
|
/** Build a deep-link URL for a record. */
|
|
href?: (args: { space: string; docId: string; id: string; record: Record<string, unknown> }) => string | undefined;
|
|
/** Emit extra per-record tags. */
|
|
tags?: (record: Record<string, unknown>) => string[] | undefined;
|
|
/** Return false to skip a record (e.g. abstain votes, tombstones). */
|
|
filter?: (record: Record<string, unknown>) => boolean;
|
|
/** Pull a short description body. */
|
|
description?: (record: Record<string, unknown>) => string | undefined;
|
|
}
|
|
|
|
export interface CreationSpec {
|
|
module: string;
|
|
label: string;
|
|
icon: string;
|
|
color?: string;
|
|
/**
|
|
* Doc-ID patterns this module owns.
|
|
* `{space}` is replaced with the space slug.
|
|
* Trailing `*` is a prefix wildcard (expanded via listDocIds).
|
|
* Otherwise the pattern is loaded as a single doc.
|
|
* Examples:
|
|
* `{space}:cal:events` (single doc)
|
|
* `{space}:rnotes:vaults:*` (fan-out over all vaults)
|
|
*/
|
|
docPatterns: string[];
|
|
collections: CreationCollection[];
|
|
}
|
|
|
|
function pickTitle(record: Record<string, unknown>, id: string, col: CreationCollection): string {
|
|
if (col.title) return col.title(record, id);
|
|
const candidates = ['title', 'name', 'path', 'subject', 'label'] as const;
|
|
for (const k of candidates) {
|
|
const v = record[k];
|
|
if (typeof v === 'string' && v.trim()) return v;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
async function expandDocIds(pattern: string, space: string): Promise<string[]> {
|
|
const resolved = pattern.replace(/\{space\}/g, space);
|
|
if (resolved.endsWith('*')) {
|
|
return listDocIds(resolved.slice(0, -1));
|
|
}
|
|
return [resolved];
|
|
}
|
|
|
|
export function createCreationEnumerator(spec: CreationSpec): CreationEnumerator {
|
|
return {
|
|
module: spec.module,
|
|
label: spec.label,
|
|
icon: spec.icon,
|
|
color: spec.color,
|
|
async enumerate({ space, from, to }) {
|
|
const out: Creation[] = [];
|
|
for (const pattern of spec.docPatterns) {
|
|
const docIds = await expandDocIds(pattern, space);
|
|
for (const docId of docIds) {
|
|
const doc = await loadDoc<Record<string, unknown>>(docId);
|
|
if (!doc) continue;
|
|
for (const col of spec.collections) {
|
|
const records = doc[col.path];
|
|
if (!records || typeof records !== 'object') continue;
|
|
const tsField = col.timestampField ?? 'createdAt';
|
|
for (const [id, raw] of Object.entries(records as Record<string, unknown>)) {
|
|
if (!raw || typeof raw !== 'object') continue;
|
|
const rec = raw as Record<string, unknown>;
|
|
if (col.filter && !col.filter(rec)) continue;
|
|
const ts = rec[tsField];
|
|
if (typeof ts !== 'number' || !Number.isFinite(ts)) continue;
|
|
if (from !== undefined && ts < from) continue;
|
|
if (to !== undefined && ts > to) continue;
|
|
const updatedAt = typeof rec.updatedAt === 'number' ? rec.updatedAt as number : undefined;
|
|
out.push({
|
|
createdAt: ts,
|
|
updatedAt,
|
|
title: pickTitle(rec, id, col),
|
|
recordType: col.recordType,
|
|
recordId: id,
|
|
href: col.href?.({ space, docId, id, record: rec }),
|
|
tags: col.tags?.(rec),
|
|
description: col.description?.(rec),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
},
|
|
};
|
|
}
|