@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/catalog.controller.d.ts +8 -0
  4. package/dist/catalog.controller.js +482 -0
  5. package/dist/catalog.decorators.d.ts +37 -0
  6. package/dist/catalog.decorators.js +50 -0
  7. package/dist/catalog.environment.d.ts +442 -0
  8. package/dist/catalog.environment.js +645 -0
  9. package/dist/catalog.events.d.ts +179 -0
  10. package/dist/catalog.events.js +110 -0
  11. package/dist/catalog.module.d.ts +5 -0
  12. package/dist/catalog.module.js +71 -0
  13. package/dist/catalog.options.d.ts +79 -0
  14. package/dist/catalog.options.js +4 -0
  15. package/dist/catalog.overlay-store.d.ts +25 -0
  16. package/dist/catalog.overlay-store.js +44 -0
  17. package/dist/catalog.overlay-store.token.d.ts +1 -0
  18. package/dist/catalog.overlay-store.token.js +4 -0
  19. package/dist/catalog.pipeline.d.ts +800 -0
  20. package/dist/catalog.pipeline.js +606 -0
  21. package/dist/catalog.principal.d.ts +209 -0
  22. package/dist/catalog.principal.js +245 -0
  23. package/dist/catalog.query-cache.d.ts +25 -0
  24. package/dist/catalog.query-cache.js +0 -0
  25. package/dist/catalog.query.d.ts +76 -0
  26. package/dist/catalog.query.js +64 -0
  27. package/dist/catalog.registry.base.d.ts +21 -0
  28. package/dist/catalog.registry.base.js +17 -0
  29. package/dist/catalog.registry.d.ts +44 -0
  30. package/dist/catalog.registry.js +359 -0
  31. package/dist/catalog.service.d.ts +115 -0
  32. package/dist/catalog.service.js +366 -0
  33. package/dist/catalog.store.d.ts +419 -0
  34. package/dist/catalog.store.js +175 -0
  35. package/dist/catalog.types.d.ts +165 -0
  36. package/dist/catalog.types.js +19 -0
  37. package/dist/catalog.workspace.d.ts +426 -0
  38. package/dist/catalog.workspace.js +87 -0
  39. package/dist/client.d.ts +86 -0
  40. package/dist/client.js +83 -0
  41. package/dist/index.d.ts +19 -0
  42. package/dist/index.js +109 -0
  43. package/dist/stores/mikro-orm-read.store.d.ts +20 -0
  44. package/dist/stores/mikro-orm-read.store.js +120 -0
  45. package/dist/transform-runner.d.ts +54 -0
  46. package/dist/transform-runner.js +280 -0
  47. 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[];