@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,179 @@
1
+ /**
2
+ * What this package publishes on `aviary:catalog:*`.
3
+ *
4
+ * Diagnostics rather than a logger, and certainly rather than a direct
5
+ * Telescope dependency: the channel is the neutral seam the ecosystem is built
6
+ * on, so a host that wants these in Telescope installs
7
+ * `@dudousxd/nestjs-catalog-telescope`, one that wants them in its own tracing
8
+ * subscribes directly, and one that wants neither pays nothing — `emit` builds
9
+ * no envelope unless the channel has subscribers.
10
+ *
11
+ * These events are also the lineage feed. Since the catalog deliberately has no
12
+ * dataset history to walk (see the Iceberg decision), the record of who loaded
13
+ * what and when only exists because it was emitted as it happened.
14
+ */
15
+ export declare const CATALOG_LIB = "catalog";
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", "connector.run.started", "connector.run.finished", "transform.changed", "workflow.changed"];
18
+ export type CatalogEvent = (typeof CATALOG_EVENTS)[number];
19
+ /**
20
+ * Where each event sits in the life of one load.
21
+ *
22
+ * This exists because a timestamp is not enough to order a trace. A fast load
23
+ * emits its whole story inside a single tick of whatever clock the recorder
24
+ * writes — the bundled MySQL store keeps whole seconds — so sorting by time
25
+ * alone leaves the events in insertion order, and insertion order is arbitrary.
26
+ * The observed result is a trace that reads `finished → written → committed →
27
+ * started`: causally impossible, rendered with total confidence. Sorting by
28
+ * time *and then* by this rank puts a same-tick story back in the only order it
29
+ * could have happened in, and leaves genuinely separated events alone.
30
+ *
31
+ * It is a rank, not a schedule. Nothing here says an event must occur, or must
32
+ * occur once — a load writes many batches, and a retried run emits a second
33
+ * `connector.run.started` — it only says which came first when the clock cannot.
34
+ */
35
+ export declare const CATALOG_EVENT_PHASE: Record<CatalogEvent, number>;
36
+ /**
37
+ * Where an event nobody here declared is placed: in the middle, with the work.
38
+ *
39
+ * Not at either end deliberately. An unknown event sorted last would be read as
40
+ * the thing that ended the trace, and one sorted first as the thing that caused
41
+ * it — both are claims this library has no basis to make about a name it does
42
+ * not recognise.
43
+ */
44
+ export declare const CATALOG_EVENT_PHASE_FALLBACK = 4;
45
+ export declare function catalogEventPhase(event: string): number;
46
+ export interface CatalogEventPayloads {
47
+ /** DDL was applied to an object type's physical table. Always additive. */
48
+ 'schema.changed': {
49
+ typeName: string;
50
+ table: string;
51
+ addedColumns: string[];
52
+ created: boolean;
53
+ };
54
+ /** A batch landed. Fires per batch, so a large load emits many. */
55
+ 'snapshot.written': {
56
+ typeName: string;
57
+ snapshotId: string;
58
+ principalId: string;
59
+ rows: number;
60
+ };
61
+ /** A load became the one readers get. */
62
+ 'snapshot.committed': {
63
+ typeName: string;
64
+ snapshotId: string;
65
+ principalId: string;
66
+ rowCount: number;
67
+ };
68
+ 'snapshot.dropped': {
69
+ typeName: string;
70
+ snapshotId: string;
71
+ };
72
+ /**
73
+ * Someone changed a label, description, unit or visibility.
74
+ *
75
+ * Presentation-only, and emitted anyway: "who renamed this column and when"
76
+ * is a governance question, and the answer is otherwise nowhere.
77
+ */
78
+ 'type.curated': {
79
+ typeName: string;
80
+ property?: string;
81
+ changed: string[];
82
+ };
83
+ /** A connector began pulling. */
84
+ 'connector.run.started': {
85
+ connectorId: string;
86
+ connectorName: string;
87
+ typeName: string;
88
+ snapshotId: string;
89
+ principalId: string;
90
+ };
91
+ /**
92
+ * It finished, either way.
93
+ *
94
+ * One event for both outcomes rather than two: a watcher that cares about
95
+ * failures has to subscribe to successes anyway to notice the ones that never
96
+ * finished.
97
+ */
98
+ 'connector.run.finished': {
99
+ connectorId: string;
100
+ connectorName: string;
101
+ typeName: string;
102
+ snapshotId: string;
103
+ principalId: string;
104
+ status: 'succeeded' | 'failed';
105
+ fetched: number;
106
+ written: number;
107
+ error?: string;
108
+ transformVersion?: number;
109
+ };
110
+ /**
111
+ * Someone changed code that shapes stored data.
112
+ *
113
+ * The one event on this channel that is not about data movement, and the most
114
+ * important for governance: a load that produced surprising numbers is
115
+ * investigated afterwards, and "who changed the transform, and when" is the
116
+ * first question.
117
+ */
118
+ 'transform.changed': {
119
+ transformId: string;
120
+ name: string;
121
+ language: string;
122
+ version: number;
123
+ changedBy: string;
124
+ };
125
+ /**
126
+ * Someone changed the wiring rather than the code.
127
+ *
128
+ * Emitted for the same governance reason as `transform.changed`, and it is
129
+ * genuinely a second question: a load can change behaviour because a
130
+ * transform was edited *or* because a node was rewired between two transforms
131
+ * that both stayed exactly as they were. The graph hash rides along because a
132
+ * version number only identifies a graph within one catalog database, and
133
+ * these events are read across environments.
134
+ */
135
+ 'workflow.changed': {
136
+ workflowId: string;
137
+ name: string;
138
+ version: number;
139
+ graphHash: string;
140
+ /** The type its sink writes, so a lineage view can place the edit. */
141
+ targetType: string;
142
+ nodeCount: number;
143
+ changedBy: string;
144
+ };
145
+ }
146
+ /**
147
+ * Teach the diagnostics channel what this library puts on it.
148
+ *
149
+ * Without this, `CatalogEventPayloads` only helps callers who go through
150
+ * `emitCatalog`. Anyone subscribing the way the ecosystem actually subscribes —
151
+ * generic `emit`/`trace`/watcher code that names the lib as a string — gets
152
+ * `unknown` and narrows by hand, which is where a payload field gets misspelled
153
+ * and nothing says so until the panel is blank in production.
154
+ *
155
+ * Pointed at the interface rather than restating its members. The sibling
156
+ * libraries hand-list every event here, which is a second copy of a map that
157
+ * already exists, and a second copy is the thing that stops matching. There is
158
+ * nothing to keep in step: this *is* the map.
159
+ */
160
+ declare module '@dudousxd/nestjs-diagnostics' {
161
+ interface ChannelRegistry {
162
+ catalog: CatalogEventPayloads;
163
+ }
164
+ }
165
+ /**
166
+ * The channel an event is published on.
167
+ *
168
+ * Exported so a recorder or a watcher can subscribe without rebuilding the
169
+ * `aviary:<lib>:<event>` convention by hand — the one place that string is
170
+ * assembled is the one place it can drift.
171
+ */
172
+ export declare function channelNameFor(event: CatalogEvent | string): string;
173
+ /**
174
+ * Typed wrapper over `emit`.
175
+ *
176
+ * Never throws — observability that can break a load is worse than no
177
+ * observability — which `emit` already guarantees; this exists for the types.
178
+ */
179
+ export declare function emitCatalog<E extends CatalogEvent>(event: E, payload: CatalogEventPayloads[E]): void;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CATALOG_EVENTS = exports.CATALOG_LIB = void 0;
4
+ exports.catalogEventPhase = catalogEventPhase;
5
+ exports.channelNameFor = channelNameFor;
6
+ exports.emitCatalog = emitCatalog;
7
+ const nestjs_diagnostics_1 = require("@dudousxd/nestjs-diagnostics");
8
+ /**
9
+ * What this package publishes on `aviary:catalog:*`.
10
+ *
11
+ * Diagnostics rather than a logger, and certainly rather than a direct
12
+ * Telescope dependency: the channel is the neutral seam the ecosystem is built
13
+ * on, so a host that wants these in Telescope installs
14
+ * `@dudousxd/nestjs-catalog-telescope`, one that wants them in its own tracing
15
+ * subscribes directly, and one that wants neither pays nothing — `emit` builds
16
+ * no envelope unless the channel has subscribers.
17
+ *
18
+ * These events are also the lineage feed. Since the catalog deliberately has no
19
+ * dataset history to walk (see the Iceberg decision), the record of who loaded
20
+ * what and when only exists because it was emitted as it happened.
21
+ */
22
+ exports.CATALOG_LIB = 'catalog';
23
+ /** Every event name this package emits. Exported so a watcher can claim them. */
24
+ exports.CATALOG_EVENTS = [
25
+ 'schema.changed',
26
+ 'snapshot.written',
27
+ 'snapshot.committed',
28
+ 'snapshot.dropped',
29
+ 'type.curated',
30
+ 'connector.run.started',
31
+ 'connector.run.finished',
32
+ 'transform.changed',
33
+ 'workflow.changed',
34
+ ];
35
+ /**
36
+ * Where each event sits in the life of one load.
37
+ *
38
+ * This exists because a timestamp is not enough to order a trace. A fast load
39
+ * emits its whole story inside a single tick of whatever clock the recorder
40
+ * writes — the bundled MySQL store keeps whole seconds — so sorting by time
41
+ * alone leaves the events in insertion order, and insertion order is arbitrary.
42
+ * The observed result is a trace that reads `finished → written → committed →
43
+ * started`: causally impossible, rendered with total confidence. Sorting by
44
+ * time *and then* by this rank puts a same-tick story back in the only order it
45
+ * could have happened in, and leaves genuinely separated events alone.
46
+ *
47
+ * It is a rank, not a schedule. Nothing here says an event must occur, or must
48
+ * occur once — a load writes many batches, and a retried run emits a second
49
+ * `connector.run.started` — it only says which came first when the clock cannot.
50
+ */
51
+ exports.CATALOG_EVENT_PHASE = {
52
+ 'connector.run.started': 0,
53
+ 'transform.changed': 1,
54
+ // The same rank as the transform edit above, which is allowed and meant:
55
+ // these ranks order a same-tick trace, and editing a graph and editing the
56
+ // code inside it are the same kind of thing at the same point in the story —
57
+ // an authoring event that precedes whatever run is being investigated. Giving
58
+ // one an earlier rank than the other would assert a causal order between two
59
+ // edits that have none.
60
+ 'workflow.changed': 1,
61
+ // Curation carries no snapshot id, so it never lands inside a trace and this
62
+ // rank is never consulted. It is written out rather than left to the fallback
63
+ // because the type demands every event have one, and a `Record` that has to
64
+ // be complete is exactly the mechanism that will fail the build the day a new
65
+ // event is added and nobody thinks about where it belongs.
66
+ 'type.curated': 2,
67
+ 'schema.changed': 3,
68
+ 'snapshot.written': 4,
69
+ 'snapshot.committed': 5,
70
+ 'snapshot.dropped': 6,
71
+ 'connector.run.finished': 7,
72
+ };
73
+ /**
74
+ * Where an event nobody here declared is placed: in the middle, with the work.
75
+ *
76
+ * Not at either end deliberately. An unknown event sorted last would be read as
77
+ * the thing that ended the trace, and one sorted first as the thing that caused
78
+ * it — both are claims this library has no basis to make about a name it does
79
+ * not recognise.
80
+ */
81
+ exports.CATALOG_EVENT_PHASE_FALLBACK = 4;
82
+ function catalogEventPhase(event) {
83
+ const phase = Reflect.get(exports.CATALOG_EVENT_PHASE, event);
84
+ return typeof phase === 'number' ? phase : exports.CATALOG_EVENT_PHASE_FALLBACK;
85
+ }
86
+ /**
87
+ * The channel an event is published on.
88
+ *
89
+ * Exported so a recorder or a watcher can subscribe without rebuilding the
90
+ * `aviary:<lib>:<event>` convention by hand — the one place that string is
91
+ * assembled is the one place it can drift.
92
+ */
93
+ function channelNameFor(event) {
94
+ return (0, nestjs_diagnostics_1.channelName)(exports.CATALOG_LIB, event);
95
+ }
96
+ /**
97
+ * Typed wrapper over `emit`.
98
+ *
99
+ * Never throws — observability that can break a load is worse than no
100
+ * observability — which `emit` already guarantees; this exists for the types.
101
+ */
102
+ function emitCatalog(event, payload) {
103
+ // The type arguments are named rather than inferred. `emit` types its payload
104
+ // as `PayloadOf<TLib, TEvent>`, a conditional that stays unresolved while
105
+ // `TEvent` is still an open generic — so inference leaves the compiler unable
106
+ // to see that the registry entry and `CatalogEventPayloads` are now the same
107
+ // type. Passing the whole event union makes the conditional distribute into a
108
+ // union of every payload, which the one being emitted is a member of.
109
+ (0, nestjs_diagnostics_1.emit)(exports.CATALOG_LIB, event, payload);
110
+ }
@@ -0,0 +1,5 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import { type CatalogModuleOptions } from './catalog.options';
3
+ export declare class CatalogModule {
4
+ static forRoot(options?: CatalogModuleOptions): DynamicModule;
5
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var CatalogModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.CatalogModule = void 0;
11
+ const node_path_1 = require("node:path");
12
+ const common_1 = require("@nestjs/common");
13
+ const catalog_controller_1 = require("./catalog.controller");
14
+ const catalog_options_1 = require("./catalog.options");
15
+ const catalog_overlay_store_1 = require("./catalog.overlay-store");
16
+ const catalog_overlay_store_token_1 = require("./catalog.overlay-store.token");
17
+ const catalog_registry_1 = require("./catalog.registry");
18
+ const catalog_registry_base_1 = require("./catalog.registry.base");
19
+ const catalog_service_1 = require("./catalog.service");
20
+ const catalog_store_1 = require("./catalog.store");
21
+ const mikro_orm_read_store_1 = require("./stores/mikro-orm-read.store");
22
+ let CatalogModule = CatalogModule_1 = class CatalogModule {
23
+ static forRoot(options = {}) {
24
+ const path = options.path ?? 'api/catalog';
25
+ const mountController = options.controller !== false;
26
+ const controllers = mountController
27
+ ? [(0, catalog_controller_1.createCatalogController)(path, options.guards ?? [], options.decorators ?? [])]
28
+ : [];
29
+ const overlayStore = options.overlayStore ??
30
+ new catalog_overlay_store_1.FileCatalogOverlayStore(options.overlayPath ?? (0, node_path_1.join)(process.cwd(), '.catalog', 'overlay.json'));
31
+ return {
32
+ module: CatalogModule_1,
33
+ imports: options.imports ?? [],
34
+ controllers,
35
+ providers: [
36
+ { provide: catalog_options_1.CATALOG_OPTIONS, useValue: options },
37
+ { provide: catalog_overlay_store_token_1.CATALOG_OVERLAY_STORE, useValue: overlayStore },
38
+ // The MikroORM-backed defaults are registered only when nothing
39
+ // overrides them. Registering them unconditionally would be worse than
40
+ // wasteful: a provider declared in this module shadows the same token
41
+ // exported by an imported one, so the default would quietly win over
42
+ // the host's override and the failure would look like the override
43
+ // never took effect.
44
+ //
45
+ // Derived is the right default — the model comes from the ORM of the
46
+ // application that owns the tables, and the rows come from those same
47
+ // tables, so nothing is stale and no infrastructure is needed. A
48
+ // warehouse overrides both: its model arrived over the wire and there
49
+ // are no entity classes to reflect over.
50
+ ...(options.registry
51
+ ? [options.registry]
52
+ : [
53
+ catalog_registry_1.MikroOrmCatalogRegistry,
54
+ {
55
+ provide: catalog_registry_base_1.CatalogRegistry,
56
+ useExisting: catalog_registry_1.MikroOrmCatalogRegistry,
57
+ },
58
+ ]),
59
+ ...(options.store
60
+ ? [options.store]
61
+ : [mikro_orm_read_store_1.MikroOrmReadStore, { provide: catalog_store_1.CATALOG_STORE, useExisting: mikro_orm_read_store_1.MikroOrmReadStore }]),
62
+ catalog_service_1.CatalogService,
63
+ ],
64
+ exports: [catalog_registry_base_1.CatalogRegistry, catalog_service_1.CatalogService, catalog_store_1.CATALOG_STORE],
65
+ };
66
+ }
67
+ };
68
+ exports.CatalogModule = CatalogModule;
69
+ exports.CatalogModule = CatalogModule = CatalogModule_1 = __decorate([
70
+ (0, common_1.Module)({})
71
+ ], CatalogModule);
@@ -0,0 +1,79 @@
1
+ import type { DynamicModule, ForwardReference, Provider, Type } from '@nestjs/common';
2
+ import type { CatalogOverlayStore } from './catalog.overlay-store';
3
+ export declare const CATALOG_OPTIONS: unique symbol;
4
+ export interface CatalogModuleOptions {
5
+ /** Only expose these entity class names. Empty/undefined means all of them. */
6
+ include?: string[];
7
+ /** Never expose these, whatever else says. */
8
+ exclude?: string[];
9
+ /** Group for types that declare none. */
10
+ defaultGroup?: string;
11
+ /** Where tier-0 edits are persisted. Defaults to a JSON file. */
12
+ overlayPath?: string;
13
+ overlayStore?: CatalogOverlayStore;
14
+ /**
15
+ * Route prefix for the introspection + object-read API.
16
+ * Defaults to `api/catalog`.
17
+ */
18
+ path?: string;
19
+ /**
20
+ * Guards applied to every catalog route. The library ships none: an
21
+ * introspection endpoint that lists every table in the system is exactly the
22
+ * kind of thing that should never be open by default, and only the host app
23
+ * knows what its auth looks like.
24
+ */
25
+ guards?: Type<unknown>[];
26
+ /**
27
+ * Extra class decorators applied to the generated controller — the escape
28
+ * hatch for host-specific metadata (roles, rate limits, audit tags) that a
29
+ * generic library has no business knowing the shape of.
30
+ */
31
+ decorators?: ClassDecorator[];
32
+ /**
33
+ * Modules this dynamic module should import.
34
+ *
35
+ * Needed because Nest instantiates a guard in the injector of the module that
36
+ * *declares* the controller — which, for a library that generates its own
37
+ * controller, is the library's module and not yours. Whatever your guards
38
+ * inject has to be resolvable from here.
39
+ */
40
+ imports?: Array<Type<unknown> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
41
+ /**
42
+ * Where the objects live. A provider for `CATALOG_STORE`.
43
+ *
44
+ * Defaults to reading the host application's own tables through MikroORM,
45
+ * which is always current and needs no infrastructure. Supply a warehouse
46
+ * adapter instead when the catalog should hold its own copy and keep history
47
+ * — the trade is a load to schedule and a staleness window to explain.
48
+ */
49
+ store?: Provider;
50
+ /**
51
+ * Where the type definitions come from. A provider for `CatalogRegistry`.
52
+ *
53
+ * Defaults to deriving them from the host's MikroORM metadata. A warehouse
54
+ * that receives its model from other applications binds a stored
55
+ * implementation here instead.
56
+ */
57
+ registry?: Provider;
58
+ /**
59
+ * Mount the built-in controller. Default true.
60
+ *
61
+ * Set false to keep the registry and reads but publish your own routes:
62
+ * inject `CatalogService` by class and shape the HTTP surface however your
63
+ * app already shapes it. Everything the built-in controller does is on that
64
+ * one service.
65
+ */
66
+ controller?: boolean;
67
+ /** Hard cap on generic object reads, so the explorer cannot pull a table. */
68
+ maxPageSize?: number;
69
+ /** Hard cap on rows an ad-hoc query may return. Default 1000. */
70
+ maxQueryRows?: number;
71
+ /**
72
+ * How long a query may run. Default 15s.
73
+ *
74
+ * A console where anyone can write a cartesian join needs this more than it
75
+ * needs a bigger row cap — a query that returns nothing for four minutes is
76
+ * holding a connection the whole time.
77
+ */
78
+ queryTimeoutMs?: number;
79
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CATALOG_OPTIONS = void 0;
4
+ exports.CATALOG_OPTIONS = Symbol('CATALOG_OPTIONS');
@@ -0,0 +1,25 @@
1
+ import type { CatalogOverlay } from './catalog.types';
2
+ /**
3
+ * Where tier-0 edits live.
4
+ *
5
+ * Deliberately not the application database. The overlay is control-plane
6
+ * state, and mixing it into the operational schema is the first step toward a
7
+ * service that owns a database connection it should not have.
8
+ */
9
+ export interface CatalogOverlayStore {
10
+ load(): Promise<CatalogOverlay>;
11
+ save(overlay: CatalogOverlay): Promise<void>;
12
+ }
13
+ /** The default: a JSON file. Good enough until there is a control plane. */
14
+ export declare class FileCatalogOverlayStore implements CatalogOverlayStore {
15
+ private readonly path;
16
+ constructor(path: string);
17
+ load(): Promise<CatalogOverlay>;
18
+ save(overlay: CatalogOverlay): Promise<void>;
19
+ }
20
+ /** For tests, and for deployments that want the catalog strictly read-only. */
21
+ export declare class InMemoryCatalogOverlayStore implements CatalogOverlayStore {
22
+ private overlay;
23
+ load(): Promise<CatalogOverlay>;
24
+ save(overlay: CatalogOverlay): Promise<void>;
25
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryCatalogOverlayStore = exports.FileCatalogOverlayStore = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ /** The default: a JSON file. Good enough until there is a control plane. */
7
+ class FileCatalogOverlayStore {
8
+ path;
9
+ constructor(path) {
10
+ this.path = path;
11
+ }
12
+ async load() {
13
+ try {
14
+ const raw = await (0, promises_1.readFile)(this.path, 'utf8');
15
+ const parsed = JSON.parse(raw);
16
+ if (parsed &&
17
+ typeof parsed === 'object' &&
18
+ 'types' in parsed &&
19
+ typeof parsed.types === 'object') {
20
+ return parsed;
21
+ }
22
+ return { types: {} };
23
+ }
24
+ catch {
25
+ return { types: {} };
26
+ }
27
+ }
28
+ async save(overlay) {
29
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(this.path), { recursive: true });
30
+ await (0, promises_1.writeFile)(this.path, `${JSON.stringify(overlay, null, 2)}\n`, 'utf8');
31
+ }
32
+ }
33
+ exports.FileCatalogOverlayStore = FileCatalogOverlayStore;
34
+ /** For tests, and for deployments that want the catalog strictly read-only. */
35
+ class InMemoryCatalogOverlayStore {
36
+ overlay = { types: {} };
37
+ async load() {
38
+ return this.overlay;
39
+ }
40
+ async save(overlay) {
41
+ this.overlay = overlay;
42
+ }
43
+ }
44
+ exports.InMemoryCatalogOverlayStore = InMemoryCatalogOverlayStore;
@@ -0,0 +1 @@
1
+ export declare const CATALOG_OVERLAY_STORE: unique symbol;
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CATALOG_OVERLAY_STORE = void 0;
4
+ exports.CATALOG_OVERLAY_STORE = Symbol('CATALOG_OVERLAY_STORE');