@rokkit/graph 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/dist/Graph.svelte +369 -0
  4. package/dist/Graph.svelte.d.ts +4 -0
  5. package/dist/GraphState.svelte.d.ts +171 -0
  6. package/dist/GraphState.svelte.js +398 -0
  7. package/dist/icons.d.ts +13 -0
  8. package/dist/icons.js +38 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.js +8 -0
  11. package/dist/layout/cards.d.ts +19 -0
  12. package/dist/layout/cards.js +31 -0
  13. package/dist/layout/cluster.d.ts +11 -0
  14. package/dist/layout/cluster.js +39 -0
  15. package/dist/layout/clusters.d.ts +39 -0
  16. package/dist/layout/clusters.js +217 -0
  17. package/dist/layout/constants.d.ts +12 -0
  18. package/dist/layout/constants.js +15 -0
  19. package/dist/layout/edges.d.ts +5 -0
  20. package/dist/layout/edges.js +110 -0
  21. package/dist/layout/index.d.ts +9 -0
  22. package/dist/layout/index.js +7 -0
  23. package/dist/layout/neighborhood.d.ts +9 -0
  24. package/dist/layout/neighborhood.js +123 -0
  25. package/dist/layout/nested.d.ts +13 -0
  26. package/dist/layout/nested.js +156 -0
  27. package/dist/layout/points.d.ts +6 -0
  28. package/dist/layout/points.js +204 -0
  29. package/dist/layout/types.d.ts +115 -0
  30. package/dist/layout/types.js +1 -0
  31. package/dist/model/normalize.d.ts +7 -0
  32. package/dist/model/normalize.js +215 -0
  33. package/dist/model/path.d.ts +13 -0
  34. package/dist/model/path.js +23 -0
  35. package/dist/preset.d.ts +47 -0
  36. package/dist/preset.js +61 -0
  37. package/dist/schema/EntitiesView.svelte +83 -0
  38. package/dist/schema/EntitiesView.svelte.d.ts +13 -0
  39. package/dist/schema/EntityView.svelte +116 -0
  40. package/dist/schema/EntityView.svelte.d.ts +13 -0
  41. package/dist/schema/NoteBlocks.svelte +31 -0
  42. package/dist/schema/NoteBlocks.svelte.d.ts +6 -0
  43. package/dist/schema/column-type.d.ts +4 -0
  44. package/dist/schema/column-type.js +19 -0
  45. package/dist/schema/fromSchemaModel.d.ts +66 -0
  46. package/dist/schema/fromSchemaModel.js +92 -0
  47. package/dist/schema/index.d.ts +8 -0
  48. package/dist/schema/index.js +7 -0
  49. package/dist/schema/notes.d.ts +12 -0
  50. package/dist/schema/notes.js +65 -0
  51. package/dist/types.d.ts +160 -0
  52. package/dist/types.js +1 -0
  53. package/package.json +62 -0
package/dist/preset.js ADDED
@@ -0,0 +1,61 @@
1
+ import { categoricalPalette } from '@rokkit/core';
2
+ /**
3
+ * Mirrors `createChartPreset`'s shape on purpose — a consumer who has met one
4
+ * has met both. Shade picks differ from chart's because a cluster is a large
5
+ * background area rather than a small mark.
6
+ *
7
+ * The `groups` ramp and the `patterns` list are chart's own leading entries, in
8
+ * chart's order, so a graph and a chart on one page differentiate alike.
9
+ */
10
+ export const defaultGraphPreset = {
11
+ groups: ['blue', 'emerald', 'rose', 'amber', 'violet', 'sky', 'pink', 'teal'],
12
+ // The label shade is measured against the FILL shade, not against paper: the cluster label
13
+ // sits on the cluster. 700-on-100 came out at 3.63:1 for green — under the 4.5 text bar —
14
+ // so the label sits two more steps away from its own background.
15
+ shades: {
16
+ light: { fill: '100', stroke: '400', label: '900' },
17
+ dark: { fill: '900', stroke: '600', label: '100' }
18
+ },
19
+ patterns: ['diagonal', 'dots', 'triangles', 'hatch', 'lattice', 'swell', 'checkerboard', 'waves'],
20
+ using: 'color'
21
+ };
22
+ export function createGraphPreset(overrides = {}) {
23
+ return {
24
+ ...defaultGraphPreset,
25
+ ...overrides,
26
+ shades: {
27
+ light: { ...defaultGraphPreset.shades.light, ...overrides.shades?.light },
28
+ dark: { ...defaultGraphPreset.shades.dark, ...overrides.shades?.dark }
29
+ }
30
+ };
31
+ }
32
+ /**
33
+ * Resolves group names to CSS custom properties.
34
+ *
35
+ * Returns custom properties rather than concrete fills so a theme or an app can
36
+ * still override with one attribute rule — the JS decides the DEFAULT, never the
37
+ * final paint.
38
+ *
39
+ * Assignment is by SORTED group name so a group keeps its colour when `arrange`
40
+ * reorders the layout. Callers pass one name per NODE, so names are de-duplicated
41
+ * first; otherwise the ramp would advance per node and two nodes in the same group
42
+ * would disagree.
43
+ */
44
+ export function resolveGroupStyles(groups, mode, preset = defaultGraphPreset) {
45
+ const styles = new Map();
46
+ const ordered = [...new Set(groups)].sort();
47
+ ordered.forEach((group, index) => {
48
+ if (preset.using === 'pattern') {
49
+ styles.set(group, { '--group-pattern': preset.patterns[index % preset.patterns.length] });
50
+ return;
51
+ }
52
+ const family = categoricalPalette[preset.groups[index % preset.groups.length]];
53
+ const shades = preset.shades[mode];
54
+ styles.set(group, {
55
+ '--group-fill': family[shades.fill],
56
+ '--group-stroke': family[shades.stroke],
57
+ '--group-label': family[shades.label]
58
+ });
59
+ });
60
+ return styles;
61
+ }
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import { getContext, untrack } from 'svelte'
3
+ import { Table } from '@rokkit/ui'
4
+ import type { TableColumn } from '@rokkit/ui'
5
+ import { GraphState } from '../GraphState.svelte.js'
6
+ import type { EntityRow, GraphStateConfig } from '../GraphState.svelte.js'
7
+ import NoteBlocks from './NoteBlocks.svelte'
8
+ import type { GraphFields } from '../types.js'
9
+
10
+ let {
11
+ state: provided,
12
+ nodes = [],
13
+ edges = [],
14
+ fields = {},
15
+ caption = undefined,
16
+ class: className = ''
17
+ }: {
18
+ state?: GraphState
19
+ nodes?: unknown[]
20
+ edges?: unknown[]
21
+ fields?: GraphFields
22
+ caption?: string
23
+ class?: string
24
+ } = $props()
25
+
26
+ const config = (): GraphStateConfig => ({ nodes, edges, fields })
27
+
28
+ // untrack: the constructor's initial read must not become a dependency of the creating
29
+ // scope. Only built when this view is standalone — see the resolution order below.
30
+ const own = untrack(() => new GraphState(config()))
31
+ const fromContext = getContext<GraphState | undefined>('graph-state')
32
+
33
+ // Resolved ONCE, in the same three-way order Graph.svelte uses, so a view works inside a
34
+ // <Graph>, beside one, or alone: the prop wins, then context, then the state we build.
35
+ // svelte-ignore state_referenced_locally
36
+ const graph = provided ?? fromContext ?? own
37
+
38
+ // Only sync the state WE own. A prop- or context-supplied state is its owner's to drive.
39
+ $effect(() => {
40
+ if (graph === own) own.update(config())
41
+ })
42
+
43
+ const columns: TableColumn[] = [
44
+ { name: 'label', label: 'Entity', snippet: 'entity' },
45
+ { name: 'rowCount', label: 'Rows', align: 'right', snippet: 'rows' },
46
+ { name: 'refCount', label: 'Refs', align: 'right', snippet: 'refs' },
47
+ { name: 'note', label: 'Comment', sortable: false, snippet: 'comment' }
48
+ ]
49
+ </script>
50
+
51
+ {#snippet entity(_value: unknown, _column: TableColumn, row: Record<string, unknown>)}
52
+ {@const item = row as unknown as EntityRow}
53
+ <span data-graph-entity data-graph-entity-kind={item.kind}>
54
+ {#if item.group}<span data-graph-entity-group>{item.group}.</span>{/if}<span
55
+ data-graph-entity-label>{item.label}</span
56
+ >
57
+ </span>
58
+ {/snippet}
59
+
60
+ {#snippet rows(_value: unknown, _column: TableColumn, row: Record<string, unknown>)}
61
+ <span data-graph-entity-rows>{(row as unknown as EntityRow).rowCount}</span>
62
+ {/snippet}
63
+
64
+ {#snippet refs(_value: unknown, _column: TableColumn, row: Record<string, unknown>)}
65
+ {@const count = (row as unknown as EntityRow).refCount}
66
+ <!-- The em dash is a PRESENTATION choice and belongs here: state reports the number 0,
67
+ and a column of zeros reads as noise next to the counts that matter. -->
68
+ <span data-graph-entity-refs>{count > 0 ? count : '—'}</span>
69
+ {/snippet}
70
+
71
+ {#snippet comment(_value: unknown, _column: TableColumn, row: Record<string, unknown>)}
72
+ <NoteBlocks note={(row as unknown as EntityRow).note} />
73
+ {/snippet}
74
+
75
+ <Table
76
+ data={graph.entities}
77
+ {columns}
78
+ {caption}
79
+ class={className}
80
+ selectable="single"
81
+ onselect={(_value, row) => graph.select((row as unknown as EntityRow).id)}
82
+ snippets={{ entity, rows, refs, comment }}
83
+ />
@@ -0,0 +1,13 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { GraphFields } from '../types.js';
3
+ type $$ComponentProps = {
4
+ state?: GraphState;
5
+ nodes?: unknown[];
6
+ edges?: unknown[];
7
+ fields?: GraphFields;
8
+ caption?: string;
9
+ class?: string;
10
+ };
11
+ declare const EntitiesView: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type EntitiesView = ReturnType<typeof EntitiesView>;
13
+ export default EntitiesView;
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ import { getContext, untrack } from 'svelte'
3
+ import { GraphState } from '../GraphState.svelte.js'
4
+ import type { GraphStateConfig } from '../GraphState.svelte.js'
5
+ import NoteBlocks from './NoteBlocks.svelte'
6
+ import { baseType, typeSize } from './column-type.js'
7
+ import type { GraphFields } from '../types.js'
8
+
9
+ let {
10
+ state: provided,
11
+ nodes = [],
12
+ edges = [],
13
+ fields = {},
14
+ value = undefined,
15
+ class: className = ''
16
+ }: {
17
+ state?: GraphState
18
+ nodes?: unknown[]
19
+ edges?: unknown[]
20
+ fields?: GraphFields
21
+ value?: string | null
22
+ class?: string
23
+ } = $props()
24
+
25
+ const config = (): GraphStateConfig => ({ nodes, edges, fields, value })
26
+
27
+ const own = untrack(() => new GraphState(config()))
28
+ const fromContext = getContext<GraphState | undefined>('graph-state')
29
+
30
+ // Same three-way resolution as Graph and EntitiesView: prop, then context, then the state
31
+ // we build. Resolved once — context captures a value, not a getter.
32
+ // svelte-ignore state_referenced_locally
33
+ const graph = provided ?? fromContext ?? own
34
+
35
+ $effect(() => {
36
+ if (graph === own) own.update(config())
37
+ })
38
+
39
+ // A read, not a derivation: GraphState owns the in/out split and the meta passthrough.
40
+ const entity = $derived(graph.entity)
41
+ const indexes = $derived(
42
+ (entity?.meta.indexes ?? []) as { def: string; unique?: boolean; name?: string }[]
43
+ )
44
+ </script>
45
+
46
+ {#if entity}
47
+ <article data-graph-entity data-node-kind={entity.kind} class={className}>
48
+ <header data-graph-entity-head>
49
+ {#if entity.group}<span data-graph-entity-group>{entity.group}.</span>{/if}
50
+ <h2 data-graph-entity-title>{entity.label}</h2>
51
+ <span data-graph-entity-count>{entity.rows.length} columns</span>
52
+ </header>
53
+
54
+ {#if entity.note}
55
+ <section data-graph-section="note">
56
+ <h3 data-graph-section-title>Comment</h3>
57
+ <NoteBlocks note={entity.note} />
58
+ </section>
59
+ {/if}
60
+
61
+ <section data-graph-section="columns">
62
+ <h3 data-graph-section-title>Columns</h3>
63
+ {#each entity.rows as row (row.name)}
64
+ <div data-graph-column>
65
+ <span data-graph-column-name>{row.name}</span>
66
+ {#if row.note}<span data-graph-column-note>{row.note}</span>{/if}
67
+ <span data-graph-column-badges>
68
+ {#each row.badges as badge (badge)}
69
+ <span data-row-badge={badge}>{badge}</span>
70
+ {/each}
71
+ </span>
72
+ <span data-graph-column-type>{baseType(row.type ?? '')}</span>
73
+ <span data-graph-column-size>{typeSize(row.type ?? '')}</span>
74
+ </div>
75
+ {/each}
76
+ </section>
77
+
78
+ {#if indexes.length > 0}
79
+ <section data-graph-section="indexes">
80
+ <h3 data-graph-section-title>Indexes</h3>
81
+ {#each indexes as index, i (i)}
82
+ <div data-graph-index>
83
+ <span data-graph-index-def>{index.def}</span>
84
+ {#if index.unique}<span data-graph-index-unique>unique</span>{/if}
85
+ {#if index.name}<span data-graph-index-name>{index.name}</span>{/if}
86
+ </div>
87
+ {/each}
88
+ </section>
89
+ {/if}
90
+
91
+ <section data-graph-section="relationships">
92
+ <h3 data-graph-section-title>Relationships</h3>
93
+ {#if graph.relationships.length === 0}
94
+ <p data-graph-relationships-empty>No relationships reference this entity.</p>
95
+ {:else}
96
+ {#each graph.relationships as rel, i (rel.edge.id + i)}
97
+ <button
98
+ type="button"
99
+ data-graph-relationship={rel.direction}
100
+ onclick={() => graph.select(rel.id)}
101
+ >
102
+ {#if rel.group}<span data-graph-relationship-group>{rel.group}.</span>{/if}
103
+ <span data-graph-relationship-label>{rel.label}</span>
104
+ {#if rel.edge.action}
105
+ <span data-graph-relationship-action>{rel.edge.action}</span>
106
+ {/if}
107
+ </button>
108
+ {/each}
109
+ {/if}
110
+ </section>
111
+ </article>
112
+ {:else}
113
+ <article data-graph-entity data-graph-entity-empty class={className}>
114
+ <p>No entity selected.</p>
115
+ </article>
116
+ {/if}
@@ -0,0 +1,13 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { GraphFields } from '../types.js';
3
+ type $$ComponentProps = {
4
+ state?: GraphState;
5
+ nodes?: unknown[];
6
+ edges?: unknown[];
7
+ fields?: GraphFields;
8
+ value?: string | null;
9
+ class?: string;
10
+ };
11
+ declare const EntityView: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type EntityView = ReturnType<typeof EntityView>;
13
+ export default EntityView;
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { noteBlocks } from './notes.js'
3
+
4
+ let { note }: { note?: string } = $props()
5
+
6
+ const blocks = $derived(noteBlocks(note))
7
+ </script>
8
+
9
+ {#snippet segs(parts: ReturnType<typeof noteBlocks>[number]['lines'][number])}
10
+ <!-- Keyed by INDEX, not by text. A segment has no identity of its own, and its text is
11
+ not unique — "either `a` or `a`" yields two code segments reading `a`, which throws
12
+ each_key_duplicate and renders nothing. Position is the only stable key here, and
13
+ the whole list is re-derived whenever the note changes. -->
14
+ {#each parts as part, i (i)}
15
+ {#if part.code}<code data-graph-note-code>{part.text}</code>{:else}{part.text}{/if}
16
+ {/each}
17
+ {/snippet}
18
+
19
+ <div data-graph-note>
20
+ {#each blocks as block (block)}
21
+ {#if block.type === 'ul'}
22
+ <ul data-graph-note-list>
23
+ {#each block.lines as line (line)}
24
+ <li>{@render segs(line)}</li>
25
+ {/each}
26
+ </ul>
27
+ {:else}
28
+ <p>{@render segs(block.lines[0])}</p>
29
+ {/if}
30
+ {/each}
31
+ </div>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ note?: string;
3
+ };
4
+ declare const NoteBlocks: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type NoteBlocks = ReturnType<typeof NoteBlocks>;
6
+ export default NoteBlocks;
@@ -0,0 +1,4 @@
1
+ /** `varchar(255)` -> `varchar`, `text[]` -> `text`. */
2
+ export declare function baseType(type: string): string;
3
+ /** `varchar(255)` -> `255`, `text[]` -> `[]`, `uuid` -> an em dash. */
4
+ export declare function typeSize(type: string): string;
@@ -0,0 +1,19 @@
1
+ /* Splitting a SQL type into its base and its size, for the two-column display in the
2
+ entity detail panel. Ported from dbd's EntityView module block.
3
+
4
+ Parsing, not formatting choice, so it lives here with its own tests rather than inline
5
+ in the template — the component stays free of computation. */
6
+ const ARG = /\(([^)]*)\)/;
7
+ /** `varchar(255)` -> `varchar`, `text[]` -> `text`. */
8
+ export function baseType(type) {
9
+ return type.replace(ARG, '').replace(/\[\]$/, '');
10
+ }
11
+ /** `varchar(255)` -> `255`, `text[]` -> `[]`, `uuid` -> an em dash. */
12
+ export function typeSize(type) {
13
+ const matched = type.match(ARG);
14
+ if (matched)
15
+ return matched[1];
16
+ if (type.endsWith('[]'))
17
+ return '[]';
18
+ return '—';
19
+ }
@@ -0,0 +1,66 @@
1
+ import type { GraphFields, GraphModel } from '../types.js';
2
+ /**
3
+ * Field map from a dbd `SchemaModel`'s ER half to the canonical model.
4
+ *
5
+ * Exported so a consumer can hand it straight to `<Graph fields={SCHEMA_FIELDS}>`
6
+ * and skip the transform entirely.
7
+ *
8
+ * Note what is NOT here: dbd's `SchemaModel` type. The package never imports it,
9
+ * so the hand-written TS mirror of `schema_model.rs` stays dbd's concern and this
10
+ * package is not a third definition to keep in step.
11
+ *
12
+ * One canonical field stays deliberately unmapped, because dbd's shape does not carry it and
13
+ * a path that cannot resolve looks correct while silently never firing:
14
+ *
15
+ * - `cardinality` — dbd's `Ref` is `{ from, to, action? }`.
16
+ */
17
+ export declare const SCHEMA_FIELDS: GraphFields;
18
+ /**
19
+ * Field map for a dbd v2 `DepEdge` — `{ from: { s, n }, to: { s, n }, kind, unresolved }`.
20
+ *
21
+ * `n` not `t`, and no `c`: a dependency links whole objects, not columns. `kind` is the VERB
22
+ * (`reads | writes | calls | member`), which is why it maps to `relation` and the edge kind
23
+ * comes from `defaultEdgeKind` instead — every entry in `deps` is a dependency by definition.
24
+ */
25
+ export declare const DEPS_FIELDS: GraphFields;
26
+ /**
27
+ * Which of a `SchemaModel`'s two graphs to build.
28
+ *
29
+ * dbd v2 splits the database in two and says so in its own doc comments: `tables` is "Tables
30
+ * only", `refs` is "Foreign keys only — the dependency graph is `deps`; an ER renderer wants
31
+ * these and a call-graph renderer wants those".
32
+ *
33
+ * That split is a modelling fact, not a rendering preference. An ER diagram is entities and
34
+ * their relationships; a view is a derived projection and a routine is behaviour, so neither
35
+ * is an entity and putting them on an ER canvas is a category error — they have no keys, no
36
+ * relationships, and (in v2) no columns, so they render as orphan cards with nothing in them.
37
+ *
38
+ * - `er` — `tables` + `refs`. The default, so a v1 caller reading a v2 model is unaffected.
39
+ * - `dependencies` — `tables` + `entities` as nodes, `deps` as edges. Tables stay because a
40
+ * view READS a table; dropping them would leave every dependency dangling.
41
+ */
42
+ export type SchemaScope = 'er' | 'dependencies';
43
+ /** The minimum of dbd's `SchemaModel` this view reads. v2 fields are optional. */
44
+ type SchemaModelInput = {
45
+ tables: unknown[];
46
+ refs: unknown[];
47
+ entities?: unknown[];
48
+ deps?: unknown[];
49
+ };
50
+ /**
51
+ * Consumer-side input for one scope: `nodes`, `edges` and the matching field map.
52
+ *
53
+ * Returned separately from `fromSchemaModel` because `<Graph>` takes `nodes`/`edges`/`fields`
54
+ * directly — a consumer binding the component does not want a pre-normalized model, and
55
+ * making them unpack one would mean normalizing twice.
56
+ */
57
+ export declare function toGraphInput(model: SchemaModelInput, scope?: SchemaScope): {
58
+ nodes: unknown[];
59
+ edges: unknown[];
60
+ fields: GraphFields;
61
+ };
62
+ /** Convenience wrapper over `normalizeGraph` for dbd-shaped schema JSON. */
63
+ export declare function fromSchemaModel(model: SchemaModelInput, options?: {
64
+ as?: SchemaScope;
65
+ }): GraphModel;
66
+ export {};
@@ -0,0 +1,92 @@
1
+ import { normalizeGraph } from '../model/normalize.js';
2
+ /**
3
+ * Field map from a dbd `SchemaModel`'s ER half to the canonical model.
4
+ *
5
+ * Exported so a consumer can hand it straight to `<Graph fields={SCHEMA_FIELDS}>`
6
+ * and skip the transform entirely.
7
+ *
8
+ * Note what is NOT here: dbd's `SchemaModel` type. The package never imports it,
9
+ * so the hand-written TS mirror of `schema_model.rs` stays dbd's concern and this
10
+ * package is not a third definition to keep in step.
11
+ *
12
+ * One canonical field stays deliberately unmapped, because dbd's shape does not carry it and
13
+ * a path that cannot resolve looks correct while silently never firing:
14
+ *
15
+ * - `cardinality` — dbd's `Ref` is `{ from, to, action? }`.
16
+ */
17
+ export const SCHEMA_FIELDS = {
18
+ // Identity must NOT be derived from the group, or regrouping re-keys every node. `id`
19
+ // defaults to `${group}.${label}`, so a consumer switching the axis to `kind` would turn
20
+ // `public.orders` into `table.orders` and every dep edge — which resolves
21
+ // `${from.s}.${name}` — would stop matching and the graph would render entirely unplaced.
22
+ // Safe for a consumer passing raw dbd rows too: a row with no `id` falls back as before.
23
+ id: 'id',
24
+ label: 'name',
25
+ group: 'schema',
26
+ kind: 'kind',
27
+ rows: 'columns',
28
+ note: 'noteMd',
29
+ rowName: 'name',
30
+ rowType: 'type',
31
+ rowNote: 'note',
32
+ // v2 (2026-09-27) added `fk` and `uq` to `Column`. `fk` is ALSO derived from the resolved
33
+ // refs; both are kept because they answer different questions — dbd resolved the
34
+ // constraint against the whole database, so a column referencing a table outside a scoped
35
+ // model is still honestly a foreign key even though no edge in this model proves it.
36
+ rowBadges: { pk: 'pk', nn: 'nn', fk: 'fk', uq: 'uq' },
37
+ source: 'from.t',
38
+ target: 'to.t',
39
+ sourceGroup: 'from.s',
40
+ targetGroup: 'to.s',
41
+ sourceRow: 'from.c',
42
+ targetRow: 'to.c',
43
+ action: 'action'
44
+ };
45
+ /**
46
+ * Field map for a dbd v2 `DepEdge` — `{ from: { s, n }, to: { s, n }, kind, unresolved }`.
47
+ *
48
+ * `n` not `t`, and no `c`: a dependency links whole objects, not columns. `kind` is the VERB
49
+ * (`reads | writes | calls | member`), which is why it maps to `relation` and the edge kind
50
+ * comes from `defaultEdgeKind` instead — every entry in `deps` is a dependency by definition.
51
+ */
52
+ export const DEPS_FIELDS = {
53
+ ...SCHEMA_FIELDS,
54
+ source: 'from.n',
55
+ target: 'to.n',
56
+ sourceGroup: 'from.s',
57
+ targetGroup: 'to.s',
58
+ sourceRow: undefined,
59
+ targetRow: undefined,
60
+ action: undefined,
61
+ relation: 'kind',
62
+ defaultEdgeKind: 'dependency'
63
+ };
64
+ /** A dbd node row, with the canonical id made explicit so the grouping axis stays free. */
65
+ function identified(rows) {
66
+ return rows.map((row) => {
67
+ const r = (row ?? {});
68
+ return { ...r, id: r.schema ? `${r.schema}.${r.name}` : String(r.name ?? '') };
69
+ });
70
+ }
71
+ /**
72
+ * Consumer-side input for one scope: `nodes`, `edges` and the matching field map.
73
+ *
74
+ * Returned separately from `fromSchemaModel` because `<Graph>` takes `nodes`/`edges`/`fields`
75
+ * directly — a consumer binding the component does not want a pre-normalized model, and
76
+ * making them unpack one would mean normalizing twice.
77
+ */
78
+ export function toGraphInput(model, scope = 'er') {
79
+ if (scope === 'er') {
80
+ return { nodes: identified(model.tables), edges: model.refs, fields: SCHEMA_FIELDS };
81
+ }
82
+ return {
83
+ nodes: identified([...model.tables, ...(model.entities ?? [])]),
84
+ edges: model.deps ?? [],
85
+ fields: DEPS_FIELDS
86
+ };
87
+ }
88
+ /** Convenience wrapper over `normalizeGraph` for dbd-shaped schema JSON. */
89
+ export function fromSchemaModel(model, options = {}) {
90
+ const { nodes, edges, fields } = toGraphInput(model, options.as ?? 'er');
91
+ return normalizeGraph(nodes, edges, fields);
92
+ }
@@ -0,0 +1,8 @@
1
+ export { default as EntityView } from './EntityView.svelte';
2
+ export { default as EntitiesView } from './EntitiesView.svelte';
3
+ export { default as NoteBlocks } from './NoteBlocks.svelte';
4
+ export { SCHEMA_FIELDS, DEPS_FIELDS, fromSchemaModel, toGraphInput } from './fromSchemaModel.js';
5
+ export type { SchemaScope } from './fromSchemaModel.js';
6
+ export { inlineSegs, noteBlocks } from './notes.js';
7
+ export { baseType, typeSize } from './column-type.js';
8
+ export type { Block, Seg } from './notes.js';
@@ -0,0 +1,7 @@
1
+ // Public surface for `@rokkit/graph/schema` — the ER/schema-flavoured views.
2
+ export { default as EntityView } from './EntityView.svelte';
3
+ export { default as EntitiesView } from './EntitiesView.svelte';
4
+ export { default as NoteBlocks } from './NoteBlocks.svelte';
5
+ export { SCHEMA_FIELDS, DEPS_FIELDS, fromSchemaModel, toGraphInput } from './fromSchemaModel.js';
6
+ export { inlineSegs, noteBlocks } from './notes.js';
7
+ export { baseType, typeSize } from './column-type.js';
@@ -0,0 +1,12 @@
1
+ export type Seg = {
2
+ code: boolean;
3
+ text: string;
4
+ };
5
+ export type Block = {
6
+ type: 'p' | 'ul';
7
+ lines: Seg[][];
8
+ };
9
+ /** Split a line into plain / `code` segments. */
10
+ export declare function inlineSegs(text: string): Seg[];
11
+ /** Parse a note string into paragraph / bullet-list blocks. */
12
+ export declare function noteBlocks(src?: string): Block[];
@@ -0,0 +1,65 @@
1
+ /* Minimal note renderer for DDL comments: paragraphs, bullet lists, and `inline code`.
2
+ Shared by EntitiesView and EntityView so they format comments identically. Returns a
3
+ structured tree the components render.
4
+
5
+ Deliberately NOT a markdown library. It recognises backticks and bullets and nothing
6
+ else, so no tag is ever interpreted — the segment text carries any markup verbatim and
7
+ Svelte's text interpolation escapes it at render time. That is what lets this package
8
+ ship with no markdown parser and no sanitiser, which are exactly the dependencies
9
+ @rokkit/ui's MarkdownRenderer would have dragged in. */
10
+ const BULLET = /^[-•]\s/;
11
+ /** Split a line into plain / `code` segments. */
12
+ export function inlineSegs(text) {
13
+ const segs = [];
14
+ let last = 0;
15
+ for (const m of text.matchAll(/`([^`]+)`/g)) {
16
+ const idx = m.index ?? 0;
17
+ if (idx > last)
18
+ segs.push({ code: false, text: text.slice(last, idx) });
19
+ segs.push({ code: true, text: m[1] });
20
+ last = idx + m[0].length;
21
+ }
22
+ if (last < text.length)
23
+ segs.push({ code: false, text: text.slice(last) });
24
+ return segs;
25
+ }
26
+ /** Consume a run of bullet lines starting at `i`, returning the block and the next index. */
27
+ function takeList(lines, start) {
28
+ const items = [];
29
+ let i = start;
30
+ while (i < lines.length && BULLET.test(lines[i].trim())) {
31
+ items.push(inlineSegs(lines[i].trim().replace(BULLET, '')));
32
+ i++;
33
+ }
34
+ return { block: { type: 'ul', lines: items }, next: i };
35
+ }
36
+ /** Consume a run of non-blank, non-bullet lines as one paragraph. */
37
+ function takeParagraph(lines, start) {
38
+ const para = [];
39
+ let i = start;
40
+ while (i < lines.length && lines[i].trim() && !BULLET.test(lines[i].trim())) {
41
+ para.push(lines[i].trim());
42
+ i++;
43
+ }
44
+ return { block: { type: 'p', lines: [inlineSegs(para.join(' '))] }, next: i };
45
+ }
46
+ /** Parse a note string into paragraph / bullet-list blocks. */
47
+ export function noteBlocks(src) {
48
+ if (!src)
49
+ return [];
50
+ const out = [];
51
+ const lines = src.split('\n');
52
+ let i = 0;
53
+ while (i < lines.length) {
54
+ if (!lines[i].trim()) {
55
+ i++;
56
+ continue;
57
+ }
58
+ const { block, next } = BULLET.test(lines[i].trim())
59
+ ? takeList(lines, i)
60
+ : takeParagraph(lines, i);
61
+ out.push(block);
62
+ i = next;
63
+ }
64
+ return out;
65
+ }