@malloydata/malloy 0.0.374 → 0.0.376

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.
@@ -161,10 +161,12 @@ export declare class MalloyConfig {
161
161
  *
162
162
  * Returns the value the named overlay produces for the given path,
163
163
  * or `undefined` if the overlay doesn't exist or the path has no value.
164
+ * Async because overlays may be async (secret stores, session readers);
165
+ * `await` tolerates both sync and Promise return types.
164
166
  *
165
- * config.readOverlay('config', 'rootDirectory')
166
- * config.readOverlay('config', 'configURL')
167
- * config.readOverlay('env', 'PG_PASSWORD')
167
+ * await config.readOverlay('config', 'rootDirectory')
168
+ * await config.readOverlay('config', 'configURL')
169
+ * await config.readOverlay('env', 'PG_PASSWORD')
168
170
  */
169
- readOverlay(overlayName: string, ...path: string[]): unknown;
171
+ readOverlay(overlayName: string, ...path: string[]): Promise<unknown>;
170
172
  }
@@ -5,9 +5,9 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.MalloyConfig = exports.Manifest = void 0;
8
- const registry_1 = require("../../connection/registry");
9
8
  const config_compile_1 = require("./config_compile");
10
9
  const config_resolve_1 = require("./config_resolve");
10
+ const config_lookup_1 = require("./config_lookup");
11
11
  const config_overlays_1 = require("./config_overlays");
12
12
  /**
13
13
  * In-memory manifest store. Reads, updates, and serializes manifest data.
@@ -175,24 +175,25 @@ class MalloyConfig {
175
175
  // Compile → typed tree + validation warnings.
176
176
  const compiled = (0, config_compile_1.compileConfig)(pojo);
177
177
  log.push(...compiled.log);
178
- // Resolve against the merged config overlays. Entries from the passed
179
- // overlays replace same-named entries in the defaults; anything new
180
- // is added.
178
+ // Merge the host-supplied overlays onto the defaults. Same-named
179
+ // entries replace defaults; new keys are added.
181
180
  const mergedOverlays = {
182
181
  ...(0, config_overlays_1.defaultConfigOverlays)(),
183
182
  ...overlays,
184
183
  };
185
- const resolved = (0, config_resolve_1.resolveConfig)(compiled.compiled, mergedOverlays, log);
186
- // Hand fully-resolved connection entries to the registry — factories
187
- // never see raw references.
188
- this._managedLookup = (0, registry_1.createConnectionsFromConfig)({
189
- connections: resolved.connections,
190
- });
184
+ // Synchronous prep: extract literal sections (manifestPath, virtualMap),
185
+ // pull out compiled connection subtrees, fabricate bare entries for
186
+ // missing registered types when `includeDefaultConnections` is set.
187
+ // Reference resolution for connection properties is *not* done here —
188
+ // it happens async at `lookupConnection` time, so overlays that touch
189
+ // IO (secret stores, session reads) have a natural async seam.
190
+ const prepared = (0, config_resolve_1.prepareConfig)(compiled.compiled, log);
191
+ this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, mergedOverlays, log);
191
192
  this._connections = this._managedLookup;
192
193
  this._overlays = mergedOverlays;
193
- this.virtualMap = toVirtualMap(resolved.virtualMap);
194
- this.manifestPath = resolved.manifestPath;
195
- this.manifestURL = computeManifestURL(resolved.manifestPath, mergedOverlays);
194
+ this.virtualMap = toVirtualMap(prepared.virtualMap);
195
+ this.manifestPath = prepared.manifestPath;
196
+ this.manifestURL = computeManifestURL(prepared.manifestPath, mergedOverlays, log);
196
197
  this.log = log;
197
198
  }
198
199
  /**
@@ -245,14 +246,16 @@ class MalloyConfig {
245
246
  *
246
247
  * Returns the value the named overlay produces for the given path,
247
248
  * or `undefined` if the overlay doesn't exist or the path has no value.
249
+ * Async because overlays may be async (secret stores, session readers);
250
+ * `await` tolerates both sync and Promise return types.
248
251
  *
249
- * config.readOverlay('config', 'rootDirectory')
250
- * config.readOverlay('config', 'configURL')
251
- * config.readOverlay('env', 'PG_PASSWORD')
252
+ * await config.readOverlay('config', 'rootDirectory')
253
+ * await config.readOverlay('config', 'configURL')
254
+ * await config.readOverlay('env', 'PG_PASSWORD')
252
255
  */
253
- readOverlay(overlayName, ...path) {
256
+ async readOverlay(overlayName, ...path) {
254
257
  var _a, _b;
255
- return (_b = (_a = this._overlays)[overlayName]) === null || _b === void 0 ? void 0 : _b.call(_a, path);
258
+ return await ((_b = (_a = this._overlays)[overlayName]) === null || _b === void 0 ? void 0 : _b.call(_a, path));
256
259
  }
257
260
  }
258
261
  exports.MalloyConfig = MalloyConfig;
@@ -276,6 +279,14 @@ const MANIFEST_FILENAME = 'malloy-manifest.json';
276
279
  *
277
280
  * Rules:
278
281
  * - If there's no `configURL` in the `config` overlay → undefined.
282
+ * - **The `config` overlay MUST resolve `configURL` synchronously.** This
283
+ * is the one construction-time overlay call; it runs before the first
284
+ * `lookupConnection` and must return a plain string (or undefined). If
285
+ * it returns a Promise, `manifestURL` would be `undefined` — persistence
286
+ * would silently stop working. To avoid the silent failure, we warn on
287
+ * Promise returns so hosts discover the mistake. Other fields inside the
288
+ * `config` overlay (`rootDirectory`, etc.) can still be async; only
289
+ * `configURL` is sync-only.
279
290
  * - `manifestPath` defaults to `'MANIFESTS'`.
280
291
  * - `new URL(manifestPath, configURL)` handles three shapes uniformly:
281
292
  * "MANIFESTS" → relative to configURL's directory
@@ -285,9 +296,17 @@ const MANIFEST_FILENAME = 'malloy-manifest.json';
285
296
  * - A trailing slash is forced onto the directory portion so that the
286
297
  * final `new URL(MANIFEST_FILENAME, dir)` joins correctly.
287
298
  */
288
- function computeManifestURL(manifestPath, overlays) {
299
+ function computeManifestURL(manifestPath, overlays, log) {
289
300
  const configOverlay = overlays['config'];
290
301
  const configURLValue = configOverlay === null || configOverlay === void 0 ? void 0 : configOverlay(['configURL']);
302
+ if (isThenable(configURLValue)) {
303
+ log.push({
304
+ message: 'the `config` overlay returned a Promise for `configURL`; `configURL` must be resolved synchronously. manifestURL will be undefined and persistence will not work.',
305
+ severity: 'warn',
306
+ code: 'config-overlay',
307
+ });
308
+ return undefined;
309
+ }
291
310
  if (typeof configURLValue !== 'string')
292
311
  return undefined;
293
312
  let configURL;
@@ -336,6 +355,11 @@ function toVirtualMap(raw) {
336
355
  function isRecord(value) {
337
356
  return typeof value === 'object' && value !== null && !Array.isArray(value);
338
357
  }
358
+ function isThenable(value) {
359
+ return (typeof value === 'object' &&
360
+ value !== null &&
361
+ typeof value.then === 'function');
362
+ }
339
363
  function isBuildManifestEntry(value) {
340
364
  return isRecord(value) && typeof value['tableName'] === 'string';
341
365
  }
@@ -0,0 +1,31 @@
1
+ import type { LogMessage } from '../../lang/parse-log';
2
+ import type { ManagedConnectionLookup } from '../../connection/registry';
3
+ import type { ConfigDict } from './config_compile';
4
+ import type { ConfigOverlays } from './config_overlays';
5
+ /**
6
+ * Build the `ManagedConnectionLookup` a `MalloyConfig` hands to Runtime.
7
+ *
8
+ * Reference resolution and property-default application run *per lookup*,
9
+ * asynchronously. This is the point where overlays are actually called —
10
+ * which means secret stores, session readers, and any other IO-backed
11
+ * overlay has a natural async seam to live in. Construction of the
12
+ * `MalloyConfig` stays synchronous and zero-IO.
13
+ *
14
+ * Lookup flow for a connection name:
15
+ * 1. Find its compiled entry. Throw if the name is unknown.
16
+ * 2. Walk the entry tree, `await`-ing each overlay reference.
17
+ * 3. Fill in any property whose definition carries a `default` and that
18
+ * the user didn't specify (including post-step-2 drops from unresolved
19
+ * inline references).
20
+ * 4. Hand the resolved POJO to the registered factory.
21
+ * 5. Cache the resulting `Connection` by name so subsequent lookups skip
22
+ * the whole pipeline.
23
+ *
24
+ * Warnings collected during steps 2–3 (unknown overlay source) are pushed
25
+ * into the shared `log` array — same array exposed as `MalloyConfig.log`.
26
+ * The log grows the first time a connection is looked up; callers that
27
+ * read `log` before any lookup won't see resolution warnings. That's an
28
+ * intentional consequence of deferred resolution: we don't pay for
29
+ * warnings on connections nobody asks about.
30
+ */
31
+ export declare function buildManagedLookup(compiledConnections: Record<string, ConfigDict>, overlays: ConfigOverlays, log: LogMessage[]): ManagedConnectionLookup;
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright Contributors to the Malloy project
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.buildManagedLookup = buildManagedLookup;
8
+ const registry_1 = require("../../connection/registry");
9
+ /**
10
+ * Build the `ManagedConnectionLookup` a `MalloyConfig` hands to Runtime.
11
+ *
12
+ * Reference resolution and property-default application run *per lookup*,
13
+ * asynchronously. This is the point where overlays are actually called —
14
+ * which means secret stores, session readers, and any other IO-backed
15
+ * overlay has a natural async seam to live in. Construction of the
16
+ * `MalloyConfig` stays synchronous and zero-IO.
17
+ *
18
+ * Lookup flow for a connection name:
19
+ * 1. Find its compiled entry. Throw if the name is unknown.
20
+ * 2. Walk the entry tree, `await`-ing each overlay reference.
21
+ * 3. Fill in any property whose definition carries a `default` and that
22
+ * the user didn't specify (including post-step-2 drops from unresolved
23
+ * inline references).
24
+ * 4. Hand the resolved POJO to the registered factory.
25
+ * 5. Cache the resulting `Connection` by name so subsequent lookups skip
26
+ * the whole pipeline.
27
+ *
28
+ * Warnings collected during steps 2–3 (unknown overlay source) are pushed
29
+ * into the shared `log` array — same array exposed as `MalloyConfig.log`.
30
+ * The log grows the first time a connection is looked up; callers that
31
+ * read `log` before any lookup won't see resolution warnings. That's an
32
+ * intentional consequence of deferred resolution: we don't pay for
33
+ * warnings on connections nobody asks about.
34
+ */
35
+ function buildManagedLookup(compiledConnections, overlays, log) {
36
+ const entries = Object.entries(compiledConnections);
37
+ const firstConnectionName = entries.length > 0 ? entries[0][0] : undefined;
38
+ const cache = new Map();
39
+ return {
40
+ async lookupConnection(connectionName) {
41
+ if (connectionName === undefined) {
42
+ connectionName = firstConnectionName;
43
+ }
44
+ if (connectionName === undefined) {
45
+ throw new Error('No connections defined in config');
46
+ }
47
+ const cached = cache.get(connectionName);
48
+ if (cached)
49
+ return cached;
50
+ const compiledEntry = compiledConnections[connectionName];
51
+ if (!compiledEntry) {
52
+ throw new Error(`No connection named "${connectionName}" found in config`);
53
+ }
54
+ const resolved = await resolveCompiledEntry(compiledEntry, overlays, log);
55
+ // compileConnections guarantees `is` is present and a string-valued
56
+ // literal node — resolveCompiledEntry preserves it. Defensive check
57
+ // in case a compiler bug sneaks through.
58
+ if (typeof resolved['is'] !== 'string') {
59
+ throw new Error(`Connection "${connectionName}" is missing a valid "is" field`);
60
+ }
61
+ const typeDef = (0, registry_1.getConnectionTypeDef)(resolved.is);
62
+ if (!typeDef) {
63
+ throw new Error(`No registered connection type "${resolved.is}" for connection "${connectionName}". ` +
64
+ 'Did you forget to import the connection package?');
65
+ }
66
+ const connConfig = { name: connectionName };
67
+ for (const [key, value] of Object.entries(resolved)) {
68
+ if (key === 'is')
69
+ continue;
70
+ if (value !== undefined && value !== null) {
71
+ connConfig[key] = value;
72
+ }
73
+ }
74
+ const connection = await typeDef.factory(connConfig);
75
+ cache.set(connectionName, connection);
76
+ return connection;
77
+ },
78
+ async close() {
79
+ const connections = [...cache.values()];
80
+ cache.clear();
81
+ for (const conn of connections) {
82
+ await conn.close();
83
+ }
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * Async walk of a single connection's compiled entry against the overlays,
89
+ * followed by property-default application. Returns the plain POJO that
90
+ * gets handed to the factory.
91
+ */
92
+ async function resolveCompiledEntry(entry, overlays, log) {
93
+ const resolved = (await resolveNode(entry, overlays, log));
94
+ await applyPropertyDefaults(resolved, overlays);
95
+ return resolved;
96
+ }
97
+ /**
98
+ * Walk a node, awaiting any overlay calls. References that fail to resolve
99
+ * return `undefined`; the parent dict walker drops the corresponding
100
+ * property. Unknown overlay sources push a warning to the log (case 1);
101
+ * overlays returning undefined are silently dropped (case 2).
102
+ */
103
+ async function resolveNode(node, overlays, log) {
104
+ switch (node.kind) {
105
+ case 'value':
106
+ return node.value;
107
+ case 'reference':
108
+ return resolveReference(node, overlays, log);
109
+ case 'dict': {
110
+ const out = {};
111
+ for (const [k, child] of Object.entries(node.entries)) {
112
+ const r = await resolveNode(child, overlays, log);
113
+ if (r !== undefined)
114
+ out[k] = r;
115
+ }
116
+ return out;
117
+ }
118
+ }
119
+ }
120
+ async function resolveReference(ref, overlays, log) {
121
+ const overlay = overlays[ref.source];
122
+ if (!overlay) {
123
+ // Case 1: unknown overlay source — warn and drop.
124
+ log.push({
125
+ message: `unknown overlay source "${ref.source}" for reference path ${JSON.stringify(ref.path)}`,
126
+ severity: 'warn',
127
+ code: 'config-overlay',
128
+ });
129
+ return undefined;
130
+ }
131
+ // Case 2: overlay returns undefined (sync or via a Promise) — silent drop.
132
+ // `await` tolerates both sync and Promise return types.
133
+ return await overlay(ref.path);
134
+ }
135
+ /**
136
+ * Fill in missing properties on a resolved entry from the registry's
137
+ * declared defaults. Reference-shaped defaults (like DuckDB's
138
+ * `workingDirectory: {config: 'rootDirectory'}`) resolve through the same
139
+ * overlays as inline refs. Unresolved defaults are silently dropped — a
140
+ * default is a hint, not a requirement (case 3).
141
+ *
142
+ * Interaction with inline references: if the user wrote a property as a
143
+ * reference that resolved to `undefined`, `resolveNode` already dropped it
144
+ * before we see the entry. From here the property looks absent, and the
145
+ * default applies — effectively "try the inline ref, else fall back to the
146
+ * default." Almost always what users want.
147
+ */
148
+ async function applyPropertyDefaults(entry, overlays) {
149
+ var _a;
150
+ const typeName = entry['is'];
151
+ if (typeof typeName !== 'string')
152
+ return;
153
+ const props = (_a = (0, registry_1.getConnectionProperties)(typeName)) !== null && _a !== void 0 ? _a : [];
154
+ for (const prop of props) {
155
+ if (prop.default === undefined)
156
+ continue;
157
+ if (entry[prop.name] !== undefined)
158
+ continue;
159
+ const v = await resolveDefault(prop.default, overlays);
160
+ if (v !== undefined)
161
+ entry[prop.name] = v;
162
+ }
163
+ }
164
+ async function resolveDefault(def, overlays) {
165
+ if (typeof def !== 'object')
166
+ return def;
167
+ const keys = Object.keys(def);
168
+ if (keys.length !== 1)
169
+ return undefined;
170
+ const source = keys[0];
171
+ const raw = def[source];
172
+ const path = typeof raw === 'string' ? [raw] : raw;
173
+ if (!Array.isArray(path))
174
+ return undefined;
175
+ const overlay = overlays[source];
176
+ if (!overlay)
177
+ return undefined;
178
+ return await overlay(path);
179
+ }
180
+ //# sourceMappingURL=config_lookup.js.map
@@ -2,13 +2,20 @@
2
2
  * An overlay is a lambda that takes a compiled reference path and returns
3
3
  * a value (or undefined if the path is not present in the overlay source).
4
4
  *
5
+ * Overlays may return synchronously or asynchronously — the return type is
6
+ * `unknown | Promise<unknown>`. The reference resolver awaits every overlay
7
+ * result, so a sync overlay and an async overlay are interchangeable from
8
+ * the caller's perspective. Use sync for purely in-memory values (env vars,
9
+ * host context dicts) and async for anything that touches IO (secret stores,
10
+ * session fetches, enterprise-managed injected values).
11
+ *
5
12
  * The path is an array because references can address nested data:
6
13
  * {env: "HOME"} -> path = ["HOME"]
7
14
  * {user: ["address", "zip"]} -> path = ["address", "zip"]
8
15
  *
9
16
  * Overlay values should resolve to ordinary JSON-compatible values.
10
17
  */
11
- export type Overlay = (path: string[]) => unknown;
18
+ export type Overlay = (path: string[]) => unknown | Promise<unknown>;
12
19
  /**
13
20
  * A dict keyed by overlay name. A config reference like
14
21
  * `{env: "PG_PASSWORD"}` is resolved by looking up `overlays["env"]` and
@@ -1,49 +1,39 @@
1
1
  import type { LogMessage } from '../../lang/parse-log';
2
- import type { ConnectionConfigEntry } from '../../connection/registry';
3
2
  import type { ConfigDict } from './config_compile';
4
- import type { ConfigOverlays } from './config_overlays';
5
3
  /**
6
- * The shape the class body consumes: a plain POJO with the same top-level
7
- * sections as the input, but with all references resolved and defaults
8
- * applied. This is fed into the connection registry to build connections.
4
+ * The synchronous slice of config preparation. What the `MalloyConfig`
5
+ * constructor needs *before* any overlay IO happens:
9
6
  *
10
- * `connections` uses the registry's `ConnectionConfigEntry` shape directly —
11
- * a resolved entry is precisely what the registry consumes, so there's no
12
- * reason to invent a local alias and cast across the boundary.
7
+ * - `compiledConnections` per-connection compiled subtrees. References
8
+ * inside these are resolved lazily at `lookupConnection` time, not here.
9
+ * Fabricated bare `{is: typeName}` entries are included when the POJO
10
+ * opts in via `includeDefaultConnections`.
11
+ * - `manifestPath` — raw string (never a reference; section compiler only
12
+ * produces value nodes).
13
+ * - `virtualMap` — raw literal POJO (same — virtualMap is a literal slot).
13
14
  *
14
- * Note: `includeDefaultConnections` is an input directive, not a resolved
15
- * value by the time `resolveConfig` returns, fabrication has already
16
- * happened. It intentionally does not appear on this interface.
15
+ * `includeDefaultConnections` is an input directive, not an output value —
16
+ * fabrication has already happened by the time this returns.
17
17
  */
18
- export interface ResolvedConfig {
19
- connections: Record<string, ConnectionConfigEntry>;
18
+ export interface PreparedConfig {
19
+ compiledConnections: Record<string, ConfigDict>;
20
20
  manifestPath?: string;
21
21
  virtualMap?: unknown;
22
22
  }
23
23
  /**
24
- * Walk a compiled tree against the overlay dict and produce a plain
25
- * resolved POJO.
24
+ * Synchronous top-level walk of a compiled config tree. Extracts the
25
+ * non-connection sections (which only contain literals — see the section
26
+ * compilers) and hands back the compiled connection subtrees untouched.
26
27
  *
27
- * Two distinct "defaults" mechanisms, deliberately separated:
28
+ * Reference resolution for connection properties is *not* done here. It is
29
+ * deferred until `lookupConnection` is called, at which point the async
30
+ * walker in `config_lookup.ts` can `await` overlays that do IO (secret
31
+ * stores, session fetches, etc.). This keeps `MalloyConfig` construction
32
+ * synchronous and zero-IO.
28
33
  *
29
- * 1. **Property defaults** (`applyPropertyDefaults`) fill in missing
30
- * properties on *every* connection entry, user-listed or fabricated.
31
- * This is a uniform per-property rule; there is no asymmetry between
32
- * explicit and auto-generated entries.
33
- *
34
- * 2. **`includeDefaultConnections`** (`fabricateMissingConnections`) —
35
- * fabricate a bare `{is: typeName}` entry for each registered backend
36
- * not already represented. Property defaults then fill in their
37
- * properties via (1).
38
- *
39
- * Order matters: fabrication runs before property defaults so that
40
- * fabricated entries pick up defaults in the same pass as user-listed
41
- * ones.
42
- *
43
- * Three unresolved-reference cases, each with different handling:
44
- * 1. Unknown overlay source → warning, drop property
45
- * 2. Known overlay → undefined → silent drop
46
- * 3. Property default → unresolved (either of the above inside a default)
47
- * → silent drop (a default is a hint, not a requirement)
34
+ * Fabrication of bare `{is: typeName}` compiled entries for registered
35
+ * backends not otherwise represented happens here when the config opts in
36
+ * via `includeDefaultConnections`. Property defaults are filled in at
37
+ * lookup time alongside reference resolution.
48
38
  */
49
- export declare function resolveConfig(compiled: ConfigDict, overlays: ConfigOverlays, log: LogMessage[]): ResolvedConfig;
39
+ export declare function prepareConfig(compiled: ConfigDict, _log: LogMessage[]): PreparedConfig;