@dudousxd/nestjs-catalog 0.1.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.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/catalog.controller.d.ts +8 -0
- package/dist/catalog.controller.js +482 -0
- package/dist/catalog.decorators.d.ts +37 -0
- package/dist/catalog.decorators.js +50 -0
- package/dist/catalog.environment.d.ts +442 -0
- package/dist/catalog.environment.js +645 -0
- package/dist/catalog.events.d.ts +179 -0
- package/dist/catalog.events.js +110 -0
- package/dist/catalog.module.d.ts +5 -0
- package/dist/catalog.module.js +71 -0
- package/dist/catalog.options.d.ts +79 -0
- package/dist/catalog.options.js +4 -0
- package/dist/catalog.overlay-store.d.ts +25 -0
- package/dist/catalog.overlay-store.js +44 -0
- package/dist/catalog.overlay-store.token.d.ts +1 -0
- package/dist/catalog.overlay-store.token.js +4 -0
- package/dist/catalog.pipeline.d.ts +800 -0
- package/dist/catalog.pipeline.js +606 -0
- package/dist/catalog.principal.d.ts +209 -0
- package/dist/catalog.principal.js +245 -0
- package/dist/catalog.query-cache.d.ts +25 -0
- package/dist/catalog.query-cache.js +0 -0
- package/dist/catalog.query.d.ts +76 -0
- package/dist/catalog.query.js +64 -0
- package/dist/catalog.registry.base.d.ts +21 -0
- package/dist/catalog.registry.base.js +17 -0
- package/dist/catalog.registry.d.ts +44 -0
- package/dist/catalog.registry.js +359 -0
- package/dist/catalog.service.d.ts +115 -0
- package/dist/catalog.service.js +366 -0
- package/dist/catalog.store.d.ts +419 -0
- package/dist/catalog.store.js +175 -0
- package/dist/catalog.types.d.ts +165 -0
- package/dist/catalog.types.js +19 -0
- package/dist/catalog.workspace.d.ts +426 -0
- package/dist/catalog.workspace.js +87 -0
- package/dist/client.d.ts +86 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +109 -0
- package/dist/stores/mikro-orm-read.store.d.ts +20 -0
- package/dist/stores/mikro-orm-read.store.js +120 -0
- package/dist/transform-runner.d.ts +54 -0
- package/dist/transform-runner.js +280 -0
- package/package.json +54 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CATALOG_PROPERTY_META = exports.CATALOG_TYPE_META = void 0;
|
|
4
|
+
exports.CatalogType = CatalogType;
|
|
5
|
+
exports.CatalogProperty = CatalogProperty;
|
|
6
|
+
exports.readTypeOptions = readTypeOptions;
|
|
7
|
+
exports.readPropertyOptions = readPropertyOptions;
|
|
8
|
+
require("reflect-metadata");
|
|
9
|
+
exports.CATALOG_TYPE_META = Symbol('catalog:type');
|
|
10
|
+
exports.CATALOG_PROPERTY_META = Symbol('catalog:property');
|
|
11
|
+
/**
|
|
12
|
+
* Declares the semantics the database cannot know: what this entity is called
|
|
13
|
+
* in the business, which section it belongs to, and what to show when it
|
|
14
|
+
* appears as a link somewhere else.
|
|
15
|
+
*
|
|
16
|
+
* Structure is never declared here — it is read off the ORM. An entity with no
|
|
17
|
+
* `@CatalogType` still appears in the catalog, just with a name derived from
|
|
18
|
+
* its class and no group.
|
|
19
|
+
*/
|
|
20
|
+
function CatalogType(options = {}) {
|
|
21
|
+
return (target) => {
|
|
22
|
+
const existing = Reflect.getMetadata(exports.CATALOG_TYPE_META, target) ?? {};
|
|
23
|
+
Reflect.defineMetadata(exports.CATALOG_TYPE_META, { ...existing, ...options }, target);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Enriches one property. Everything it sets is tier 0 — the overlay can
|
|
28
|
+
* override any of it at runtime without a migration, which is precisely why
|
|
29
|
+
* these live in metadata rather than in the column definition.
|
|
30
|
+
*/
|
|
31
|
+
function CatalogProperty(options = {}) {
|
|
32
|
+
return (target, propertyKey) => {
|
|
33
|
+
const ctor = target.constructor;
|
|
34
|
+
const existing = Reflect.getMetadata(exports.CATALOG_PROPERTY_META, ctor) ?? {};
|
|
35
|
+
Reflect.defineMetadata(exports.CATALOG_PROPERTY_META, {
|
|
36
|
+
...existing,
|
|
37
|
+
[String(propertyKey)]: { ...existing[String(propertyKey)], ...options },
|
|
38
|
+
}, ctor);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function readTypeOptions(target) {
|
|
42
|
+
if (typeof target !== 'function')
|
|
43
|
+
return {};
|
|
44
|
+
return Reflect.getMetadata(exports.CATALOG_TYPE_META, target) ?? {};
|
|
45
|
+
}
|
|
46
|
+
function readPropertyOptions(target) {
|
|
47
|
+
if (typeof target !== 'function')
|
|
48
|
+
return {};
|
|
49
|
+
return Reflect.getMetadata(exports.CATALOG_PROPERTY_META, target) ?? {};
|
|
50
|
+
}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which copy of the world a caller is looking at.
|
|
3
|
+
*
|
|
4
|
+
* A catalog that only ever has one copy of everything is a catalog where the
|
|
5
|
+
* first time a transform runs against real data is the time it runs against the
|
|
6
|
+
* data people depend on. Environments fix that, and the whole design turns on
|
|
7
|
+
* one distinction that is easy to blur and expensive to get wrong:
|
|
8
|
+
*
|
|
9
|
+
* - **Data never moves between environments.** Not by a button, not by an
|
|
10
|
+
* export, not by an accident of a missing `WHERE` clause. Production's rows
|
|
11
|
+
* are production's, and dev's rows are whatever dev loaded for itself.
|
|
12
|
+
* - **Configuration is promoted between them.** The model (object types and the
|
|
13
|
+
* labels somebody curated onto them) and the pipeline (connectors, transforms)
|
|
14
|
+
* are things you write once, try in dev, and then *release* to production.
|
|
15
|
+
*
|
|
16
|
+
* Isolation is therefore physical — one database per environment, so a
|
|
17
|
+
* statement issued on the dev connection cannot name a production table — and
|
|
18
|
+
* promotion is a deliberate, previewable release rather than a copy. See
|
|
19
|
+
* {@link planPromotion} for exactly what crosses and what is refused.
|
|
20
|
+
*
|
|
21
|
+
* This is not the same axis as a durable *tenant*. A tenant answers "who
|
|
22
|
+
* executes this work"; an environment answers "which copy of the world is it
|
|
23
|
+
* executing against". They are related only in that each environment must get
|
|
24
|
+
* its own durable keyspace ({@link durableKeyspaceFor}), for the reason spelled
|
|
25
|
+
* out there.
|
|
26
|
+
*/
|
|
27
|
+
import type { ConnectorKind, TransformLanguage, WorkflowEdge, WorkflowNode } from './catalog.pipeline';
|
|
28
|
+
/**
|
|
29
|
+
* Where a request declares its environment.
|
|
30
|
+
*
|
|
31
|
+
* A header rather than a subdomain or a port, because the whole point is that
|
|
32
|
+
* one deployment can serve several environments and the choice has to be
|
|
33
|
+
* visible in the request itself — a proxy rule or a DNS entry is a place the
|
|
34
|
+
* answer can be wrong without anybody reading it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const CATALOG_ENVIRONMENT_HEADER = "x-catalog-environment";
|
|
37
|
+
/**
|
|
38
|
+
* The query-parameter spelling, for a browser that cannot set a header — a
|
|
39
|
+
* `<img>` pointing at an embedded chart, a link somebody pastes into a ticket.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately *lower* precedence than the header (see
|
|
42
|
+
* {@link environmentIdFromRequest}). A query string travels in referrers, logs
|
|
43
|
+
* and bookmarks; a header does not, so when both are present the one that was
|
|
44
|
+
* set deliberately by the client wins.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CATALOG_ENVIRONMENT_QUERY = "environment";
|
|
47
|
+
/** An environment's name. Short, lowercase, and safe as a SQL identifier. */
|
|
48
|
+
export type CatalogEnvironmentId = string;
|
|
49
|
+
export declare function isEnvironmentId(value: unknown): value is CatalogEnvironmentId;
|
|
50
|
+
/**
|
|
51
|
+
* Narrows an id or throws, with the rule in the message.
|
|
52
|
+
*
|
|
53
|
+
* Throwing rather than sanitising: an id that had to be repaired is an id that
|
|
54
|
+
* no longer matches whatever the operator wrote in their configuration, and the
|
|
55
|
+
* two would disagree about which database the environment lives in.
|
|
56
|
+
*/
|
|
57
|
+
export declare function assertEnvironmentId(value: unknown): CatalogEnvironmentId;
|
|
58
|
+
/**
|
|
59
|
+
* One copy of the world.
|
|
60
|
+
*
|
|
61
|
+
* Everything that distinguishes an environment from its siblings is a physical
|
|
62
|
+
* address of some kind, which is the point: there is no field here that a
|
|
63
|
+
* `WHERE` clause could be built out of, because a filter is precisely the sort
|
|
64
|
+
* of isolation that fails silently the one time somebody forgets it.
|
|
65
|
+
*/
|
|
66
|
+
export interface CatalogEnvironment {
|
|
67
|
+
id: CatalogEnvironmentId;
|
|
68
|
+
displayName: string;
|
|
69
|
+
/**
|
|
70
|
+
* The physical database this environment's tables live in.
|
|
71
|
+
*
|
|
72
|
+
* Not a schema *within* a shared database, and in MySQL those are the same
|
|
73
|
+
* word anyway — `CREATE SCHEMA` is an alias for `CREATE DATABASE`. What
|
|
74
|
+
* matters is that the store's table names are fixed (`catalog_object_type`,
|
|
75
|
+
* `obj_<type>`) and carry no environment in them, so two environments in one
|
|
76
|
+
* database would collide on every table. Separate databases make the
|
|
77
|
+
* collision impossible and make MySQL's own `GRANT` the enforcement point.
|
|
78
|
+
*/
|
|
79
|
+
databaseName: string;
|
|
80
|
+
/**
|
|
81
|
+
* The MikroORM context name this environment's connection is registered
|
|
82
|
+
* under. Feeds `CatalogMikroOrmStoreModule.forRoot({ contextName })`.
|
|
83
|
+
*/
|
|
84
|
+
contextName: string;
|
|
85
|
+
/** The durable keyspace. Always derived — see {@link durableKeyspaceFor}. */
|
|
86
|
+
durableKeyspace: string;
|
|
87
|
+
/**
|
|
88
|
+
* How far along the release path this environment sits. Lower is earlier.
|
|
89
|
+
*
|
|
90
|
+
* Its only job is to let a promotion refuse to run backwards. Promoting
|
|
91
|
+
* production's connectors into dev sounds harmless and is not: it would
|
|
92
|
+
* overwrite the very edits somebody is in the middle of testing, and the
|
|
93
|
+
* lost work has no version to recover from.
|
|
94
|
+
*/
|
|
95
|
+
rank: number;
|
|
96
|
+
/**
|
|
97
|
+
* Whether this environment refuses changes that did not arrive as a reviewed
|
|
98
|
+
* promotion.
|
|
99
|
+
*
|
|
100
|
+
* True for production. It does not lock the environment — an operator can
|
|
101
|
+
* still fix something by hand — it means the API demands the explicit
|
|
102
|
+
* confirmation described on {@link CatalogPromotionApproval} rather than
|
|
103
|
+
* accepting a plan somebody generated in another tab ten minutes ago.
|
|
104
|
+
*/
|
|
105
|
+
protected: boolean;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The caller named no environment.
|
|
109
|
+
*
|
|
110
|
+
* Its own class, and never folded into "unknown environment", because the two
|
|
111
|
+
* have different fixes: this one is a client that has not been taught to say
|
|
112
|
+
* which world it wants, and the other is a client that named one nobody has
|
|
113
|
+
* created. A single error message would send everybody to the wrong place.
|
|
114
|
+
*/
|
|
115
|
+
export declare class UnresolvedEnvironmentError extends Error {
|
|
116
|
+
constructor();
|
|
117
|
+
}
|
|
118
|
+
/** The caller named an environment this deployment does not run. */
|
|
119
|
+
export declare class UnknownEnvironmentError extends Error {
|
|
120
|
+
readonly requested: string;
|
|
121
|
+
constructor(requested: string, known: readonly CatalogEnvironmentId[]);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Reads the requested environment id off a request, without deciding whether it
|
|
125
|
+
* exists.
|
|
126
|
+
*
|
|
127
|
+
* Total and side-effect free so it can be used anywhere a request-like object
|
|
128
|
+
* is in hand — a guard, a middleware, a test. Returns undefined rather than
|
|
129
|
+
* throwing, because "nobody said" and "they said something wrong" are different
|
|
130
|
+
* refusals and the caller is the one that knows which message to produce.
|
|
131
|
+
*/
|
|
132
|
+
export declare function environmentIdFromRequest(request: unknown): string | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Turns a request into exactly one environment, or refuses.
|
|
135
|
+
*
|
|
136
|
+
* There is no fallback branch in this function and there must never be one.
|
|
137
|
+
* The failure this prevents is the one that has no symptom: a request that
|
|
138
|
+
* meant dev and was served by production returns perfectly plausible rows, and
|
|
139
|
+
* the only way anybody finds out is by noticing later that a number is wrong.
|
|
140
|
+
*/
|
|
141
|
+
export declare function resolveEnvironment(request: unknown, known: readonly CatalogEnvironment[]): CatalogEnvironment;
|
|
142
|
+
/**
|
|
143
|
+
* The Redis keyspace an environment's durable engine owns.
|
|
144
|
+
*
|
|
145
|
+
* Derived rather than configured, and that is the whole safety property. A
|
|
146
|
+
* deployment that sets `DURABLE_TENANT` by hand per environment has a
|
|
147
|
+
* copy-paste away from two environments on one keyspace, and the resulting
|
|
148
|
+
* failure is the one this project has already lived through: every
|
|
149
|
+
* `BullMQTransport` built on the bare `durable-*` prefix consumes a *single*
|
|
150
|
+
* `durable-results` queue, so one engine silently eats another's step results,
|
|
151
|
+
* the originating run's checkpoint stays `pending` forever, and nothing
|
|
152
|
+
* anywhere logs a word about it.
|
|
153
|
+
*
|
|
154
|
+
* The prefix keeps environments of different *services* apart on a shared
|
|
155
|
+
* Redis; the environment id keeps environments of this service apart from each
|
|
156
|
+
* other. Both halves are needed: `dev` alone would collide with any other
|
|
157
|
+
* product's dev engine.
|
|
158
|
+
*/
|
|
159
|
+
export declare function durableKeyspaceFor(environmentId: CatalogEnvironmentId, prefix?: string): string;
|
|
160
|
+
/**
|
|
161
|
+
* The database name an environment's tables live in.
|
|
162
|
+
*
|
|
163
|
+
* Composed here so the same rule is used by the thing that creates the database
|
|
164
|
+
* and the thing that connects to it. An operator may still override it per
|
|
165
|
+
* environment; what they may not do is have the two disagree.
|
|
166
|
+
*/
|
|
167
|
+
export declare function catalogDatabaseNameFor(base: string, environmentId: CatalogEnvironmentId): string;
|
|
168
|
+
/**
|
|
169
|
+
* An audit event, told with the environment it happened in.
|
|
170
|
+
*
|
|
171
|
+
* With a database per environment the environment is *implied* by which
|
|
172
|
+
* database the row sits in, and for a single-environment read that is enough.
|
|
173
|
+
* It stops being enough the moment anyone asks a governance question across
|
|
174
|
+
* environments — "everything this person did this week" has to be answerable
|
|
175
|
+
* without the reader having to remember which of three lists they are looking
|
|
176
|
+
* at — so the environment is stamped onto every event as it leaves its store.
|
|
177
|
+
*
|
|
178
|
+
* Stamped on read rather than stored in a column on purpose: a stored column
|
|
179
|
+
* can be wrong, because nothing in the database stops a row in the production
|
|
180
|
+
* table saying `environment: "dev"`. A value derived from which connection the
|
|
181
|
+
* row was read through cannot be.
|
|
182
|
+
*/
|
|
183
|
+
export interface EnvironmentStampedAuditEvent {
|
|
184
|
+
environment: CatalogEnvironmentId;
|
|
185
|
+
}
|
|
186
|
+
/** Stamps a batch of events with the environment they were read from. */
|
|
187
|
+
export declare function stampEnvironment<T>(environment: CatalogEnvironmentId, events: readonly T[]): Array<T & EnvironmentStampedAuditEvent>;
|
|
188
|
+
/** What a promotion is allowed to carry. */
|
|
189
|
+
export declare const PROMOTABLE_KINDS: readonly ["objectType", "transform", "workflow", "connector"];
|
|
190
|
+
export type PromotableKind = (typeof PROMOTABLE_KINDS)[number];
|
|
191
|
+
export declare function isPromotableKind(value: unknown): value is PromotableKind;
|
|
192
|
+
/**
|
|
193
|
+
* Connector fields a promotion never carries, and why each one would be a bug.
|
|
194
|
+
*
|
|
195
|
+
* - `state` is where the last run got to — a watermark, a continuation token, a
|
|
196
|
+
* last-seen id. Carrying dev's watermark into production tells production it
|
|
197
|
+
* has already read everything up to that point, so the next production run
|
|
198
|
+
* skips real data and reports success. Carrying production's into dev makes
|
|
199
|
+
* dev replay. Either way the corruption is invisible: both runs finish green.
|
|
200
|
+
* - `lastRunAt` / `lastRunStatus` describe something that happened in the other
|
|
201
|
+
* environment. A production connector showing a green run it never performed
|
|
202
|
+
* is a lie told by the screen people check first.
|
|
203
|
+
* - `secretEnvVar` names the credential. It is only a *name*, so promoting it
|
|
204
|
+
* leaks nothing — but the name is how the environment finds its own key, and
|
|
205
|
+
* production reading `DEV_WAREHOUSE_PASSWORD` is production pointed at dev.
|
|
206
|
+
* - `enabled` is withheld on create only, so a newly promoted connector arrives
|
|
207
|
+
* switched off. Arriving enabled means the first scheduler tick runs, in
|
|
208
|
+
* production, code nobody has yet watched run there. On update the target
|
|
209
|
+
* keeps whatever it had: re-promoting a transform must not silently
|
|
210
|
+
* re-enable a connector an operator deliberately turned off.
|
|
211
|
+
*/
|
|
212
|
+
export declare const PROMOTION_WITHHELD_CONNECTOR_FIELDS: readonly string[];
|
|
213
|
+
/**
|
|
214
|
+
* A connection is matched, never copied — every field of it is withheld.
|
|
215
|
+
*
|
|
216
|
+
* A connection *is* an address and a credential reference, and those are the
|
|
217
|
+
* two things that most define what an environment is. Promoting dev's
|
|
218
|
+
* connection config into production would repoint production at the dev
|
|
219
|
+
* database, and the load that followed would be a successful load of the wrong
|
|
220
|
+
* data. So promotion requires a connection with the same id to already exist in
|
|
221
|
+
* the target, configured by whoever owns that environment, and reports its
|
|
222
|
+
* absence as a blocker rather than creating one.
|
|
223
|
+
*/
|
|
224
|
+
export declare const PROMOTION_WITHHELD_CONNECTION_FIELDS: readonly string[];
|
|
225
|
+
/** A type and its properties, reduced to what a promotion carries. */
|
|
226
|
+
export interface PromotableObjectType {
|
|
227
|
+
name: string;
|
|
228
|
+
/**
|
|
229
|
+
* The application that publishes this type.
|
|
230
|
+
*
|
|
231
|
+
* Carried on create and never on update, which is why it is absent from
|
|
232
|
+
* {@link diffObjectType}'s field list. It has to be carried at all because a
|
|
233
|
+
* type owned by nobody in the target is a type its publisher would be refused
|
|
234
|
+
* from publishing into — the store checks ownership by exactly this string.
|
|
235
|
+
* It must never be *changed* by a promotion, because that would be a way to
|
|
236
|
+
* take a type away from the application that owns it without anybody
|
|
237
|
+
* approving an ownership change.
|
|
238
|
+
*/
|
|
239
|
+
ownerPrincipalId: string;
|
|
240
|
+
displayName: string;
|
|
241
|
+
pluralDisplayName: string;
|
|
242
|
+
description?: string;
|
|
243
|
+
icon?: string;
|
|
244
|
+
group: string;
|
|
245
|
+
titleProperty?: string;
|
|
246
|
+
primaryKey: string[];
|
|
247
|
+
properties: Array<{
|
|
248
|
+
name: string;
|
|
249
|
+
displayName: string;
|
|
250
|
+
description?: string;
|
|
251
|
+
type: string;
|
|
252
|
+
sourceColumn: string;
|
|
253
|
+
nullable: boolean;
|
|
254
|
+
primary: boolean;
|
|
255
|
+
hidden: boolean;
|
|
256
|
+
position: number;
|
|
257
|
+
unit?: string;
|
|
258
|
+
classification?: string;
|
|
259
|
+
}>;
|
|
260
|
+
}
|
|
261
|
+
export interface PromotableTransform {
|
|
262
|
+
id: string;
|
|
263
|
+
name: string;
|
|
264
|
+
description?: string;
|
|
265
|
+
language: TransformLanguage;
|
|
266
|
+
code: string;
|
|
267
|
+
/** The source's version, carried for the record only — see {@link planPromotion}. */
|
|
268
|
+
version: number;
|
|
269
|
+
}
|
|
270
|
+
export interface PromotableConnector {
|
|
271
|
+
id: string;
|
|
272
|
+
name: string;
|
|
273
|
+
description?: string;
|
|
274
|
+
kind: ConnectorKind;
|
|
275
|
+
targetType: string;
|
|
276
|
+
config: Record<string, unknown>;
|
|
277
|
+
connectionId?: string;
|
|
278
|
+
transformId?: string;
|
|
279
|
+
/**
|
|
280
|
+
* The graph it runs, when it runs one instead of a single transform.
|
|
281
|
+
*
|
|
282
|
+
* Carried for the same reason `transformId` is: a connector promoted without
|
|
283
|
+
* the thing that shapes its rows arrives pointing at nothing. It was missed
|
|
284
|
+
* once already — workflows were added to the connector while this file was
|
|
285
|
+
* being written — and the symptom would have been a load failing on its first
|
|
286
|
+
* scheduled run in the target, which is the worst place to find out.
|
|
287
|
+
*/
|
|
288
|
+
workflowId?: string;
|
|
289
|
+
schedule?: string;
|
|
290
|
+
mode?: 'full' | 'incremental';
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A workflow as promotion sees it.
|
|
294
|
+
*
|
|
295
|
+
* `graphHash` rather than `version` is what decides whether two environments
|
|
296
|
+
* hold the same graph. A version counts edits inside one database, so dev's v7
|
|
297
|
+
* and production's v7 are unrelated numbers; the hash is over the nodes, their
|
|
298
|
+
* executable config and the edges in order, so it means the same thing in both
|
|
299
|
+
* places. Comparing versions across environments would report a difference
|
|
300
|
+
* every time somebody edited dev twice and production once, and agreement every
|
|
301
|
+
* time the counts happened to line up.
|
|
302
|
+
*/
|
|
303
|
+
export interface PromotableWorkflow {
|
|
304
|
+
id: string;
|
|
305
|
+
name: string;
|
|
306
|
+
description?: string;
|
|
307
|
+
targetType: string;
|
|
308
|
+
graphHash: string;
|
|
309
|
+
version: number;
|
|
310
|
+
nodes: WorkflowNode[];
|
|
311
|
+
edges: WorkflowEdge[];
|
|
312
|
+
}
|
|
313
|
+
/** A connection as promotion sees it: an identity to match against, nothing more. */
|
|
314
|
+
export interface PromotionConnectionRef {
|
|
315
|
+
id: string;
|
|
316
|
+
name: string;
|
|
317
|
+
kind: ConnectorKind;
|
|
318
|
+
}
|
|
319
|
+
/** Everything promotable, read out of one environment. */
|
|
320
|
+
export interface CatalogPromotableSet {
|
|
321
|
+
objectTypes: PromotableObjectType[];
|
|
322
|
+
transforms: PromotableTransform[];
|
|
323
|
+
workflows: PromotableWorkflow[];
|
|
324
|
+
connectors: PromotableConnector[];
|
|
325
|
+
connections: PromotionConnectionRef[];
|
|
326
|
+
}
|
|
327
|
+
export type PromotionAction = 'create' | 'update' | 'unchanged';
|
|
328
|
+
/** One field that would change, with both sides shown. */
|
|
329
|
+
export interface PromotionFieldDiff {
|
|
330
|
+
field: string;
|
|
331
|
+
from: unknown;
|
|
332
|
+
to: unknown;
|
|
333
|
+
}
|
|
334
|
+
export interface CatalogPromotionChange {
|
|
335
|
+
kind: PromotableKind;
|
|
336
|
+
id: string;
|
|
337
|
+
name: string;
|
|
338
|
+
action: PromotionAction;
|
|
339
|
+
fields: PromotionFieldDiff[];
|
|
340
|
+
/** Things a reviewer should read before approving. Never fatal. */
|
|
341
|
+
notes: string[];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Something that makes the plan unappliable.
|
|
345
|
+
*
|
|
346
|
+
* A blocker is not a warning. The plan is reported in full so a reviewer can
|
|
347
|
+
* see everything that *would* happen, and then refused, because a promotion
|
|
348
|
+
* that applies the half it can and skips the half it cannot leaves the target
|
|
349
|
+
* in a state neither environment ever had.
|
|
350
|
+
*/
|
|
351
|
+
export interface CatalogPromotionBlocker {
|
|
352
|
+
kind: PromotableKind | 'connection';
|
|
353
|
+
id: string;
|
|
354
|
+
name: string;
|
|
355
|
+
reason: string;
|
|
356
|
+
}
|
|
357
|
+
/** Something the promotion deliberately left behind, named so the preview says so. */
|
|
358
|
+
export interface CatalogPromotionWithheld {
|
|
359
|
+
kind: PromotableKind | 'connection';
|
|
360
|
+
id: string;
|
|
361
|
+
name: string;
|
|
362
|
+
fields: readonly string[];
|
|
363
|
+
why: string;
|
|
364
|
+
}
|
|
365
|
+
export interface CatalogPromotionPlan {
|
|
366
|
+
from: CatalogEnvironmentId;
|
|
367
|
+
to: CatalogEnvironmentId;
|
|
368
|
+
createdAt: string;
|
|
369
|
+
changes: CatalogPromotionChange[];
|
|
370
|
+
blockers: CatalogPromotionBlocker[];
|
|
371
|
+
withheld: CatalogPromotionWithheld[];
|
|
372
|
+
/**
|
|
373
|
+
* A hash of exactly what this plan would do.
|
|
374
|
+
*
|
|
375
|
+
* The point of a preview is that somebody read it. Without a fingerprint the
|
|
376
|
+
* apply call is a fresh promotion that happens to have been preceded by a
|
|
377
|
+
* preview, and anything that changed in between — a colleague editing the
|
|
378
|
+
* transform, a connector deleted — goes in unreviewed. The apply endpoint
|
|
379
|
+
* demands this value back, recomputes the plan, and refuses if the two
|
|
380
|
+
* differ. That is what turns "we show a diff" into "you approved this diff".
|
|
381
|
+
*/
|
|
382
|
+
fingerprint: string;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The audit event name a completed promotion records.
|
|
386
|
+
*
|
|
387
|
+
* Deliberately *not* added to `CATALOG_EVENTS` and not emitted through
|
|
388
|
+
* `emitCatalog`. That channel is for things the library itself does during a
|
|
389
|
+
* load, and it is consumed by a recorder that writes wherever its workspace
|
|
390
|
+
* store points — which, with environments in play, is whichever environment
|
|
391
|
+
* happens to be in scope. A promotion is the one act that is genuinely about
|
|
392
|
+
* two environments at once, so its record is written directly into the target's
|
|
393
|
+
* audit table by the code that performed it, where there is no question about
|
|
394
|
+
* which environment the row belongs to.
|
|
395
|
+
*/
|
|
396
|
+
export declare const PROMOTION_AUDIT_EVENT = "promotion.applied";
|
|
397
|
+
/** What an apply call must present to prove which plan was approved. */
|
|
398
|
+
export interface CatalogPromotionApproval {
|
|
399
|
+
fingerprint: string;
|
|
400
|
+
/** Free text the operator typed. Recorded in the audit trail, never parsed. */
|
|
401
|
+
reason?: string;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Compute what promoting `source` into `target` would do.
|
|
405
|
+
*
|
|
406
|
+
* Pure, and pure on purpose: this is the function whose output a person reads
|
|
407
|
+
* before agreeing to change production, so it must be runnable against two
|
|
408
|
+
* plain objects, in a test, with no database anywhere near it.
|
|
409
|
+
*
|
|
410
|
+
* **Additive.** Nothing in the target that is absent from the source is
|
|
411
|
+
* touched, let alone deleted. A promotion is a release of the things somebody
|
|
412
|
+
* built, not a mirror of one environment onto another — and a production
|
|
413
|
+
* connector that exists for a reason dev knows nothing about must survive a
|
|
414
|
+
* colleague promoting an unrelated transform.
|
|
415
|
+
*
|
|
416
|
+
* **Version numbers do not cross.** A transform's `version` counts the edits
|
|
417
|
+
* made *in the environment it lives in*. Copying dev's version onto production
|
|
418
|
+
* would make production's own history unreadable — v7 following v3 with nothing
|
|
419
|
+
* in between — so the target bumps its own, and the source version is carried
|
|
420
|
+
* in the change note instead, which is what actually answers "which code is
|
|
421
|
+
* this".
|
|
422
|
+
*/
|
|
423
|
+
export declare function planPromotion(input: {
|
|
424
|
+
from: CatalogEnvironmentId;
|
|
425
|
+
to: CatalogEnvironmentId;
|
|
426
|
+
source: CatalogPromotableSet;
|
|
427
|
+
target: CatalogPromotableSet;
|
|
428
|
+
/**
|
|
429
|
+
* Restrict the promotion to specific things, by `kind:id`
|
|
430
|
+
* (`transform:abc`, `objectType:Mvr`). Undefined means everything promotable.
|
|
431
|
+
*
|
|
432
|
+
* Worth having rather than an all-or-nothing release: dev accumulates
|
|
433
|
+
* experiments nobody intends to ship, and a promotion that could only carry
|
|
434
|
+
* every one of them would be a promotion nobody dares run.
|
|
435
|
+
*/
|
|
436
|
+
select?: readonly string[];
|
|
437
|
+
now?: () => Date;
|
|
438
|
+
}): CatalogPromotionPlan;
|
|
439
|
+
/** Whether a plan may be applied at all. */
|
|
440
|
+
export declare function isPromotable(plan: CatalogPromotionPlan): boolean;
|
|
441
|
+
/** The changes that actually do something. Used by both the apply and the summary. */
|
|
442
|
+
export declare function effectiveChanges(plan: CatalogPromotionPlan): CatalogPromotionChange[];
|