Extract reverse-lookup scan to core/relationships (one shared impl)

This commit is contained in:
2026-06-07 23:04:20 +03:00
parent 39555322e4
commit 1cad9ba140
6 changed files with 167 additions and 76 deletions
+3 -15
View File
@@ -16,6 +16,7 @@ import { useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { datasetReference, type DataSource, type Dataset } from '@core/dataset';
import { detectFormat, detectFormatFromUrl, type DataFormat } from '@core/format-detection';
import { datasetUsageCounts, snippetsReferencingDataset } from '@core/relationships';
import { closeModal, openModal, resnapshot } from '../modals/ModalCoordinator';
import { confirm } from '../stores/ConfirmStore';
import { notify } from '../stores/NotificationStore';
@@ -44,18 +45,6 @@ function humanBytes(bytes: number): string {
return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
}
/** Map of dataset name (lower-cased) → how many snippets reference it. */
function usageByName(snippets: ReadonlyArray<{ datasetRefs: string[] }>): Map<string, number> {
const counts = new Map<string, number>();
for (const s of snippets) {
for (const ref of s.datasetRefs) {
const key = ref.toLowerCase();
counts.set(key, (counts.get(key) ?? 0) + 1);
}
}
return counts;
}
const SOURCE_OPTIONS: ReadonlyArray<SegmentedOption<DataSource>> = [
{ value: 'inline', label: 'Inline' },
{ value: 'url', label: 'URL' },
@@ -70,7 +59,7 @@ export function DatasetsModal() {
const select = useDatasetStore((s) => s.select);
const startCreate = useDatasetStore((s) => s.startCreate);
const usage = usageByName(snippets);
const usage = datasetUsageCounts(snippets);
const ordered = [...datasets].sort(byModifiedDesc);
const handleNew = () => {
@@ -165,8 +154,7 @@ function DatasetDetail({
const selectSnippet = useSnippetStore((s) => s.selectSnippet);
const [copied, setCopied] = useState(false);
const lower = dataset.name.toLowerCase();
const linked = snippets.filter((s) => s.datasetRefs.some((r) => r.toLowerCase() === lower));
const linked = snippetsReferencingDataset(snippets, dataset.name);
const handleEdit = () => {
startEdit();
+12 -14
View File
@@ -45,23 +45,21 @@ describe('reverse lookup', () => {
describe('renameDatasetEverywhere', () => {
test('renames the dataset record and every referencing snippet', () => {
useDatasetStore
.getState()
.add(
createDataset({
name: 'Sales',
data: [{ a: 1 }],
format: 'json',
source: 'inline',
now: T,
}),
);
useDatasetStore.getState().add(
createDataset({
name: 'Sales',
data: [{ a: 1 }],
format: 'json',
source: 'inline',
now: T,
}),
);
useSnippetStore.getState().hydrate([seedSnippet('a', 'Sales')], 'a');
useSnippetStore.getState().publish(T);
const updated = renameDatasetEverywhere('Sales', 'Revenue', new Date('2026-07-01T00:00:00Z'));
const result = renameDatasetEverywhere('Sales', 'Revenue', new Date('2026-07-01T00:00:00Z'));
expect(updated).toBe(1);
expect(result).toEqual({ updated: 1 });
expect(useDatasetStore.getState().datasets[0].name).toBe('Revenue');
const s = useSnippetStore.getState().snippets[0];
expect(s.datasetRefs).toEqual(['Revenue']);
@@ -69,6 +67,6 @@ describe('renameDatasetEverywhere', () => {
});
test('is a no-op when old and new names are equal', () => {
expect(renameDatasetEverywhere('Sales', 'Sales')).toBe(0);
expect(renameDatasetEverywhere('Sales', 'Sales')).toEqual({ updated: 0 });
});
});
+19 -20
View File
@@ -13,25 +13,19 @@
*/
import type { Snippet } from '@core/snippet';
import { snippetsReferencingDataset } from '@core/relationships';
import { useDatasetStore } from '../stores/DatasetStore';
import { useSnippetStore } from '../stores/SnippetStore';
// TODO: nothing in production calls this service yet — DatasetsModal derives
// usage/linked-snippets inline (it must read `snippets` reactively, not via the
// getState snapshot these use), and DatasetStore.save propagates rename itself
// (it updates other fields besides the name, so it can't delegate cleanly).
// `renameDatasetEverywhere` also matches by exact name, unlike the rest of the
// case-insensitive naming policy, and arch/07 documents it returning `{ updated }`
// not a bare number. Either reconcile the duplication (extract a reactive-friendly
// pure `snippetsReferencing(snippets, name)` helper both sides share) or fold this
// service in when M4's "Build Chart from dataset" needs a non-reactive caller.
// The reverse-lookup scan itself is the pure `snippetsReferencingDataset`
// (`core/relationships`) — the *one* implementation arch/07 §6 calls for. These
// wrappers just bind it to a store snapshot for non-reactive callers (e.g. a
// programmatic rename); the reactive UI calls the core helper directly with its
// live `snippets` selection, so neither side duplicates the matching logic.
/** Snippets whose `datasetRefs` include `name` (case-insensitive). */
/** Snippets whose `datasetRefs` include `name` (case-insensitive), from the store. */
export function findSnippetsReferencingDataset(name: string): Snippet[] {
const lower = name.toLowerCase();
return useSnippetStore
.getState()
.snippets.filter((s) => s.datasetRefs.some((ref) => ref.toLowerCase() === lower));
return snippetsReferencingDataset(useSnippetStore.getState().snippets, name);
}
/** Count for the dataset usage badge (spec §05 → List item). */
@@ -42,13 +36,18 @@ export function datasetUsageCount(name: string): number {
/**
* Rename a dataset and propagate everywhere (docs/architecture/07 §6): renames
* the dataset record, then rewrites every referencing snippet's spec, draftSpec,
* and datasetRefs. The caller is responsible for collision policy on `newName`
* (the edit form rejects a taken name; programmatic paths pre-resolve a unique
* one). Returns the number of snippets updated.
* and datasetRefs (via `SnippetStore.renameDatasetRefs`, the single rename impl).
* The caller is responsible for collision policy on `newName` (the edit form
* rejects a taken name; programmatic paths pre-resolve a unique one). Returns the
* number of snippets updated, as `{ updated }` (arch/07 §6).
*/
export function renameDatasetEverywhere(oldName: string, newName: string, now?: Date): number {
if (oldName === newName) return 0;
export function renameDatasetEverywhere(
oldName: string,
newName: string,
now?: Date,
): { updated: number } {
if (oldName === newName) return { updated: 0 };
const dataset = useDatasetStore.getState().datasets.find((d) => d.name === oldName);
if (dataset) useDatasetStore.getState().update(dataset.id, { name: newName }, now);
return useSnippetStore.getState().renameDatasetRefs(oldName, newName, now);
return { updated: useSnippetStore.getState().renameDatasetRefs(oldName, newName, now) };
}