@dudousxd/nestjs-catalog 0.17.0 → 0.18.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.
@@ -14,7 +14,7 @@
14
14
  */
15
15
  export declare const CATALOG_LIB = "catalog";
16
16
  /** Every event name this package emits. Exported so a watcher can claim them. */
17
- export declare const CATALOG_EVENTS: readonly ["schema.changed", "snapshot.written", "snapshot.committed", "snapshot.dropped", "type.curated", "overlay.reset", "connector.run.started", "connector.run.finished", "transform.changed", "workflow.changed", "query.shared", "dashboard.shared"];
17
+ export declare const CATALOG_EVENTS: readonly ["schema.changed", "snapshot.written", "snapshot.committed", "snapshot.dropped", "type.curated", "overlay.reset", "connector.run.started", "connector.run.finished", "transform.changed", "reusable-node.changed", "workflow.changed", "query.shared", "dashboard.shared"];
18
18
  export type CatalogEvent = (typeof CATALOG_EVENTS)[number];
19
19
  /**
20
20
  * Where each event sits in the life of one load.
@@ -286,6 +286,31 @@ export interface CatalogEventPayloads {
286
286
  version: number;
287
287
  changedBy: string;
288
288
  };
289
+ /**
290
+ * Someone changed a node body that several graphs share.
291
+ *
292
+ * The third authoring event, and the one that answers a question the other two
293
+ * cannot. `transform.changed` and `workflow.changed` both name something
294
+ * inside the graph that changed behaviour; this names a change **outside**
295
+ * every graph it affects. A load that started reading a different query last
296
+ * Tuesday, whose own graph has not been touched in a month, has its whole
297
+ * explanation here and nowhere else — which is precisely the silence that
298
+ * makes shared nodes dangerous and precisely what this event is for.
299
+ *
300
+ * It carries no list of the graphs downstream, deliberately. That list is a
301
+ * query (`GET reusable-nodes/:id/workflows`) and it is answered as of *now*,
302
+ * whereas an event is a record of one moment; freezing a list into the trail
303
+ * would produce a fact that is wrong by the following week and looks
304
+ * authoritative.
305
+ */
306
+ 'reusable-node.changed': {
307
+ reusableNodeId: string;
308
+ name: string;
309
+ /** `source` or `sink` — see `REUSABLE_NODE_KINDS`. */
310
+ kind: string;
311
+ version: number;
312
+ changedBy: string;
313
+ };
289
314
  /**
290
315
  * Someone changed the wiring rather than the code.
291
316
  *
@@ -32,6 +32,7 @@ exports.CATALOG_EVENTS = [
32
32
  'connector.run.started',
33
33
  'connector.run.finished',
34
34
  'transform.changed',
35
+ 'reusable-node.changed',
35
36
  'workflow.changed',
36
37
  'query.shared',
37
38
  'dashboard.shared',
@@ -63,6 +64,13 @@ exports.CATALOG_EVENT_PHASE = {
63
64
  // one an earlier rank than the other would assert a causal order between two
64
65
  // edits that have none.
65
66
  'workflow.changed': 1,
67
+ // And the same rank again, for the same argument one step further out.
68
+ // Editing a reusable node is an authoring act that precedes the run being
69
+ // investigated, exactly as editing a transform or rewiring a graph is — and
70
+ // it is the one of the three that can change a load nobody edited, so a trace
71
+ // that could not place it beside the others would be missing the entry that
72
+ // explains the surprise.
73
+ 'reusable-node.changed': 1,
66
74
  // Curation carries no snapshot id, so it never lands inside a trace and this
67
75
  // rank is never consulted. It is written out rather than left to the fallback
68
76
  // because the type demands every event have one, and a `Record` that has to
@@ -7,7 +7,7 @@
7
7
  * schedules, retries and checkpoints, and writing a second one would mean two
8
8
  * systems each believing they decide when a load runs.
9
9
  */
10
- import type { CatalogRevision } from './catalog.workspace';
10
+ import { type CatalogRevision } from './catalog.workspace';
11
11
  /**
12
12
  * Where a connector pulls from.
13
13
  *
@@ -903,7 +903,7 @@ export declare function workflowRowY(row: number): number;
903
903
  * vocabularies would let them disagree. Credentials stay out of the catalog
904
904
  * here exactly as they do everywhere else.
905
905
  */
906
- export interface WorkflowSourceNode extends WorkflowNodeBase {
906
+ export interface WorkflowSourceNode extends WorkflowNodeBase, ReusableNodeRef {
907
907
  kind: 'source';
908
908
  /** Named `sourceKind` rather than `kind`, which the union already uses. */
909
909
  sourceKind: ConnectorKind;
@@ -925,6 +925,56 @@ export interface WorkflowTransformNode extends WorkflowNodeBase {
925
925
  * logic used at three points in a graph is versioned once and fixed once.
926
926
  */
927
927
  transformId: string;
928
+ /**
929
+ * Which version of that code to run. Absent follows the latest.
930
+ *
931
+ * ## The claim this field exists to make true
932
+ *
933
+ * The line above says a shared transform is "versioned once and fixed once",
934
+ * and until this field there was nothing here to fix it *to*. A transform node
935
+ * named a `transformId` and nothing else, `runTransform` resolved it with
936
+ * `getTransform`, and `getTransform` answers with whatever is in the row
937
+ * today. So editing a transform changed every graph that referenced it, at
938
+ * once, with nothing in anybody else's diff and nothing in their run history
939
+ * to explain the change — the graph's fingerprint does not move (see
940
+ * `workflowGraphHash`, which excludes the transform's version on purpose and
941
+ * still does) so there is not even a new graph version to look at.
942
+ *
943
+ * That was survivable only because almost nothing was shared. It stops being
944
+ * survivable the moment reusable nodes make sharing the point, which is why
945
+ * this landed with them rather than after them.
946
+ *
947
+ * The precedent is {@link WorkflowCallNode.callVersion}, whose docblock makes
948
+ * the same argument about somebody else's workflow: the version is authored,
949
+ * and a run that would have used a different one is refused rather than
950
+ * quietly run. This is that rule pointed at code stored in the same database.
951
+ *
952
+ * ## Why absent is allowed to mean "latest" rather than being backfilled
953
+ *
954
+ * Because that is what every graph already in a deployment means, exactly, and
955
+ * a backfill would be a behaviour change dressed as a migration. Pinning the
956
+ * live version at upgrade time freezes graphs whose authors have been relying
957
+ * on edits reaching them; pinning nothing but *refusing* an unpinned node
958
+ * stops every scheduled load on the deployment. Both are an upgrade that
959
+ * changes what runs, and neither is a decision this package gets to make for
960
+ * somebody. So absent keeps meaning precisely what it has always meant, and
961
+ * the repair is that following is now a **stated** position with a pinned
962
+ * alternative beside it, rather than the only position and an unstated one.
963
+ *
964
+ * What does change is that it is no longer silent: `describeTransformPin`
965
+ * turns either state into a sentence a screen can render, so "this follows
966
+ * whatever that code becomes" is something the author is told rather than
967
+ * something they find out.
968
+ *
969
+ * ## What a pin costs
970
+ *
971
+ * A pinned version is resolved out of `catalog_revision`, which is bounded per
972
+ * subject (`CATALOG_REVISION_LIMIT`). A pin to a version that has been evicted
973
+ * cannot be honoured, and the run fails saying so rather than falling back to
974
+ * the latest — a pin nobody could check is not a pin, which is the sentence
975
+ * `WorkflowRunSteps.checkCall` already stands on.
976
+ */
977
+ transformVersion?: number;
928
978
  }
929
979
  /**
930
980
  * Writes into an object type and commits.
@@ -937,9 +987,17 @@ export interface WorkflowTransformNode extends WorkflowNodeBase {
937
987
  * convenience that two workflows already provide. Branching inside the graph
938
988
  * stays fully supported; every path simply has to arrive here.
939
989
  */
940
- export interface WorkflowSinkNode extends WorkflowNodeBase {
990
+ export interface WorkflowSinkNode extends WorkflowNodeBase, ReusableNodeRef {
941
991
  kind: 'sink';
942
- /** Which object type the rows become. */
992
+ /**
993
+ * Which object type the rows become.
994
+ *
995
+ * Stays on the node even when the node is an instance of a reusable one, and
996
+ * that is the one field a reusable body may **not** move under a graph. See
997
+ * {@link ReusableNodeRef} — the write grants a graph was checked against are
998
+ * checked against this string, so a reusable sink that could repoint it would
999
+ * be a way to write a type the author was never granted.
1000
+ */
943
1001
  targetType: string;
944
1002
  /**
945
1003
  * Whether the commit replaces the dataset or merges into it. Exactly the
@@ -1586,6 +1644,281 @@ export interface WorkflowFilterNode extends WorkflowNodeBase {
1586
1644
  * a `transformId` and nothing catches it.
1587
1645
  */
1588
1646
  export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode | WorkflowFilterNode;
1647
+ /**
1648
+ * The node kinds that can be saved once and used in several graphs.
1649
+ *
1650
+ * Source and sink, and the reason is that those two are the only kinds whose
1651
+ * *composition* is worth a name. A connection is already a shared object, and it
1652
+ * answers "which database" — but nobody reaches for "the warehouse" when they
1653
+ * draw a graph, they reach for "the nightly MVR pull from the warehouse", which
1654
+ * is the connection **plus** the query, plus whether it reads everything or only
1655
+ * what changed, plus what the thing is called. That composition had nowhere to
1656
+ * live, so it was retyped per graph and the fourteenth copy was the one with the
1657
+ * typo in the `WHERE` clause.
1658
+ *
1659
+ * A transform is deliberately **not** here, and that is not an omission: a
1660
+ * transform is already a stored object referenced by id
1661
+ * ({@link WorkflowTransformNode.transformId}), so a reusable transform node
1662
+ * would be a second way to say the same thing. What it was missing is a version
1663
+ * pin, which is {@link WorkflowTransformNode.transformVersion}, not this.
1664
+ *
1665
+ * `call`, `if` and `filter` are not here either, and the record below is where
1666
+ * each of them says so — see {@link NODE_KIND_IS_REUSABLE}.
1667
+ */
1668
+ export declare const REUSABLE_NODE_KINDS: readonly ["source", "sink"];
1669
+ export type ReusableNodeKind = (typeof REUSABLE_NODE_KINDS)[number];
1670
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
1671
+ export declare function isReusableNodeKind(value: unknown): value is ReusableNodeKind;
1672
+ /**
1673
+ * Whether each node kind may be saved as a reusable node.
1674
+ *
1675
+ * A record over every kind rather than a shorter list of the two that can,
1676
+ * because this codebase keeps being bitten by hand-maintained lists going quiet
1677
+ * — most recently the add-node row that shipped the `filter` node with no way to
1678
+ * create it. A kind added to {@link WORKFLOW_NODE_KINDS} without an entry here
1679
+ * is a type error in this file naming the decision it has not made.
1680
+ *
1681
+ * The second `satisfies` is the other half of the same guard, in the other
1682
+ * direction: anything named in {@link REUSABLE_NODE_KINDS} has to be `true`
1683
+ * here, so the list and this table cannot come apart. Adding `'filter'` to that
1684
+ * list without a `ReusableFilterBody` therefore fails to compile twice — once
1685
+ * here, and once at every narrowing over {@link ReusableNodeBody}.
1686
+ *
1687
+ * Why each `false`:
1688
+ *
1689
+ * - `transform` — already a reference to a stored object. See
1690
+ * {@link REUSABLE_NODE_KINDS}.
1691
+ * - `call` — already a reference to somebody else's registered workflow, pinned
1692
+ * by name and version. There is nothing left to name.
1693
+ * - `if` and `filter` — a predicate is *about* the rows in front of it. A gate
1694
+ * saved under a name and dropped into another graph tests a column that graph
1695
+ * may not have, and a filter is worse: {@link WorkflowFilterNode.narrows} is
1696
+ * an acknowledgement about *this* graph's sinks, so a shared one would carry
1697
+ * somebody else's acknowledgement into a graph they never saw.
1698
+ */
1699
+ export declare const NODE_KIND_IS_REUSABLE: {
1700
+ readonly source: true;
1701
+ readonly transform: false;
1702
+ readonly sink: true;
1703
+ readonly call: false;
1704
+ readonly if: false;
1705
+ readonly filter: false;
1706
+ };
1707
+ /** Whether this kind can be saved as a reusable node. Reads {@link NODE_KIND_IS_REUSABLE}. */
1708
+ export declare function nodeKindIsReusable(kind: WorkflowNodeKind): boolean;
1709
+ /**
1710
+ * What a node carries when it is an instance of a reusable one.
1711
+ *
1712
+ * ## By reference, and that is the whole feature
1713
+ *
1714
+ * The cheap version of "save this node" copies its fields into the next graph
1715
+ * and forgets where they came from. It is cheaper in every way except the one
1716
+ * that was asked for: "quantos workflows tão usando quais nós" is unanswerable
1717
+ * about a copy, because after the copy there is nothing left that says the two
1718
+ * nodes are the same node. So the id stays on the node, `GET
1719
+ * reusable-nodes/:id/workflows` counts by it, and the count is exact rather than
1720
+ * a guess at which configurations look alike.
1721
+ *
1722
+ * ## The fields stay on the node as well, and are not the authority
1723
+ *
1724
+ * A source node that names a reusable node still carries its own `sourceKind`,
1725
+ * `config` and the rest. That is a **cache**, not a copy — the identical
1726
+ * arrangement `toGraph` already documents for a source that names a connector:
1727
+ * the fields are kept so that `validateWorkflow` stays pure and the canvas can
1728
+ * draw the node without a round trip, and execution re-reads the stored object,
1729
+ * so an edit takes effect on the next run.
1730
+ *
1731
+ * ## Which is exactly why {@link version} exists
1732
+ *
1733
+ * "An edit takes effect on the next run" is the useful behaviour and the
1734
+ * dangerous one, and which of the two it is depends on whether the person
1735
+ * editing knows who else is downstream. So the reference states its position:
1736
+ *
1737
+ * - **absent** — follows the latest. What a connector reference has always
1738
+ * meant, and the right default for "the warehouse pull" that four graphs
1739
+ * share and all four want fixed at once.
1740
+ * - **present** — pinned. The reusable node may move on and this graph does not,
1741
+ * until somebody edits *this* graph, which is a new version of it with a diff
1742
+ * to read.
1743
+ *
1744
+ * Both are in the graph fingerprint, so changing position is an edit; neither is
1745
+ * silent, which was the whole complaint. This is the same rule
1746
+ * {@link WorkflowTransformNode.transformVersion} states for transforms, and it
1747
+ * is stated twice on purpose rather than shared: they are two different stored
1748
+ * objects and a reader arriving at either should not have to find the other.
1749
+ */
1750
+ export interface ReusableNodeRef {
1751
+ /**
1752
+ * The reusable node this is an instance of, or absent for a node configured
1753
+ * in place. Both remain first-class, indefinitely: a one-off source is not a
1754
+ * failure to reuse something.
1755
+ */
1756
+ useId?: string;
1757
+ /** The pinned version of that reusable node. Absent follows the latest. */
1758
+ useVersion?: number;
1759
+ }
1760
+ /**
1761
+ * The part of a source node that is worth saving under a name.
1762
+ *
1763
+ * Everything a source needs to read, and nothing that belongs to the graph it
1764
+ * sits in. Absent here, on purpose: `id`, which has to be unique within one
1765
+ * graph and is also a durable step name, and `position`, which is where somebody
1766
+ * dragged the box on one canvas.
1767
+ *
1768
+ * `name` is absent too, and that is the less obvious one. A reusable node has a
1769
+ * name — it is how "flip db sink" is a thing anybody can ask for — but it lives
1770
+ * on {@link CatalogReusableNode} rather than in the body, because a graph is
1771
+ * allowed to call its instance something else. Folding the name in would rename
1772
+ * every node in every graph the moment somebody tidied up the library's naming,
1773
+ * and a node's name is documented as cosmetic precisely so that it is nobody
1774
+ * else's business.
1775
+ */
1776
+ export interface ReusableSourceBody {
1777
+ kind: 'source';
1778
+ sourceKind: ConnectorKind;
1779
+ connectionId?: string;
1780
+ config: Record<string, unknown>;
1781
+ secretEnvVar?: string;
1782
+ mode?: 'full' | 'incremental';
1783
+ }
1784
+ /**
1785
+ * The part of a sink node that is worth saving under a name.
1786
+ *
1787
+ * `targetType` is in here, so "the Mvr full reload" is a thing that can be named
1788
+ * — and it is also the field {@link applyReusableNode} refuses to move under a
1789
+ * graph that already committed to a different one. Both are true at once and
1790
+ * they are not in tension: a graph adopting this body *takes* the type at the
1791
+ * moment it is saved, and is grant-checked for it then. What may not happen is
1792
+ * the type changing afterwards, under a graph whose author is not looking, into
1793
+ * one they were never granted.
1794
+ */
1795
+ export interface ReusableSinkBody {
1796
+ kind: 'sink';
1797
+ targetType: string;
1798
+ mode?: 'full' | 'incremental';
1799
+ }
1800
+ export type ReusableNodeBody = ReusableSourceBody | ReusableSinkBody;
1801
+ /**
1802
+ * {@link unreachableNodeKind}, for reusable bodies, and for the identical
1803
+ * reason: every branch over {@link ReusableNodeBody} ends here, so a body added
1804
+ * to the union without a rule for folding it onto a node is a type error naming
1805
+ * the file rather than a graph that saves and then runs a node nobody
1806
+ * configured. It throws as well, because these arrive as JSON out of a column.
1807
+ */
1808
+ export declare function unreachableReusableNodeKind(body: never, where: string): never;
1809
+ /**
1810
+ * A node body saved once, under a name, and used from several graphs.
1811
+ *
1812
+ * Versioned exactly as a {@link CatalogTransform} is, and archived in the same
1813
+ * `catalog_revision` table under its own subject — one table, one retention
1814
+ * rule, which is the argument `RevisionRow` already makes for holding transforms
1815
+ * and saved queries together. That is what makes
1816
+ * {@link ReusableNodeRef.version} resolvable rather than merely a number: a
1817
+ * graph pinned to v2 can still be handed v2's body after v3 exists.
1818
+ *
1819
+ * There is no library screen and there is deliberately not going to be one. A
1820
+ * reusable node is offered where a node is added and its usage count is shown on
1821
+ * the node itself, because the number changes a decision exactly at the moment
1822
+ * somebody is about to change something four other graphs depend on — which is
1823
+ * not a moment they spend on a listing page.
1824
+ */
1825
+ export interface CatalogReusableNode {
1826
+ id: string;
1827
+ /**
1828
+ * What people ask for it by — "flip db sink". Unique across reusable nodes,
1829
+ * enforced in the store, because two of them called the same thing is a
1830
+ * picker that cannot be used.
1831
+ */
1832
+ name: string;
1833
+ description?: string;
1834
+ /** Which node kind this stands for. Redundant with `body.kind` and indexed. */
1835
+ kind: ReusableNodeKind;
1836
+ body: ReusableNodeBody;
1837
+ /**
1838
+ * Counts saves that changed the **body**, exactly as a transform's counts
1839
+ * saves that changed the code: renaming a reusable node is not a new version
1840
+ * of it, and inflating the number would make a pin to it meaningless.
1841
+ */
1842
+ version: number;
1843
+ createdBy: string;
1844
+ createdAt: string;
1845
+ updatedAt: string;
1846
+ }
1847
+ /**
1848
+ * How many graphs use a reusable node, and which.
1849
+ *
1850
+ * Shaped after `GET connections/:id/workflows`, which answers the same question
1851
+ * for a connection, because an operator asking either is about to do the same
1852
+ * thing: change something and want to know who is downstream. A count on its own
1853
+ * would be a number to be alarmed by; the list is what makes it actionable.
1854
+ */
1855
+ export interface CatalogReusableNodeUse {
1856
+ workflowId: string;
1857
+ workflowName: string;
1858
+ status: WorkflowStatus;
1859
+ /** The node within that graph, and the position its reference states. */
1860
+ nodeId: string;
1861
+ nodeName: string;
1862
+ /** The pinned version, or absent for a reference that follows the latest. */
1863
+ pinnedVersion?: number;
1864
+ }
1865
+ /**
1866
+ * Fold a reusable body onto the node that references it.
1867
+ *
1868
+ * The one implementation, called by the store when a graph is saved and by the
1869
+ * runner when one is executed, so the node a canvas draws and the node that runs
1870
+ * cannot describe different reads. Pure, and it takes the body rather than
1871
+ * fetching one, for the reason `validateWorkflow` is pure: this file is imported
1872
+ * by the browser entry point.
1873
+ *
1874
+ * ## What it refuses
1875
+ *
1876
+ * A sink body whose `targetType` differs from the one already on the node. That
1877
+ * is not a tidiness check — it is the same shape as `WorkflowRunSteps.checkCall`
1878
+ * and it is load-bearing for the same reason. A graph's sinks are checked
1879
+ * against the author's write grants (`assertMayWriteTypes`) using the type on
1880
+ * the node, at save time. If a reusable body could repoint that afterwards, then
1881
+ * editing a shared sink would write into a type that nobody with access to this
1882
+ * graph was ever granted — and it would do it on a schedule, with the graph's
1883
+ * own diff showing nothing. So the disagreement fails, naming both types, and
1884
+ * the repair is that the referencing graph is re-saved and re-checked.
1885
+ *
1886
+ * A mismatched *kind* is refused for the plainer reason that there is nothing
1887
+ * sensible to do with it: a sink body on a source node is a reference somebody
1888
+ * repointed at the wrong object, and folding half of it in would produce a node
1889
+ * that is neither.
1890
+ */
1891
+ export declare function applyReusableNode(node: WorkflowNode, body: ReusableNodeBody): WorkflowNode;
1892
+ /** Whether a stored value is a reusable body this build can execute. */
1893
+ export declare function isReusableNodeBody(value: unknown): value is ReusableNodeBody;
1894
+ /**
1895
+ * The body a node is currently carrying, ready to be saved under a name.
1896
+ *
1897
+ * The other direction of {@link applyReusableNode}, and the reason
1898
+ * save-as-reusable cannot silently deep-copy: this is what gets stored, the node
1899
+ * keeps a `useId` pointing at it, and nothing anywhere duplicates a graph.
1900
+ *
1901
+ * Answers `undefined` for a kind that cannot be reusable rather than throwing,
1902
+ * because the caller is a route answering a person who pressed a button on a
1903
+ * node — {@link nodeKindIsReusable} is what a screen asks before offering it,
1904
+ * and the route repeats the question rather than trusting the screen asked.
1905
+ */
1906
+ export declare function reusableNodeBodyOf(node: WorkflowNode): ReusableNodeBody | undefined;
1907
+ /**
1908
+ * What a node's version discipline is, as a sentence a screen can render.
1909
+ *
1910
+ * Here rather than in the console for the reason `describeDurability`'s siblings
1911
+ * are: a screen that worked out its own wording would eventually describe
1912
+ * "follows the latest" as though it were a pin, which is the misunderstanding
1913
+ * this whole field exists to remove. One sentence, one place, and the console
1914
+ * renders it.
1915
+ */
1916
+ export interface VersionPinCopy {
1917
+ pinned: boolean;
1918
+ label: string;
1919
+ detail: string;
1920
+ }
1921
+ export declare function describeVersionPin(version: number | undefined, subject: string): VersionPinCopy;
1589
1922
  /**
1590
1923
  * Which side of an {@link WorkflowIfNode} a wire leaves by.
1591
1924
  *
@@ -2088,7 +2421,7 @@ export interface CallableWorkflowBlock {
2088
2421
  }
2089
2422
  export declare function callableWorkflowBlock(ref: CallableWorkflowRef): CallableWorkflowBlock | undefined;
2090
2423
  /** Every way a graph can be refused. Exported so a canvas can key off the code. */
2091
- export declare const WORKFLOW_ISSUE_CODES: readonly ["empty", "invalid-node-id", "duplicate-node-id", "edge-endpoint-missing", "self-edge", "duplicate-edge", "cycle", "no-source", "source-has-input", "no-sink", "duplicate-sink-type", "sink-has-output", "unreachable", "dead-end", "transform-not-named", "call-not-named", "if-not-named", "if-threshold-invalid", "if-needs-one-input", "branch-not-labelled", "branch-on-plain-edge", "filter-predicate-invalid", "filter-narrows-unacknowledged", "filter-narrows-nothing"];
2424
+ export declare const WORKFLOW_ISSUE_CODES: readonly ["empty", "invalid-node-id", "duplicate-node-id", "edge-endpoint-missing", "self-edge", "duplicate-edge", "cycle", "no-source", "source-has-input", "no-sink", "duplicate-sink-type", "sink-has-output", "unreachable", "dead-end", "transform-not-named", "call-not-named", "if-not-named", "if-threshold-invalid", "if-needs-one-input", "branch-not-labelled", "branch-on-plain-edge", "filter-predicate-invalid", "filter-narrows-unacknowledged", "filter-narrows-nothing", "version-pin-invalid"];
2092
2425
  export type WorkflowIssueCode = (typeof WORKFLOW_ISSUE_CODES)[number];
2093
2426
  export interface WorkflowValidationIssue {
2094
2427
  code: WorkflowIssueCode;
@@ -2441,6 +2774,34 @@ export declare function supportsWorkflows(store: CatalogPipelineStore): store is
2441
2774
  * flag is a claim and a method is the thing itself.
2442
2775
  */
2443
2776
  export declare function supportsTransformRevisions(store: CatalogPipelineStore): store is CatalogPipelineStore & Required<Pick<CatalogPipelineStore, 'listTransformRevisions'>>;
2777
+ /**
2778
+ * Whether this store can produce one particular version of a transform's code.
2779
+ *
2780
+ * Separate from {@link supportsTransformRevisions}, and not implied by it: one
2781
+ * answers "can a screen show the history", the other "can a run honour a pin".
2782
+ * A store could reasonably have the first and not the second, and folding them
2783
+ * together would let a graph be saved with a pin this deployment cannot resolve
2784
+ * — discovered mid-load rather than at the moment the pin was set.
2785
+ */
2786
+ export declare function supportsTransformPins(store: CatalogPipelineStore): store is CatalogPipelineStore & Required<Pick<CatalogPipelineStore, 'getTransformAt'>>;
2787
+ /**
2788
+ * The four reads and two writes a reusable node needs, as one derived type.
2789
+ *
2790
+ * `Required<Pick<...>>` rather than a second interface, which is the lesson
2791
+ * {@link CatalogLoadExpectationStore} records: these are optional members OF the
2792
+ * pipeline store, so writing them out again here would be a copy that can drift.
2793
+ */
2794
+ export type CatalogReusableNodeStore = Required<Pick<CatalogPipelineStore, 'listReusableNodes' | 'getReusableNode' | 'getReusableNodeAt' | 'saveReusableNode' | 'deleteReusableNode' | 'reusableNodeUses'>>;
2795
+ /**
2796
+ * Whether this store can hold reusable nodes.
2797
+ *
2798
+ * All six, and never a subset. A store with `getReusableNode` but no
2799
+ * `reusableNodeUses` could serve a picker and could not answer the question the
2800
+ * feature exists for — and the shape of that failure is a console offering to
2801
+ * share a node while being unable to say who already depends on it, which is
2802
+ * worse than not offering at all.
2803
+ */
2804
+ export declare function supportsReusableNodes(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogReusableNodeStore;
2444
2805
  export declare function supportsWorkflowStages(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogStageStore;
2445
2806
  /**
2446
2807
  * A store that really does hold operator-set expectations, all four members
@@ -2526,6 +2887,88 @@ export interface CatalogPipelineStore extends Partial<CatalogWorkflowStore>, Par
2526
2887
  * that bound costs.
2527
2888
  */
2528
2889
  listTransformRevisions?(id: string): Promise<CatalogRevision[]>;
2890
+ /**
2891
+ * The code at one particular version, which is what a pin resolves through.
2892
+ *
2893
+ * Separate from {@link listTransformRevisions} rather than left to a caller
2894
+ * filtering that list, and the difference is the whole point: this is on the
2895
+ * hot path of every pinned transform node of every run, and reading up to
2896
+ * {@link CATALOG_REVISION_LIMIT} whole code bodies to keep one of them is a
2897
+ * cost paid per node per run. It is also a different answer — the list falls
2898
+ * back to a synthesised head for a subject that predates the revision table,
2899
+ * and this must not, because "the only version we can produce is the current
2900
+ * one" is exactly the case a pin needs to be told about rather than handed.
2901
+ *
2902
+ * `undefined` means the version cannot be produced: it predates the archive,
2903
+ * or it has fallen off the far end of the per-subject cap. The runner turns
2904
+ * that into a failed node rather than a fall-back to the latest, which is the
2905
+ * same stand `WorkflowRunSteps.checkCall` takes — a pin nobody could check is
2906
+ * not a pin.
2907
+ *
2908
+ * **Optional**, mixed in exactly as its neighbour above is and for the same
2909
+ * reason. {@link supportsTransformPins} is how a caller asks; a store without
2910
+ * it can still run graphs, and a graph with a pinned node is refused there
2911
+ * with a sentence rather than by a method that is missing at run time.
2912
+ */
2913
+ getTransformAt?(id: string, version: number): Promise<CatalogTransform | undefined>;
2914
+ /**
2915
+ * Node bodies saved under a name and used from several graphs.
2916
+ *
2917
+ * **Optional**, mixed in for the reason every optional member here is: a store
2918
+ * written against the previous shape of this interface still satisfies it, and
2919
+ * a purely additive feature must not turn that into a compile error — or,
2920
+ * worse, into a run-time discovery, since the `supports*` probes narrow
2921
+ * structurally. {@link supportsReusableNodes} is how a caller asks.
2922
+ *
2923
+ * A deployment whose store implements none of these behaves exactly as it does
2924
+ * today: every node is configured in place, which is what they all are.
2925
+ */
2926
+ listReusableNodes?(): Promise<CatalogReusableNode[]>;
2927
+ getReusableNode?(id: string): Promise<CatalogReusableNode | undefined>;
2928
+ /**
2929
+ * The body at one particular version, for a reference that pinned one.
2930
+ *
2931
+ * The sibling of {@link getTransformAt}, with the same contract and the same
2932
+ * `undefined`: a version the archive can no longer produce is reported as
2933
+ * absent and never substituted with the latest.
2934
+ */
2935
+ getReusableNodeAt?(id: string, version: number): Promise<CatalogReusableNode | undefined>;
2936
+ /**
2937
+ * Bumps {@link CatalogReusableNode.version} when the **body** changed, and
2938
+ * archives it, exactly as {@link saveTransform} does for code.
2939
+ *
2940
+ * Editing a reusable node that other graphs pin therefore creates a new
2941
+ * version rather than refusing. Refusing was the other candidate and it is the
2942
+ * wrong one: this is the only editor there is, so a refusal would strand
2943
+ * whoever owns the node the moment anybody else pinned it, and would make
2944
+ * pinning a way to take something hostage. Creating a version costs the pinned
2945
+ * graphs nothing — they resolve through the archive and keep running the body
2946
+ * they named — and what the editor gets instead of a refusal is the count of
2947
+ * who is downstream, at the moment they are about to press save.
2948
+ */
2949
+ saveReusableNode?(input: Pick<CatalogReusableNode, 'name' | 'body'> & {
2950
+ id?: string;
2951
+ description?: string;
2952
+ }, createdBy: string): Promise<CatalogReusableNode>;
2953
+ /**
2954
+ * Refuses while any graph still references it.
2955
+ *
2956
+ * The same refusal {@link deleteConnection} makes and for the same reason:
2957
+ * deleting one out from under its graphs turns every one of them into a load
2958
+ * that fails at run time, discovered on a schedule rather than at the moment
2959
+ * somebody decided.
2960
+ */
2961
+ deleteReusableNode?(id: string): Promise<boolean>;
2962
+ /**
2963
+ * Which graphs use a reusable node, and at which node within each.
2964
+ *
2965
+ * On the store rather than derived in a controller from `listWorkflows`,
2966
+ * unlike `connections/:id/workflows` which does exactly that. The difference
2967
+ * is that this number is rendered *beside every entry of a picker*, so
2968
+ * deriving it would mean parsing every graph in the deployment once per
2969
+ * reusable node offered. A store can answer it from the rows it holds.
2970
+ */
2971
+ reusableNodeUses?(id: string): Promise<CatalogReusableNodeUse[]>;
2529
2972
  /**
2530
2973
  * Per-type load expectations as an operator set them.
2531
2974
  *
@@ -9,7 +9,7 @@
9
9
  * systems each believing they decide when a load runs.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.REDACTED_SECRET = exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_CALL_CONTRACT = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_BRANCH_LABELS = exports.WORKFLOW_FILTER_MAX_VALUES = exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_COLUMN_GAP = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_SKIP_REASONS = exports.CODE_CONTEXT_CONTRACT = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
12
+ exports.REDACTED_SECRET = exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_CALL_CONTRACT = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_BRANCH_LABELS = exports.NODE_KIND_IS_REUSABLE = exports.REUSABLE_NODE_KINDS = exports.WORKFLOW_FILTER_MAX_VALUES = exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_COLUMN_GAP = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_SKIP_REASONS = exports.CODE_CONTEXT_CONTRACT = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
13
13
  exports.isConnectorKind = isConnectorKind;
14
14
  exports.isTransformLanguage = isTransformLanguage;
15
15
  exports.isWorkflowSkipReason = isWorkflowSkipReason;
@@ -27,6 +27,13 @@ exports.unreachableFilterOperator = unreachableFilterOperator;
27
27
  exports.isWorkflowFilterValue = isWorkflowFilterValue;
28
28
  exports.isWorkflowFilterPredicate = isWorkflowFilterPredicate;
29
29
  exports.workflowFilterMatches = workflowFilterMatches;
30
+ exports.isReusableNodeKind = isReusableNodeKind;
31
+ exports.nodeKindIsReusable = nodeKindIsReusable;
32
+ exports.unreachableReusableNodeKind = unreachableReusableNodeKind;
33
+ exports.applyReusableNode = applyReusableNode;
34
+ exports.isReusableNodeBody = isReusableNodeBody;
35
+ exports.reusableNodeBodyOf = reusableNodeBodyOf;
36
+ exports.describeVersionPin = describeVersionPin;
30
37
  exports.isWorkflowBranchLabel = isWorkflowBranchLabel;
31
38
  exports.isWorkflowStatus = isWorkflowStatus;
32
39
  exports.isWorkflowExecutionMode = isWorkflowExecutionMode;
@@ -41,9 +48,17 @@ exports.isWorkflowNode = isWorkflowNode;
41
48
  exports.isWorkflowEdge = isWorkflowEdge;
42
49
  exports.supportsWorkflows = supportsWorkflows;
43
50
  exports.supportsTransformRevisions = supportsTransformRevisions;
51
+ exports.supportsTransformPins = supportsTransformPins;
52
+ exports.supportsReusableNodes = supportsReusableNodes;
44
53
  exports.supportsWorkflowStages = supportsWorkflowStages;
45
54
  exports.supportsLoadExpectations = supportsLoadExpectations;
46
55
  exports.isPipelineStore = isPipelineStore;
56
+ // The revision shape is declared beside the audit trail rather than here,
57
+ // because it is one shape over two subjects — a transform's code and a saved
58
+ // query's SQL — and neither of them owns it. This is a type-only import, and the
59
+ // edge only ever points this way: `catalog.workspace.ts` knows nothing about
60
+ // pipelines.
61
+ const catalog_workspace_1 = require("./catalog.workspace");
47
62
  /**
48
63
  * Where a connector pulls from.
49
64
  *
@@ -767,6 +782,201 @@ function orderedHolds(operator, held, wanted) {
767
782
  return held < wanted;
768
783
  return held <= wanted;
769
784
  }
785
+ /* --- reusable nodes ------------------------------------------------------ */
786
+ /**
787
+ * The node kinds that can be saved once and used in several graphs.
788
+ *
789
+ * Source and sink, and the reason is that those two are the only kinds whose
790
+ * *composition* is worth a name. A connection is already a shared object, and it
791
+ * answers "which database" — but nobody reaches for "the warehouse" when they
792
+ * draw a graph, they reach for "the nightly MVR pull from the warehouse", which
793
+ * is the connection **plus** the query, plus whether it reads everything or only
794
+ * what changed, plus what the thing is called. That composition had nowhere to
795
+ * live, so it was retyped per graph and the fourteenth copy was the one with the
796
+ * typo in the `WHERE` clause.
797
+ *
798
+ * A transform is deliberately **not** here, and that is not an omission: a
799
+ * transform is already a stored object referenced by id
800
+ * ({@link WorkflowTransformNode.transformId}), so a reusable transform node
801
+ * would be a second way to say the same thing. What it was missing is a version
802
+ * pin, which is {@link WorkflowTransformNode.transformVersion}, not this.
803
+ *
804
+ * `call`, `if` and `filter` are not here either, and the record below is where
805
+ * each of them says so — see {@link NODE_KIND_IS_REUSABLE}.
806
+ */
807
+ exports.REUSABLE_NODE_KINDS = ['source', 'sink'];
808
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
809
+ function isReusableNodeKind(value) {
810
+ return exports.REUSABLE_NODE_KINDS.some((kind) => kind === value);
811
+ }
812
+ /**
813
+ * Whether each node kind may be saved as a reusable node.
814
+ *
815
+ * A record over every kind rather than a shorter list of the two that can,
816
+ * because this codebase keeps being bitten by hand-maintained lists going quiet
817
+ * — most recently the add-node row that shipped the `filter` node with no way to
818
+ * create it. A kind added to {@link WORKFLOW_NODE_KINDS} without an entry here
819
+ * is a type error in this file naming the decision it has not made.
820
+ *
821
+ * The second `satisfies` is the other half of the same guard, in the other
822
+ * direction: anything named in {@link REUSABLE_NODE_KINDS} has to be `true`
823
+ * here, so the list and this table cannot come apart. Adding `'filter'` to that
824
+ * list without a `ReusableFilterBody` therefore fails to compile twice — once
825
+ * here, and once at every narrowing over {@link ReusableNodeBody}.
826
+ *
827
+ * Why each `false`:
828
+ *
829
+ * - `transform` — already a reference to a stored object. See
830
+ * {@link REUSABLE_NODE_KINDS}.
831
+ * - `call` — already a reference to somebody else's registered workflow, pinned
832
+ * by name and version. There is nothing left to name.
833
+ * - `if` and `filter` — a predicate is *about* the rows in front of it. A gate
834
+ * saved under a name and dropped into another graph tests a column that graph
835
+ * may not have, and a filter is worse: {@link WorkflowFilterNode.narrows} is
836
+ * an acknowledgement about *this* graph's sinks, so a shared one would carry
837
+ * somebody else's acknowledgement into a graph they never saw.
838
+ */
839
+ exports.NODE_KIND_IS_REUSABLE = {
840
+ source: true,
841
+ transform: false,
842
+ sink: true,
843
+ call: false,
844
+ if: false,
845
+ filter: false,
846
+ };
847
+ /** Whether this kind can be saved as a reusable node. Reads {@link NODE_KIND_IS_REUSABLE}. */
848
+ function nodeKindIsReusable(kind) {
849
+ return exports.NODE_KIND_IS_REUSABLE[kind];
850
+ }
851
+ /**
852
+ * {@link unreachableNodeKind}, for reusable bodies, and for the identical
853
+ * reason: every branch over {@link ReusableNodeBody} ends here, so a body added
854
+ * to the union without a rule for folding it onto a node is a type error naming
855
+ * the file rather than a graph that saves and then runs a node nobody
856
+ * configured. It throws as well, because these arrive as JSON out of a column.
857
+ */
858
+ function unreachableReusableNodeKind(body, where) {
859
+ const kind = typeof body === 'string' ? body : Reflect.get(Object(body), 'kind');
860
+ throw new Error(`${where} does not handle a reusable node body of kind ${JSON.stringify(kind)}. The reusable kinds and every decision made per kind are meant to move together.`);
861
+ }
862
+ /**
863
+ * Fold a reusable body onto the node that references it.
864
+ *
865
+ * The one implementation, called by the store when a graph is saved and by the
866
+ * runner when one is executed, so the node a canvas draws and the node that runs
867
+ * cannot describe different reads. Pure, and it takes the body rather than
868
+ * fetching one, for the reason `validateWorkflow` is pure: this file is imported
869
+ * by the browser entry point.
870
+ *
871
+ * ## What it refuses
872
+ *
873
+ * A sink body whose `targetType` differs from the one already on the node. That
874
+ * is not a tidiness check — it is the same shape as `WorkflowRunSteps.checkCall`
875
+ * and it is load-bearing for the same reason. A graph's sinks are checked
876
+ * against the author's write grants (`assertMayWriteTypes`) using the type on
877
+ * the node, at save time. If a reusable body could repoint that afterwards, then
878
+ * editing a shared sink would write into a type that nobody with access to this
879
+ * graph was ever granted — and it would do it on a schedule, with the graph's
880
+ * own diff showing nothing. So the disagreement fails, naming both types, and
881
+ * the repair is that the referencing graph is re-saved and re-checked.
882
+ *
883
+ * A mismatched *kind* is refused for the plainer reason that there is nothing
884
+ * sensible to do with it: a sink body on a source node is a reference somebody
885
+ * repointed at the wrong object, and folding half of it in would produce a node
886
+ * that is neither.
887
+ */
888
+ function applyReusableNode(node, body) {
889
+ if (body.kind === 'source') {
890
+ if (node.kind !== 'source') {
891
+ throw new Error(reusableKindMismatch(node, body.kind));
892
+ }
893
+ return {
894
+ ...node,
895
+ sourceKind: body.sourceKind,
896
+ connectionId: body.connectionId,
897
+ config: body.config,
898
+ secretEnvVar: body.secretEnvVar,
899
+ mode: body.mode,
900
+ };
901
+ }
902
+ if (body.kind === 'sink') {
903
+ if (node.kind !== 'sink') {
904
+ throw new Error(reusableKindMismatch(node, body.kind));
905
+ }
906
+ if (node.targetType.length > 0 && node.targetType !== body.targetType) {
907
+ throw new Error(`Sink "${node.name}" (${node.id}) commits ${node.targetType}, and the reusable node it uses now commits ${body.targetType}. A graph is checked against the types its sinks write at the moment it is saved, so a shared sink is not allowed to repoint one afterwards — that would write into a type nobody here was granted. Re-save this graph to adopt ${body.targetType}, which checks the grants again, or pin this node to the version that still commits ${node.targetType}.`);
908
+ }
909
+ return { ...node, targetType: body.targetType, mode: body.mode };
910
+ }
911
+ return unreachableReusableNodeKind(body, 'applyReusableNode');
912
+ }
913
+ function reusableKindMismatch(node, bodyKind) {
914
+ return `Node "${node.name}" (${node.id}) is a ${node.kind} node and the reusable node it names is a ${bodyKind}. A reference that changed kind under a graph would leave a node that is neither, so this is refused rather than half-applied.`;
915
+ }
916
+ /** Whether a stored value is a reusable body this build can execute. */
917
+ function isReusableNodeBody(value) {
918
+ if (typeof value !== 'object' || value === null)
919
+ return false;
920
+ const kind = Reflect.get(value, 'kind');
921
+ if (kind === 'source') {
922
+ const config = Reflect.get(value, 'config');
923
+ return (isConnectorKind(Reflect.get(value, 'sourceKind')) &&
924
+ typeof config === 'object' &&
925
+ config !== null &&
926
+ !Array.isArray(config));
927
+ }
928
+ if (kind === 'sink') {
929
+ const targetType = Reflect.get(value, 'targetType');
930
+ return typeof targetType === 'string' && targetType.length > 0;
931
+ }
932
+ // Refused rather than defaulted, exactly as `isWorkflowNode` refuses a kind it
933
+ // does not know: a body read back as something this build has no rule for
934
+ // would be folded onto a node as nothing at all, and the node would then run
935
+ // whatever was cached on it while claiming to be an instance of the library's.
936
+ return false;
937
+ }
938
+ /**
939
+ * The body a node is currently carrying, ready to be saved under a name.
940
+ *
941
+ * The other direction of {@link applyReusableNode}, and the reason
942
+ * save-as-reusable cannot silently deep-copy: this is what gets stored, the node
943
+ * keeps a `useId` pointing at it, and nothing anywhere duplicates a graph.
944
+ *
945
+ * Answers `undefined` for a kind that cannot be reusable rather than throwing,
946
+ * because the caller is a route answering a person who pressed a button on a
947
+ * node — {@link nodeKindIsReusable} is what a screen asks before offering it,
948
+ * and the route repeats the question rather than trusting the screen asked.
949
+ */
950
+ function reusableNodeBodyOf(node) {
951
+ if (node.kind === 'source') {
952
+ return {
953
+ kind: 'source',
954
+ sourceKind: node.sourceKind,
955
+ connectionId: node.connectionId,
956
+ config: node.config,
957
+ secretEnvVar: node.secretEnvVar,
958
+ mode: node.mode,
959
+ };
960
+ }
961
+ if (node.kind === 'sink') {
962
+ return { kind: 'sink', targetType: node.targetType, mode: node.mode };
963
+ }
964
+ return undefined;
965
+ }
966
+ function describeVersionPin(version, subject) {
967
+ if (version === undefined) {
968
+ return {
969
+ pinned: false,
970
+ label: 'follows the latest',
971
+ detail: `This node runs whatever ${subject} says today. An edit to it reaches this graph on the next run, with no new version of this graph and nothing in its diff — which is what you want when the point is that everybody moves together, and is worth pinning against when it is not.`,
972
+ };
973
+ }
974
+ return {
975
+ pinned: true,
976
+ label: `pinned to v${version}`,
977
+ detail: `This node runs v${version} of ${subject} and stays there while it is edited elsewhere. Moving to a newer version is an edit to this graph, so it has a diff and a version of its own. A pin to a version that has been superseded more than ${catalog_workspace_1.CATALOG_REVISION_LIMIT} times can no longer be produced, and the run fails saying so rather than quietly using the latest.`,
978
+ };
979
+ }
770
980
  /**
771
981
  * Which side of an {@link WorkflowIfNode} a wire leaves by.
772
982
  *
@@ -980,6 +1190,15 @@ exports.WORKFLOW_ISSUE_CODES = [
980
1190
  'filter-predicate-invalid',
981
1191
  'filter-narrows-unacknowledged',
982
1192
  'filter-narrows-nothing',
1193
+ /**
1194
+ * A version pin that is not a version — `0`, `2.5`, `"3"`, `-1`.
1195
+ *
1196
+ * One code for both pins, because they are one mistake: a threshold that
1197
+ * cannot name a version can only ever fail to resolve, and it fails inside a
1198
+ * durable step halfway through a load rather than on the canvas. The same
1199
+ * argument `if-threshold-invalid` makes one field along.
1200
+ */
1201
+ 'version-pin-invalid',
983
1202
  ];
984
1203
  /**
985
1204
  * Everything that makes a graph unrunnable, in one pure function.
@@ -1165,8 +1384,74 @@ function checkNodeWiring(nodes, incoming, outgoing, issues) {
1165
1384
  const unconfigured = nodeIsUnconfigured(node);
1166
1385
  if (unconfigured)
1167
1386
  issues.push(unconfigured);
1387
+ checkVersionPins(node, issues);
1388
+ }
1389
+ }
1390
+ /**
1391
+ * Every version pin on this node names a version that could exist.
1392
+ *
1393
+ * Both pins in one place, because they are one rule and splitting it is how one
1394
+ * of the two ends up accepting `0`. Checked here rather than only at the HTTP
1395
+ * boundary because `validateWorkflow` is what the canvas runs, so this is the
1396
+ * difference between a refusal on the screen where the number was typed and a
1397
+ * 400 after pressing Save.
1398
+ *
1399
+ * A pin has to be a whole number of at least one, matching what a version
1400
+ * actually is: {@link CatalogTransform.version} and
1401
+ * {@link CatalogReusableNode.version} both start at 1 and count up in ones.
1402
+ * `"3"` — which is what an unparsed form field sends — is refused rather than
1403
+ * coerced, for the reason `if-threshold-invalid` refuses it: a pin that no
1404
+ * stored version can equal resolves to nothing, and it does so inside a durable
1405
+ * step in the middle of a load.
1406
+ *
1407
+ * A `useVersion` with no `useId` is caught here too, as an invalid pin rather
1408
+ * than as its own code: it pins a reference that does not exist, so the number
1409
+ * can never be looked up.
1410
+ */
1411
+ function checkVersionPins(node, issues) {
1412
+ if (node.kind === 'transform') {
1413
+ const invalid = badPin(node.transformVersion);
1414
+ if (invalid) {
1415
+ issues.push({
1416
+ code: 'version-pin-invalid',
1417
+ nodeIds: [node.id],
1418
+ message: `Transform node "${node.name}" (${node.id}) is pinned to version ${invalid} of its code, and a version is a whole number of at least 1. Leave it unset for the node to follow the latest.`,
1419
+ });
1420
+ }
1421
+ return;
1422
+ }
1423
+ if (!nodeKindIsReusable(node.kind))
1424
+ return;
1425
+ // Narrowed off the union rather than read off `node` with a property check,
1426
+ // so a kind that becomes reusable without gaining the fields is a type error
1427
+ // here and not a check that silently passes.
1428
+ if (node.kind !== 'source' && node.kind !== 'sink')
1429
+ return;
1430
+ const invalid = badPin(node.useVersion);
1431
+ if (invalid) {
1432
+ issues.push({
1433
+ code: 'version-pin-invalid',
1434
+ nodeIds: [node.id],
1435
+ message: `Node "${node.name}" (${node.id}) is pinned to version ${invalid} of the reusable node it uses, and a version is a whole number of at least 1. Leave it unset for the node to follow the latest.`,
1436
+ });
1437
+ return;
1438
+ }
1439
+ if (node.useVersion !== undefined && node.useId === undefined) {
1440
+ issues.push({
1441
+ code: 'version-pin-invalid',
1442
+ nodeIds: [node.id],
1443
+ message: `Node "${node.name}" (${node.id}) is pinned to version ${node.useVersion} but names no reusable node, so there is nothing for that version to be a version of.`,
1444
+ });
1168
1445
  }
1169
1446
  }
1447
+ /** The offending value, rendered, or `undefined` when the pin is fine or absent. */
1448
+ function badPin(version) {
1449
+ if (version === undefined)
1450
+ return undefined;
1451
+ if (typeof version === 'number' && Number.isInteger(version) && version >= 1)
1452
+ return undefined;
1453
+ return JSON.stringify(version);
1454
+ }
1170
1455
  /**
1171
1456
  * Every wire out of an `if` names a branch, and no other wire does.
1172
1457
  *
@@ -1735,14 +2020,30 @@ function canonicalNode(node) {
1735
2020
  // Sorted keys, so a canvas that rewrites the object in a different order
1736
2021
  // does not look like an edit.
1737
2022
  sortedEntries(node.config),
2023
+ // Appended only when there is a reference, exactly as `edge.branch` above
2024
+ // is appended only when there is a label, and for the same reason: adding
2025
+ // reusable nodes to this file must not renumber the version of a single
2026
+ // graph that did not change. Every source drawn before they existed
2027
+ // hashes to the string it always did.
2028
+ ...canonicalReuse(node),
1738
2029
  ]);
1739
2030
  }
1740
2031
  if (node.kind === 'transform') {
1741
- // The transform's *version* is deliberately not in here. Editing a
1742
- // transform is already recorded as a new transform version, and folding it
1743
- // in would bump every graph that references it — which would say the wiring
1744
- // changed when it did not.
1745
- return JSON.stringify([node.id, node.kind, node.transformId]);
2032
+ // The transform's *version as stored* is deliberately not in here, and that
2033
+ // has not changed: editing a transform is recorded as a new transform
2034
+ // version, and folding it in would bump every graph that references it,
2035
+ // which would claim the wiring changed when it did not.
2036
+ //
2037
+ // What IS in here is the *pin* — which is the opposite choice for the
2038
+ // opposite reason, and the same one `call` makes one branch down. Moving a
2039
+ // node from v3 to v5, or off a pin onto the latest, changes what the load
2040
+ // runs as surely as rewiring it does, and is a decision somebody made in
2041
+ // this graph. Appended rather than always present, so an unpinned node —
2042
+ // which is every transform node in every deployment today — hashes to
2043
+ // exactly the string it always did.
2044
+ return node.transformVersion === undefined
2045
+ ? JSON.stringify([node.id, node.kind, node.transformId])
2046
+ : JSON.stringify([node.id, node.kind, node.transformId, node.transformVersion]);
1746
2047
  }
1747
2048
  if (node.kind === 'call') {
1748
2049
  // The called version IS in here, and that is the opposite choice from a
@@ -1778,10 +2079,35 @@ function canonicalNode(node) {
1778
2079
  ]);
1779
2080
  }
1780
2081
  if (node.kind === 'sink') {
1781
- return JSON.stringify([node.id, node.kind, node.targetType, node.mode ?? 'full']);
2082
+ return JSON.stringify([
2083
+ node.id,
2084
+ node.kind,
2085
+ node.targetType,
2086
+ node.mode ?? 'full',
2087
+ ...canonicalReuse(node),
2088
+ ]);
1782
2089
  }
1783
2090
  return unreachableNodeKind(node, 'workflowGraphHash');
1784
2091
  }
2092
+ /**
2093
+ * The reusable reference, as zero, one or two trailing hash components.
2094
+ *
2095
+ * Zero when there is no reference, which is what makes this additive: every
2096
+ * graph stored before reusable nodes existed produces the same canonical string
2097
+ * it always did, so no version is renumbered by a deployment picking up this
2098
+ * release. One when the reference follows the latest. Two when it is pinned.
2099
+ *
2100
+ * "Follows the latest" and "pinned to v1" hash differently, and they must:
2101
+ * moving a node off a pin is a real change to what it will run next month, and a
2102
+ * fingerprint that could not see it would let somebody unpin a shared sink with
2103
+ * no version bump and no diff — which is precisely the silence this feature was
2104
+ * built to end.
2105
+ */
2106
+ function canonicalReuse(node) {
2107
+ if (node.useId === undefined)
2108
+ return [];
2109
+ return node.useVersion === undefined ? [node.useId] : [node.useId, node.useVersion];
2110
+ }
1785
2111
  /**
1786
2112
  * The parts of a predicate that decide which branch runs.
1787
2113
  *
@@ -1872,13 +2198,13 @@ function fnv1a(input, offset) {
1872
2198
  function isWorkflowNode(value) {
1873
2199
  if (typeof value !== 'object' || value === null)
1874
2200
  return false;
1875
- const id = Reflect.get(value, 'id');
1876
- const name = Reflect.get(value, 'name');
1877
2201
  const kind = Reflect.get(value, 'kind');
1878
- if (typeof id !== 'string' || typeof name !== 'string')
1879
- return false;
1880
2202
  if (!isWorkflowNodeKind(kind))
1881
2203
  return false;
2204
+ // Everything every kind carries, checked once before the narrowing below
2205
+ // rather than inside the branches that could carry it. See {@link hasNodeBase}.
2206
+ if (!hasNodeBase(value))
2207
+ return false;
1882
2208
  if (kind === 'transform') {
1883
2209
  return typeof Reflect.get(value, 'transformId') === 'string';
1884
2210
  }
@@ -1934,6 +2260,49 @@ function isNarrowsList(value) {
1934
2260
  return true;
1935
2261
  return Array.isArray(value) && value.every((type) => typeof type === 'string');
1936
2262
  }
2263
+ /**
2264
+ * A version pin as it comes back out of a JSON column: absent, or a whole number
2265
+ * of at least one.
2266
+ *
2267
+ * The same rule {@link checkVersionPins} states, applied at the read boundary,
2268
+ * because the two answer different questions about the same field. The validator
2269
+ * tells an author their graph will not run; this decides whether a graph stored
2270
+ * by some other build can be read at all. Absent is accepted and always will be
2271
+ * — it is what every node written before pins existed carries, and it means
2272
+ * "follows the latest", which is exactly what those nodes have always done.
2273
+ */
2274
+ function isOptionalVersion(value) {
2275
+ if (value === undefined)
2276
+ return true;
2277
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1;
2278
+ }
2279
+ /**
2280
+ * Everything every node kind carries, whatever kind it is: its identity, and
2281
+ * the things it points at outside itself.
2282
+ *
2283
+ * Checked once, before {@link isWorkflowNode} narrows per kind, rather than in
2284
+ * the branches that could carry each field. The references are declared on two
2285
+ * members of the union and a node arriving as JSON does not respect that, so a
2286
+ * single check is both cheaper and stricter than two — and a kind that becomes
2287
+ * reusable later inherits it rather than having to remember it.
2288
+ *
2289
+ * A pin read back as `"3"` or as `0` names no stored version, and the resolution
2290
+ * happens inside a durable step in the middle of a load. So it is refused where
2291
+ * a graph is read out of a column rather than surviving as far as the run that
2292
+ * cannot honour it.
2293
+ */
2294
+ function hasNodeBase(value) {
2295
+ if (typeof Reflect.get(value, 'id') !== 'string')
2296
+ return false;
2297
+ if (typeof Reflect.get(value, 'name') !== 'string')
2298
+ return false;
2299
+ if (!isOptionalVersion(Reflect.get(value, 'transformVersion')))
2300
+ return false;
2301
+ if (!isOptionalVersion(Reflect.get(value, 'useVersion')))
2302
+ return false;
2303
+ const useId = Reflect.get(value, 'useId');
2304
+ return useId === undefined || typeof useId === 'string';
2305
+ }
1937
2306
  /**
1938
2307
  * The narrowing counterpart of {@link unreachableNodeKind}.
1939
2308
  *
@@ -1992,6 +2361,35 @@ function supportsWorkflows(store) {
1992
2361
  function supportsTransformRevisions(store) {
1993
2362
  return typeof store.listTransformRevisions === 'function';
1994
2363
  }
2364
+ /**
2365
+ * Whether this store can produce one particular version of a transform's code.
2366
+ *
2367
+ * Separate from {@link supportsTransformRevisions}, and not implied by it: one
2368
+ * answers "can a screen show the history", the other "can a run honour a pin".
2369
+ * A store could reasonably have the first and not the second, and folding them
2370
+ * together would let a graph be saved with a pin this deployment cannot resolve
2371
+ * — discovered mid-load rather than at the moment the pin was set.
2372
+ */
2373
+ function supportsTransformPins(store) {
2374
+ return typeof store.getTransformAt === 'function';
2375
+ }
2376
+ /**
2377
+ * Whether this store can hold reusable nodes.
2378
+ *
2379
+ * All six, and never a subset. A store with `getReusableNode` but no
2380
+ * `reusableNodeUses` could serve a picker and could not answer the question the
2381
+ * feature exists for — and the shape of that failure is a console offering to
2382
+ * share a node while being unable to say who already depends on it, which is
2383
+ * worse than not offering at all.
2384
+ */
2385
+ function supportsReusableNodes(store) {
2386
+ return (typeof store.listReusableNodes === 'function' &&
2387
+ typeof store.getReusableNode === 'function' &&
2388
+ typeof store.getReusableNodeAt === 'function' &&
2389
+ typeof store.saveReusableNode === 'function' &&
2390
+ typeof store.deleteReusableNode === 'function' &&
2391
+ typeof store.reusableNodeUses === 'function');
2392
+ }
1995
2393
  function supportsWorkflowStages(store) {
1996
2394
  return typeof store.writeStage === 'function' && typeof store.readStage === 'function';
1997
2395
  }
package/dist/client.d.ts CHANGED
@@ -153,8 +153,8 @@ export declare const catalogRoutes: {
153
153
  readonly traces: () => string;
154
154
  readonly trace: (id: string) => string;
155
155
  };
156
- export type { CatalogConnection, CatalogConnector, ConnectionCheck, CatalogTransform, CatalogWorkflow, CatalogWorkflowCapabilities, ConnectorKind, ConnectorRun, TransformLanguage, TransformResult, CallableWorkflowRef, WorkflowBranchLabel, WorkflowCallEnvelope, WorkflowCallNode, WorkflowCallOutput, WorkflowEdge, WorkflowExecutionMode, WorkflowFilterAll, WorkflowFilterAny, WorkflowFilterComparison, WorkflowFilterGroup, WorkflowFilterNode, WorkflowFilterOneOf, WorkflowFilterOperator, WorkflowFilterPredicate, WorkflowFilterPredicateKind, WorkflowFilterPresence, WorkflowFilterValue, WorkflowGraph, WorkflowEnvPredicate, WorkflowIfNode, WorkflowIfPredicate, WorkflowIssueCode, WorkflowNode, WorkflowNodeKind, WorkflowNodeOutcome, WorkflowPredicateKind, WorkflowRowCountPredicate, WorkflowRunOrderEntry, WorkflowSinkNode, WorkflowSkipReason, WorkflowSourceNode, WorkflowStageRef, WorkflowTransformNode, WorkflowValidationIssue, } from './catalog.pipeline';
157
- export { CONNECTOR_KINDS, isConnectorKind, isTransformLanguage, isWorkflowEdge, isWorkflowNode, REDACTED_SECRET, TRANSFORM_LANGUAGES, readWorkflowCallOutput, WORKFLOW_CALL_CONTRACT, } from './catalog.pipeline';
156
+ export type { CatalogConnection, CatalogConnector, ConnectionCheck, CatalogTransform, CatalogWorkflow, CatalogWorkflowCapabilities, ConnectorKind, ConnectorRun, TransformLanguage, TransformResult, CallableWorkflowRef, WorkflowBranchLabel, WorkflowCallEnvelope, WorkflowCallNode, WorkflowCallOutput, WorkflowEdge, WorkflowExecutionMode, WorkflowFilterAll, WorkflowFilterAny, WorkflowFilterComparison, WorkflowFilterGroup, WorkflowFilterNode, WorkflowFilterOneOf, WorkflowFilterOperator, WorkflowFilterPredicate, WorkflowFilterPredicateKind, WorkflowFilterPresence, WorkflowFilterValue, WorkflowGraph, WorkflowEnvPredicate, WorkflowIfNode, WorkflowIfPredicate, WorkflowIssueCode, WorkflowNode, WorkflowNodeKind, WorkflowNodeOutcome, WorkflowPredicateKind, WorkflowRowCountPredicate, WorkflowRunOrderEntry, WorkflowSinkNode, WorkflowSkipReason, WorkflowSourceNode, WorkflowStageRef, WorkflowTransformNode, WorkflowValidationIssue, CatalogReusableNode, CatalogReusableNodeUse, ReusableNodeBody, ReusableNodeKind, ReusableNodeRef, ReusableSinkBody, ReusableSourceBody, VersionPinCopy, } from './catalog.pipeline';
157
+ export { CONNECTOR_KINDS, isConnectorKind, isTransformLanguage, isWorkflowEdge, isWorkflowNode, REDACTED_SECRET, TRANSFORM_LANGUAGES, readWorkflowCallOutput, WORKFLOW_CALL_CONTRACT, applyReusableNode, reusableNodeBodyOf, isReusableNodeBody, isReusableNodeKind, REUSABLE_NODE_KINDS, NODE_KIND_IS_REUSABLE, nodeKindIsReusable, unreachableReusableNodeKind, describeVersionPin, } from './catalog.pipeline';
158
158
  /**
159
159
  * The workflow validator, shipped to the browser deliberately.
160
160
  *
package/dist/client.js CHANGED
@@ -11,8 +11,8 @@
11
11
  * types are.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.isWorkflowFilterValue = exports.isWorkflowFilterPredicateKind = exports.isWorkflowFilterPredicate = exports.isWorkflowFilterOperator = exports.isWorkflowBranchLabel = exports.WORKFLOW_SKIP_REASONS = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_MAX_VALUES = exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_BRANCH_LABELS = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.workflowRowY = exports.workflowColumnX = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_COLUMN_GAP = exports.WORKFLOW_CALL_CONTRACT = exports.readWorkflowCallOutput = exports.TRANSFORM_LANGUAGES = exports.REDACTED_SECRET = exports.isWorkflowNode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.catalogRoutes = exports.UnsafeIdentifierError = exports.physicalColumn = exports.outputAlias = exports.isSafeIdentifier = exports.VALUELESS_FILTER_OPERATORS = exports.resolveObjectFilters = exports.parseObjectFilter = exports.offeredFilterOperators = exports.isCatalogFilterOperator = exports.filterOperatorsFor = exports.filterOperatorTakesValue = exports.encodeObjectFilter = exports.coerceFilterValue = exports.CATALOG_FILTER_OPERATORS = exports.CATALOG_FILTER_LIMIT = exports.CATALOG_REVISION_LIMIT = void 0;
15
- exports.DELETE_RECONCILIATION_STRATEGIES = exports.callableWorkflowBlock = exports.isWorkflowStatus = exports.isWorkflowNodeKind = exports.isWorkflowExecutionMode = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_STATUSES = exports.unreachablePredicateKind = exports.unreachableNodeKind = exports.unreachableFilterPredicateKind = exports.unreachableFilterOperator = exports.workflowNodeRuns = exports.workflowNarrowedTypes = exports.workflowFilterMatches = exports.isWorkflowSkipReason = exports.isWorkflowPredicateKind = exports.isWorkflowIfPredicate = void 0;
14
+ exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_BRANCH_LABELS = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.workflowRowY = exports.workflowColumnX = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_COLUMN_GAP = exports.describeVersionPin = exports.unreachableReusableNodeKind = exports.nodeKindIsReusable = exports.NODE_KIND_IS_REUSABLE = exports.REUSABLE_NODE_KINDS = exports.isReusableNodeKind = exports.isReusableNodeBody = exports.reusableNodeBodyOf = exports.applyReusableNode = exports.WORKFLOW_CALL_CONTRACT = exports.readWorkflowCallOutput = exports.TRANSFORM_LANGUAGES = exports.REDACTED_SECRET = exports.isWorkflowNode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.catalogRoutes = exports.UnsafeIdentifierError = exports.physicalColumn = exports.outputAlias = exports.isSafeIdentifier = exports.VALUELESS_FILTER_OPERATORS = exports.resolveObjectFilters = exports.parseObjectFilter = exports.offeredFilterOperators = exports.isCatalogFilterOperator = exports.filterOperatorsFor = exports.filterOperatorTakesValue = exports.encodeObjectFilter = exports.coerceFilterValue = exports.CATALOG_FILTER_OPERATORS = exports.CATALOG_FILTER_LIMIT = exports.CATALOG_REVISION_LIMIT = void 0;
15
+ exports.DELETE_RECONCILIATION_STRATEGIES = exports.callableWorkflowBlock = exports.isWorkflowStatus = exports.isWorkflowNodeKind = exports.isWorkflowExecutionMode = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_STATUSES = exports.unreachablePredicateKind = exports.unreachableNodeKind = exports.unreachableFilterPredicateKind = exports.unreachableFilterOperator = exports.workflowNodeRuns = exports.workflowNarrowedTypes = exports.workflowFilterMatches = exports.isWorkflowSkipReason = exports.isWorkflowPredicateKind = exports.isWorkflowIfPredicate = exports.isWorkflowFilterValue = exports.isWorkflowFilterPredicateKind = exports.isWorkflowFilterPredicate = exports.isWorkflowFilterOperator = exports.isWorkflowBranchLabel = exports.WORKFLOW_SKIP_REASONS = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_MAX_VALUES = void 0;
16
16
  exports.isDeleteReconciliationStrategy = isDeleteReconciliationStrategy;
17
17
  exports.pipelineExpectationRoutes = pipelineExpectationRoutes;
18
18
  // A value, not a type: a screen saying how far back the history goes should read
@@ -151,6 +151,28 @@ Object.defineProperty(exports, "TRANSFORM_LANGUAGES", { enumerable: true, get: f
151
151
  // runner does, so the two cannot disagree about what "no rows" looks like.
152
152
  Object.defineProperty(exports, "readWorkflowCallOutput", { enumerable: true, get: function () { return catalog_pipeline_1.readWorkflowCallOutput; } });
153
153
  Object.defineProperty(exports, "WORKFLOW_CALL_CONTRACT", { enumerable: true, get: function () { return catalog_pipeline_1.WORKFLOW_CALL_CONTRACT; } });
154
+ // Reusable nodes, and the two directions a screen needs them in: the fold onto
155
+ // a node, so a picker can show what choosing one will do without a round trip,
156
+ // and the lift off a node, so "save this as reusable" sends the same body the
157
+ // server would have derived. One implementation of each rather than a copy per
158
+ // screen, for the reason `validateWorkflow` is shipped here at all — the
159
+ // alternative is a console that shows one thing and a server that stores
160
+ // another.
161
+ Object.defineProperty(exports, "applyReusableNode", { enumerable: true, get: function () { return catalog_pipeline_1.applyReusableNode; } });
162
+ Object.defineProperty(exports, "reusableNodeBodyOf", { enumerable: true, get: function () { return catalog_pipeline_1.reusableNodeBodyOf; } });
163
+ Object.defineProperty(exports, "isReusableNodeBody", { enumerable: true, get: function () { return catalog_pipeline_1.isReusableNodeBody; } });
164
+ Object.defineProperty(exports, "isReusableNodeKind", { enumerable: true, get: function () { return catalog_pipeline_1.isReusableNodeKind; } });
165
+ Object.defineProperty(exports, "REUSABLE_NODE_KINDS", { enumerable: true, get: function () { return catalog_pipeline_1.REUSABLE_NODE_KINDS; } });
166
+ // Which kinds may be offered a "save as reusable" affordance at all. Read
167
+ // rather than restated, because a hand-written list on a screen is exactly how
168
+ // the `filter` node shipped with no way to create it.
169
+ Object.defineProperty(exports, "NODE_KIND_IS_REUSABLE", { enumerable: true, get: function () { return catalog_pipeline_1.NODE_KIND_IS_REUSABLE; } });
170
+ Object.defineProperty(exports, "nodeKindIsReusable", { enumerable: true, get: function () { return catalog_pipeline_1.nodeKindIsReusable; } });
171
+ Object.defineProperty(exports, "unreachableReusableNodeKind", { enumerable: true, get: function () { return catalog_pipeline_1.unreachableReusableNodeKind; } });
172
+ // One wording for "pinned" and "follows the latest", so an inspector cannot
173
+ // describe following as though it were pinning — which is the whole
174
+ // misunderstanding the pin exists to end.
175
+ Object.defineProperty(exports, "describeVersionPin", { enumerable: true, get: function () { return catalog_pipeline_1.describeVersionPin; } });
154
176
  /**
155
177
  * The workflow validator, shipped to the browser deliberately.
156
178
  *
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { type CatalogOverlayStore, FileCatalogOverlayStore, InMemoryCatalogOverl
8
8
  export { CATALOG_OVERLAY_STORE } from './catalog.overlay-store.token';
9
9
  export { MikroOrmCatalogRegistry } from './catalog.registry';
10
10
  export { CatalogRegistry } from './catalog.registry.base';
11
- export { CATALOG_PIPELINE_STORE, CODE_CONTEXT_CONTRACT, type CatalogCodeContext, type CatalogConnection, type CatalogConnector, type ConnectionCheck, CONNECTOR_KINDS, type CatalogLoadExpectations, type CatalogLoadExpectationStore, type CatalogPipelineStore, type CatalogStageStore, type CatalogTransform, type CatalogWorkflow, type CatalogWorkflowCapabilities, type CatalogWorkflowStore, type CallableWorkflowBlock, type CallableWorkflowDisagreement, type CallableWorkflowRef, callableWorkflowBlock, type ConnectorKind, type ConnectorRun, type DeleteReconciliation, isConnectorKind, isPipelineStore, isTransformLanguage, type LoadExpectation, type RowCountBound, type StoredLoadExpectation, readWorkflowCallOutput, REDACTED_SECRET, supportsLoadExpectations, supportsTransformRevisions, isWorkflowBranchLabel, isWorkflowEdge, isWorkflowExecutionMode, isWorkflowFilterOperator, isWorkflowFilterPredicate, isWorkflowFilterPredicateKind, isWorkflowFilterValue, isWorkflowIfPredicate, isWorkflowNode, isWorkflowNodeKind, isWorkflowPredicateKind, isWorkflowSkipReason, isWorkflowStatus, supportsWorkflows, supportsWorkflowStages, TRANSFORM_RUNNER, TRANSFORM_LANGUAGES, type TransformLanguage, type TransformResult, type TransformRunner, unreachableFilterOperator, unreachableFilterPredicateKind, unreachableNodeKind, unreachablePredicateKind, validateWorkflow, WORKFLOW_BRANCH_LABELS, WORKFLOW_CALL_CONTRACT, WORKFLOW_COLUMN_GAP, WORKFLOW_EXECUTION_MODES, WORKFLOW_FILTER_COLUMN_PATTERN, WORKFLOW_FILTER_MAX_DEPTH, WORKFLOW_FILTER_MAX_VALUES, WORKFLOW_FILTER_OPERATORS, WORKFLOW_FILTER_PREDICATE_KINDS, WORKFLOW_ISSUE_CODES, WORKFLOW_NODE_HEIGHT, WORKFLOW_NODE_ID_PATTERN, WORKFLOW_NODE_KINDS, WORKFLOW_NODE_WIDTH, WORKFLOW_PREDICATE_KINDS, WORKFLOW_ROW_GAP, WORKFLOW_SKIP_REASONS, WORKFLOW_STATUSES, type WorkflowBranchLabel, workflowColumnX, workflowRowY, type WorkflowCallEnvelope, type WorkflowCallNode, type WorkflowCallOutput, type WorkflowEdge, type WorkflowExecutionMode, type WorkflowFilterAll, type WorkflowFilterAny, type WorkflowFilterComparison, type WorkflowFilterGroup, workflowFilterMatches, type WorkflowFilterNode, type WorkflowFilterOneOf, type WorkflowFilterOperator, type WorkflowFilterPredicate, type WorkflowFilterPredicateKind, type WorkflowFilterPresence, type WorkflowFilterValue, type WorkflowGraph, workflowGraphHash, type WorkflowEnvPredicate, type WorkflowIfNode, type WorkflowIfPredicate, type WorkflowIssueCode, type WorkflowNode, type WorkflowNodeKind, workflowNarrowedTypes, type WorkflowNodeOutcome, workflowNodeRuns, type WorkflowNodeStepInput, type WorkflowNodeStepOutput, workflowRunOrder, type WorkflowRunOrderEntry, type WorkflowPredicateKind, type WorkflowRowCountPredicate, type WorkflowSinkNode, type WorkflowSkipReason, type WorkflowSourceNode, type WorkflowStageRef, type WorkflowStatus, type WorkflowTransformNode, type WorkflowValidationIssue, } from './catalog.pipeline';
11
+ export { CATALOG_PIPELINE_STORE, CODE_CONTEXT_CONTRACT, type CatalogCodeContext, type CatalogConnection, type CatalogConnector, type ConnectionCheck, CONNECTOR_KINDS, type CatalogLoadExpectations, type CatalogLoadExpectationStore, type CatalogPipelineStore, type CatalogStageStore, type CatalogTransform, type CatalogWorkflow, type CatalogWorkflowCapabilities, type CatalogWorkflowStore, type CallableWorkflowBlock, type CallableWorkflowDisagreement, type CallableWorkflowRef, callableWorkflowBlock, type ConnectorKind, type ConnectorRun, type DeleteReconciliation, isConnectorKind, isPipelineStore, isTransformLanguage, type LoadExpectation, type RowCountBound, type StoredLoadExpectation, readWorkflowCallOutput, REDACTED_SECRET, supportsLoadExpectations, supportsReusableNodes, supportsTransformPins, supportsTransformRevisions, applyReusableNode, type CatalogReusableNode, type CatalogReusableNodeStore, type CatalogReusableNodeUse, describeVersionPin, isReusableNodeBody, isReusableNodeKind, NODE_KIND_IS_REUSABLE, nodeKindIsReusable, REUSABLE_NODE_KINDS, type ReusableNodeBody, type ReusableNodeKind, type ReusableNodeRef, type ReusableSinkBody, type ReusableSourceBody, reusableNodeBodyOf, unreachableReusableNodeKind, type VersionPinCopy, isWorkflowBranchLabel, isWorkflowEdge, isWorkflowExecutionMode, isWorkflowFilterOperator, isWorkflowFilterPredicate, isWorkflowFilterPredicateKind, isWorkflowFilterValue, isWorkflowIfPredicate, isWorkflowNode, isWorkflowNodeKind, isWorkflowPredicateKind, isWorkflowSkipReason, isWorkflowStatus, supportsWorkflows, supportsWorkflowStages, TRANSFORM_RUNNER, TRANSFORM_LANGUAGES, type TransformLanguage, type TransformResult, type TransformRunner, unreachableFilterOperator, unreachableFilterPredicateKind, unreachableNodeKind, unreachablePredicateKind, validateWorkflow, WORKFLOW_BRANCH_LABELS, WORKFLOW_CALL_CONTRACT, WORKFLOW_COLUMN_GAP, WORKFLOW_EXECUTION_MODES, WORKFLOW_FILTER_COLUMN_PATTERN, WORKFLOW_FILTER_MAX_DEPTH, WORKFLOW_FILTER_MAX_VALUES, WORKFLOW_FILTER_OPERATORS, WORKFLOW_FILTER_PREDICATE_KINDS, WORKFLOW_ISSUE_CODES, WORKFLOW_NODE_HEIGHT, WORKFLOW_NODE_ID_PATTERN, WORKFLOW_NODE_KINDS, WORKFLOW_NODE_WIDTH, WORKFLOW_PREDICATE_KINDS, WORKFLOW_ROW_GAP, WORKFLOW_SKIP_REASONS, WORKFLOW_STATUSES, type WorkflowBranchLabel, workflowColumnX, workflowRowY, type WorkflowCallEnvelope, type WorkflowCallNode, type WorkflowCallOutput, type WorkflowEdge, type WorkflowExecutionMode, type WorkflowFilterAll, type WorkflowFilterAny, type WorkflowFilterComparison, type WorkflowFilterGroup, workflowFilterMatches, type WorkflowFilterNode, type WorkflowFilterOneOf, type WorkflowFilterOperator, type WorkflowFilterPredicate, type WorkflowFilterPredicateKind, type WorkflowFilterPresence, type WorkflowFilterValue, type WorkflowGraph, workflowGraphHash, type WorkflowEnvPredicate, type WorkflowIfNode, type WorkflowIfPredicate, type WorkflowIssueCode, type WorkflowNode, type WorkflowNodeKind, workflowNarrowedTypes, type WorkflowNodeOutcome, workflowNodeRuns, type WorkflowNodeStepInput, type WorkflowNodeStepOutput, workflowRunOrder, type WorkflowRunOrderEntry, type WorkflowPredicateKind, type WorkflowRowCountPredicate, type WorkflowSinkNode, type WorkflowSkipReason, type WorkflowSourceNode, type WorkflowStageRef, type WorkflowStatus, type WorkflowTransformNode, type WorkflowValidationIssue, } from './catalog.pipeline';
12
12
  export { type ColumnarStageBatch, STAGE_ENCODING, STAGE_ENCODING_VERSION, type StagePayload, classifyStagePayload, decodeStageRows, encodeStageRows, isColumnarStageBatch, } from './catalog.stage-encoding';
13
13
  export * from './catalog.environment';
14
14
  export { QueryCache } from './catalog.query-cache';
package/dist/index.js CHANGED
@@ -14,9 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.unreachableFilterOperator = exports.TRANSFORM_LANGUAGES = exports.TRANSFORM_RUNNER = exports.supportsWorkflowStages = exports.supportsWorkflows = exports.isWorkflowStatus = exports.isWorkflowSkipReason = exports.isWorkflowPredicateKind = exports.isWorkflowNodeKind = exports.isWorkflowNode = exports.isWorkflowIfPredicate = exports.isWorkflowFilterValue = exports.isWorkflowFilterPredicateKind = exports.isWorkflowFilterPredicate = exports.isWorkflowFilterOperator = exports.isWorkflowExecutionMode = exports.isWorkflowEdge = exports.isWorkflowBranchLabel = exports.supportsTransformRevisions = exports.supportsLoadExpectations = exports.REDACTED_SECRET = exports.readWorkflowCallOutput = exports.isTransformLanguage = exports.isPipelineStore = exports.isConnectorKind = exports.callableWorkflowBlock = exports.CONNECTOR_KINDS = exports.CODE_CONTEXT_CONTRACT = exports.CATALOG_PIPELINE_STORE = exports.CatalogRegistry = exports.MikroOrmCatalogRegistry = exports.CATALOG_OVERLAY_STORE = exports.InMemoryCatalogOverlayStore = exports.FileCatalogOverlayStore = exports.CATALOG_OPTIONS = exports.isStreamingQueryStore = exports.isQueryStore = exports.assertReadOnlyShape = exports.CatalogModule = exports.emitCatalog = exports.curationActor = exports.channelNameFor = exports.catalogEventPhase = exports.UNATTRIBUTED_PRINCIPAL_ID = exports.CATALOG_LIB = exports.CATALOG_EVENTS = exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CatalogType = exports.CatalogProperty = void 0;
18
- exports.CATALOG_REVISION_LIMIT = exports.visibleToPrincipal = exports.searchCatalog = exports.maySearch = exports.emptySearch = exports.bestMatch = exports.MAX_SEARCH_LIMIT = exports.DEFAULT_SEARCH_LIMIT = exports.CatalogService = exports.SubprocessTransformRunner = exports.toCsv = exports.guardFormula = exports.csvLines = exports.csvCell = exports.QueryCache = exports.isColumnarStageBatch = exports.encodeStageRows = exports.decodeStageRows = exports.classifyStagePayload = exports.STAGE_ENCODING_VERSION = exports.STAGE_ENCODING = exports.workflowRunOrder = exports.workflowNodeRuns = exports.workflowNarrowedTypes = exports.workflowGraphHash = exports.workflowFilterMatches = exports.workflowRowY = exports.workflowColumnX = exports.WORKFLOW_STATUSES = exports.WORKFLOW_SKIP_REASONS = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_MAX_VALUES = exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_COLUMN_GAP = exports.WORKFLOW_CALL_CONTRACT = exports.WORKFLOW_BRANCH_LABELS = exports.validateWorkflow = exports.unreachablePredicateKind = exports.unreachableNodeKind = exports.unreachableFilterPredicateKind = void 0;
19
- exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.UnsafeIdentifierError = exports.supportsCarryForward = exports.physicalColumn = exports.outputAlias = exports.isWriteStore = exports.isSafeIdentifier = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.supportsObjectFilters = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertSafeIdentifier = exports.assertNoColumnCollisions = exports.StaticKeyPrincipalResolver = exports.readableObjectPage = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = exports.CATALOG_PRINCIPAL_RESOLVER = exports.traceOutcomeFilter = exports.supportsSavedQueryRevisions = exports.isWorkspaceStore = exports.isTraceStore = exports.isCatalogTraceOutcome = exports.embeddedVisualization = exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = exports.CATALOG_TRACE_OUTCOMES = void 0;
17
+ exports.isWorkflowFilterValue = exports.isWorkflowFilterPredicateKind = exports.isWorkflowFilterPredicate = exports.isWorkflowFilterOperator = exports.isWorkflowExecutionMode = exports.isWorkflowEdge = exports.isWorkflowBranchLabel = exports.unreachableReusableNodeKind = exports.reusableNodeBodyOf = exports.REUSABLE_NODE_KINDS = exports.nodeKindIsReusable = exports.NODE_KIND_IS_REUSABLE = exports.isReusableNodeKind = exports.isReusableNodeBody = exports.describeVersionPin = exports.applyReusableNode = exports.supportsTransformRevisions = exports.supportsTransformPins = exports.supportsReusableNodes = exports.supportsLoadExpectations = exports.REDACTED_SECRET = exports.readWorkflowCallOutput = exports.isTransformLanguage = exports.isPipelineStore = exports.isConnectorKind = exports.callableWorkflowBlock = exports.CONNECTOR_KINDS = exports.CODE_CONTEXT_CONTRACT = exports.CATALOG_PIPELINE_STORE = exports.CatalogRegistry = exports.MikroOrmCatalogRegistry = exports.CATALOG_OVERLAY_STORE = exports.InMemoryCatalogOverlayStore = exports.FileCatalogOverlayStore = exports.CATALOG_OPTIONS = exports.isStreamingQueryStore = exports.isQueryStore = exports.assertReadOnlyShape = exports.CatalogModule = exports.emitCatalog = exports.curationActor = exports.channelNameFor = exports.catalogEventPhase = exports.UNATTRIBUTED_PRINCIPAL_ID = exports.CATALOG_LIB = exports.CATALOG_EVENTS = exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CatalogType = exports.CatalogProperty = void 0;
18
+ exports.guardFormula = exports.csvLines = exports.csvCell = exports.QueryCache = exports.isColumnarStageBatch = exports.encodeStageRows = exports.decodeStageRows = exports.classifyStagePayload = exports.STAGE_ENCODING_VERSION = exports.STAGE_ENCODING = exports.workflowRunOrder = exports.workflowNodeRuns = exports.workflowNarrowedTypes = exports.workflowGraphHash = exports.workflowFilterMatches = exports.workflowRowY = exports.workflowColumnX = exports.WORKFLOW_STATUSES = exports.WORKFLOW_SKIP_REASONS = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_MAX_VALUES = exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_COLUMN_GAP = exports.WORKFLOW_CALL_CONTRACT = exports.WORKFLOW_BRANCH_LABELS = exports.validateWorkflow = exports.unreachablePredicateKind = exports.unreachableNodeKind = exports.unreachableFilterPredicateKind = exports.unreachableFilterOperator = exports.TRANSFORM_LANGUAGES = exports.TRANSFORM_RUNNER = exports.supportsWorkflowStages = exports.supportsWorkflows = exports.isWorkflowStatus = exports.isWorkflowSkipReason = exports.isWorkflowPredicateKind = exports.isWorkflowNodeKind = exports.isWorkflowNode = exports.isWorkflowIfPredicate = void 0;
19
+ exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.UnsafeIdentifierError = exports.supportsCarryForward = exports.physicalColumn = exports.outputAlias = exports.isWriteStore = exports.isSafeIdentifier = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.supportsObjectFilters = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertSafeIdentifier = exports.assertNoColumnCollisions = exports.StaticKeyPrincipalResolver = exports.readableObjectPage = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = exports.CATALOG_PRINCIPAL_RESOLVER = exports.traceOutcomeFilter = exports.supportsSavedQueryRevisions = exports.isWorkspaceStore = exports.isTraceStore = exports.isCatalogTraceOutcome = exports.embeddedVisualization = exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = exports.CATALOG_TRACE_OUTCOMES = exports.CATALOG_REVISION_LIMIT = exports.visibleToPrincipal = exports.searchCatalog = exports.maySearch = exports.emptySearch = exports.bestMatch = exports.MAX_SEARCH_LIMIT = exports.DEFAULT_SEARCH_LIMIT = exports.CatalogService = exports.SubprocessTransformRunner = exports.toCsv = void 0;
20
+ exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = void 0;
20
21
  var catalog_decorators_1 = require("./catalog.decorators");
21
22
  Object.defineProperty(exports, "CatalogProperty", { enumerable: true, get: function () { return catalog_decorators_1.CatalogProperty; } });
22
23
  Object.defineProperty(exports, "CatalogType", { enumerable: true, get: function () { return catalog_decorators_1.CatalogType; } });
@@ -66,7 +67,18 @@ Object.defineProperty(exports, "isTransformLanguage", { enumerable: true, get: f
66
67
  Object.defineProperty(exports, "readWorkflowCallOutput", { enumerable: true, get: function () { return catalog_pipeline_1.readWorkflowCallOutput; } });
67
68
  Object.defineProperty(exports, "REDACTED_SECRET", { enumerable: true, get: function () { return catalog_pipeline_1.REDACTED_SECRET; } });
68
69
  Object.defineProperty(exports, "supportsLoadExpectations", { enumerable: true, get: function () { return catalog_pipeline_1.supportsLoadExpectations; } });
70
+ Object.defineProperty(exports, "supportsReusableNodes", { enumerable: true, get: function () { return catalog_pipeline_1.supportsReusableNodes; } });
71
+ Object.defineProperty(exports, "supportsTransformPins", { enumerable: true, get: function () { return catalog_pipeline_1.supportsTransformPins; } });
69
72
  Object.defineProperty(exports, "supportsTransformRevisions", { enumerable: true, get: function () { return catalog_pipeline_1.supportsTransformRevisions; } });
73
+ Object.defineProperty(exports, "applyReusableNode", { enumerable: true, get: function () { return catalog_pipeline_1.applyReusableNode; } });
74
+ Object.defineProperty(exports, "describeVersionPin", { enumerable: true, get: function () { return catalog_pipeline_1.describeVersionPin; } });
75
+ Object.defineProperty(exports, "isReusableNodeBody", { enumerable: true, get: function () { return catalog_pipeline_1.isReusableNodeBody; } });
76
+ Object.defineProperty(exports, "isReusableNodeKind", { enumerable: true, get: function () { return catalog_pipeline_1.isReusableNodeKind; } });
77
+ Object.defineProperty(exports, "NODE_KIND_IS_REUSABLE", { enumerable: true, get: function () { return catalog_pipeline_1.NODE_KIND_IS_REUSABLE; } });
78
+ Object.defineProperty(exports, "nodeKindIsReusable", { enumerable: true, get: function () { return catalog_pipeline_1.nodeKindIsReusable; } });
79
+ Object.defineProperty(exports, "REUSABLE_NODE_KINDS", { enumerable: true, get: function () { return catalog_pipeline_1.REUSABLE_NODE_KINDS; } });
80
+ Object.defineProperty(exports, "reusableNodeBodyOf", { enumerable: true, get: function () { return catalog_pipeline_1.reusableNodeBodyOf; } });
81
+ Object.defineProperty(exports, "unreachableReusableNodeKind", { enumerable: true, get: function () { return catalog_pipeline_1.unreachableReusableNodeKind; } });
70
82
  Object.defineProperty(exports, "isWorkflowBranchLabel", { enumerable: true, get: function () { return catalog_pipeline_1.isWorkflowBranchLabel; } });
71
83
  Object.defineProperty(exports, "isWorkflowEdge", { enumerable: true, get: function () { return catalog_pipeline_1.isWorkflowEdge; } });
72
84
  Object.defineProperty(exports, "isWorkflowExecutionMode", { enumerable: true, get: function () { return catalog_pipeline_1.isWorkflowExecutionMode; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-catalog",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",