@malloydata/malloy 0.0.374 → 0.0.375

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;
@@ -4,227 +4,113 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.resolveConfig = resolveConfig;
7
+ exports.prepareConfig = prepareConfig;
8
8
  const registry_1 = require("../../connection/registry");
9
9
  /**
10
- * Walk a compiled tree against the overlay dict and produce a plain
11
- * resolved POJO.
10
+ * Synchronous top-level walk of a compiled config tree. Extracts the
11
+ * non-connection sections (which only contain literals — see the section
12
+ * compilers) and hands back the compiled connection subtrees untouched.
12
13
  *
13
- * Two distinct "defaults" mechanisms, deliberately separated:
14
+ * Reference resolution for connection properties is *not* done here. It is
15
+ * deferred until `lookupConnection` is called, at which point the async
16
+ * walker in `config_lookup.ts` can `await` overlays that do IO (secret
17
+ * stores, session fetches, etc.). This keeps `MalloyConfig` construction
18
+ * synchronous and zero-IO.
14
19
  *
15
- * 1. **Property defaults** (`applyPropertyDefaults`) fill in missing
16
- * properties on *every* connection entry, user-listed or fabricated.
17
- * This is a uniform per-property rule; there is no asymmetry between
18
- * explicit and auto-generated entries.
19
- *
20
- * 2. **`includeDefaultConnections`** (`fabricateMissingConnections`) —
21
- * fabricate a bare `{is: typeName}` entry for each registered backend
22
- * not already represented. Property defaults then fill in their
23
- * properties via (1).
24
- *
25
- * Order matters: fabrication runs before property defaults so that
26
- * fabricated entries pick up defaults in the same pass as user-listed
27
- * ones.
28
- *
29
- * Three unresolved-reference cases, each with different handling:
30
- * 1. Unknown overlay source → warning, drop property
31
- * 2. Known overlay → undefined → silent drop
32
- * 3. Property default → unresolved (either of the above inside a default)
33
- * → silent drop (a default is a hint, not a requirement)
20
+ * Fabrication of bare `{is: typeName}` compiled entries for registered
21
+ * backends not otherwise represented happens here when the config opts in
22
+ * via `includeDefaultConnections`. Property defaults are filled in at
23
+ * lookup time alongside reference resolution.
34
24
  */
35
- function resolveConfig(compiled, overlays, log) {
36
- const resolved = { connections: {} };
25
+ function prepareConfig(compiled, _log) {
26
+ let compiledConnections = {};
27
+ let manifestPath;
28
+ let virtualMap;
37
29
  let includeDefaultConnections = false;
38
30
  for (const [key, node] of Object.entries(compiled.entries)) {
39
31
  switch (key) {
40
32
  case 'connections': {
41
33
  if (node.kind !== 'dict')
42
34
  break;
43
- resolved.connections = resolveConnections(node, overlays, log);
35
+ compiledConnections = extractCompiledConnections(node);
44
36
  break;
45
37
  }
46
38
  case 'manifestPath': {
47
- const v = resolveNode(node, overlays, log);
48
- if (typeof v === 'string')
49
- resolved.manifestPath = v;
39
+ if (node.kind === 'value' && typeof node.value === 'string') {
40
+ manifestPath = node.value;
41
+ }
50
42
  break;
51
43
  }
52
44
  case 'virtualMap': {
53
- // virtualMap is literal datathe class body converts it to the
54
- // runtime Map-of-Maps representation.
55
- resolved.virtualMap = resolveNode(node, overlays, log);
45
+ // virtualMap is a literal dict slot compileVirtualMap never
46
+ // produces a reference node. MalloyConfig converts the raw POJO
47
+ // into its runtime Map-of-Maps representation.
48
+ if (node.kind === 'value')
49
+ virtualMap = node.value;
56
50
  break;
57
51
  }
58
52
  case 'includeDefaultConnections': {
59
- const v = resolveNode(node, overlays, log);
60
- if (typeof v === 'boolean')
61
- includeDefaultConnections = v;
53
+ if (node.kind === 'value' && typeof node.value === 'boolean') {
54
+ includeDefaultConnections = node.value;
55
+ }
62
56
  break;
63
57
  }
64
58
  }
65
59
  }
66
60
  if (includeDefaultConnections) {
67
- fabricateMissingConnections(resolved.connections);
61
+ fabricateMissingConnections(compiledConnections);
68
62
  }
69
- // Property defaults apply to every entry — user-listed and fabricated
70
- // alike. This is the fix for the earlier bug where defaults only fired
71
- // during fabrication, leaving explicit entries silently underconfigured.
72
- applyPropertyDefaults(resolved.connections, overlays);
73
- return resolved;
63
+ return { compiledConnections, manifestPath, virtualMap };
74
64
  }
75
- // =============================================================================
76
- // Generic walk
77
- // =============================================================================
78
65
  /**
79
- * Walk a single node and produce its resolved value. References that fail
80
- * to resolve return `undefined`; the parent dict walker then drops the
81
- * corresponding property.
66
+ * Pull each well-formed compiled connection entry out of the `connections`
67
+ * subtree. Entries are already validated by `compileConnections` anything
68
+ * shaped wrong was dropped or reported during compile. We defensively skip
69
+ * non-dict children here anyway.
82
70
  */
83
- function resolveNode(node, overlays, log) {
84
- switch (node.kind) {
85
- case 'value':
86
- return node.value;
87
- case 'reference':
88
- return resolveReference(node, overlays, log);
89
- case 'dict': {
90
- const out = {};
91
- for (const [k, child] of Object.entries(node.entries)) {
92
- const r = resolveNode(child, overlays, log);
93
- if (r !== undefined)
94
- out[k] = r;
95
- }
96
- return out;
97
- }
98
- }
99
- }
100
- function resolveReference(ref, overlays, log) {
101
- const overlay = overlays[ref.source];
102
- if (!overlay) {
103
- // Case 1: unknown overlay source — warn and drop.
104
- log.push({
105
- message: `unknown overlay source "${ref.source}" for reference path ${JSON.stringify(ref.path)}`,
106
- severity: 'warn',
107
- code: 'config-overlay',
108
- });
109
- return undefined;
110
- }
111
- // Case 2: overlay returns undefined — silent drop (no log push).
112
- return overlay(ref.path);
113
- }
114
- // =============================================================================
115
- // Connections
116
- // =============================================================================
117
- function resolveConnections(node, overlays, log) {
118
- const result = {};
119
- for (const [name, connNode] of Object.entries(node.entries)) {
120
- if (connNode.kind !== 'dict')
121
- continue;
122
- const resolved = resolveNode(connNode, overlays, log);
123
- // compileConnectionEntry guarantees `is` is a string value node, and
124
- // resolveNode preserves it. Any connection without `is` is a bug in the
125
- // compiler; skip it defensively.
126
- if (typeof resolved['is'] !== 'string')
127
- continue;
128
- result[name] = resolved;
71
+ function extractCompiledConnections(node) {
72
+ const out = {};
73
+ for (const [name, child] of Object.entries(node.entries)) {
74
+ if (child.kind === 'dict')
75
+ out[name] = child;
129
76
  }
130
- return result;
77
+ return out;
131
78
  }
132
- // =============================================================================
133
- // Fabrication and property defaults
134
- // =============================================================================
135
79
  /**
136
- * Fabricate a bare `{is: typeName}` entry for each registered connection
137
- * type not already represented in `connections`. Only runs when the
138
- * `includeDefaultConnections` flag is set on the config. Property values
139
- * are *not* filled in here that is the job of `applyPropertyDefaults`,
140
- * which runs unconditionally on every entry in a later pass.
80
+ * Add a bare `{is: typeName}` compiled entry for each registered connection
81
+ * type not already represented in `compiledConnections`. Only runs when the
82
+ * POJO sets `includeDefaultConnections: true`. Property values (including
83
+ * reference-shaped defaults like DuckDB's `{config: 'rootDirectory'}`) are
84
+ * *not* filled in here that is the job of the async lookup resolver.
141
85
  *
142
- * A user-named connection that happens to share the type name but points
143
- * at a different backend is left alone.
86
+ * Skip rules:
87
+ * - Type already in use: some existing entry has `is: typeName`.
88
+ * - Name already taken: some existing entry is *named* `typeName`, even
89
+ * if its `is` points elsewhere. This protects a user who writes
90
+ * `{duckdb: {is: 'postgres', ...}}` — naming an entry after a type but
91
+ * pointing at a different backend — from being clobbered.
144
92
  *
145
- * Mutates `connections` in place. Called only by `resolveConfig` on its
146
- * own freshly-built object.
93
+ * Mutates `compiledConnections` in place.
147
94
  */
148
- function fabricateMissingConnections(connections) {
95
+ function fabricateMissingConnections(compiledConnections) {
149
96
  const presentTypes = new Set();
150
- for (const entry of Object.values(connections)) {
151
- if (typeof entry.is === 'string')
152
- presentTypes.add(entry.is);
97
+ for (const entry of Object.values(compiledConnections)) {
98
+ const isNode = entry.entries['is'];
99
+ if ((isNode === null || isNode === void 0 ? void 0 : isNode.kind) === 'value' && typeof isNode.value === 'string') {
100
+ presentTypes.add(isNode.value);
101
+ }
153
102
  }
154
103
  for (const typeName of (0, registry_1.getRegisteredConnectionTypes)()) {
155
- // `presentTypes` catches {mydb: {is: 'duckdb'}}; the name check catches
156
- // {duckdb: {is: 'jsondb'}}. Both cases leave the registered `duckdb`
157
- // type alone — the first because it's already represented, the second
158
- // because we can't use the obvious name without clobbering.
159
104
  if (presentTypes.has(typeName))
160
105
  continue;
161
- if (connections[typeName])
106
+ if (compiledConnections[typeName])
162
107
  continue;
163
- connections[typeName] = { is: typeName };
108
+ compiledConnections[typeName] = {
109
+ kind: 'dict',
110
+ entries: {
111
+ is: { kind: 'value', value: typeName },
112
+ },
113
+ };
164
114
  }
165
115
  }
166
- /**
167
- * For every connection entry, fill in any property that the user didn't
168
- * specify and whose `ConnectionPropertyDefinition` declares a `default`.
169
- * Runs uniformly on both user-listed and fabricated entries — the earlier
170
- * behavior of only firing during fabrication was a bug that left explicit
171
- * entries silently underconfigured (e.g. a user-listed `duckdb` never
172
- * picked up `workingDirectory: {config: 'rootDirectory'}`).
173
- *
174
- * Defaults that are reference-shaped resolve through the overlays via
175
- * `resolveDefault`. Unresolved defaults are silently dropped (case 3).
176
- * User-specified values are never overwritten.
177
- *
178
- * Interaction with inline references: if the user specified a property
179
- * as a reference-shaped value that failed to resolve (e.g. `{env: 'UNSET'}`),
180
- * `resolveConnections` already dropped it before we see the entry. From
181
- * our perspective the property is simply absent, so the default applies —
182
- * effectively turning inline references into "try this first, else fall
183
- * back to the default." This is almost always what users want.
184
- *
185
- * Mutates `connections` in place. Called only by `resolveConfig` on its
186
- * own freshly-built object.
187
- */
188
- function applyPropertyDefaults(connections, overlays) {
189
- var _a;
190
- for (const entry of Object.values(connections)) {
191
- const typeName = entry.is;
192
- if (typeof typeName !== 'string')
193
- continue;
194
- const props = (_a = (0, registry_1.getConnectionProperties)(typeName)) !== null && _a !== void 0 ? _a : [];
195
- for (const prop of props) {
196
- if (prop.default === undefined)
197
- continue;
198
- if (entry[prop.name] !== undefined)
199
- continue;
200
- const v = resolveDefault(prop.default, overlays);
201
- if (v !== undefined)
202
- entry[prop.name] = v;
203
- }
204
- }
205
- }
206
- /**
207
- * Resolve a property `default` field. Literals pass through; single-key
208
- * reference-shaped objects are resolved through the overlays. Case 3:
209
- * an unresolved default is a hint, not a requirement — always silent drop.
210
- */
211
- function resolveDefault(def, overlays) {
212
- if (typeof def !== 'object')
213
- return def;
214
- const keys = Object.keys(def);
215
- if (keys.length !== 1)
216
- return undefined;
217
- const source = keys[0];
218
- const raw = def[source];
219
- const path = typeof raw === 'string' ? [raw] : raw;
220
- // The type says `raw` is `string | string[]`, but `default` comes from
221
- // registered backend definitions which are runtime-dynamic — a
222
- // malformed registration would blow up inside the overlay otherwise.
223
- if (!Array.isArray(path))
224
- return undefined;
225
- const overlay = overlays[source];
226
- if (!overlay)
227
- return undefined;
228
- return overlay(path);
229
- }
230
116
  //# sourceMappingURL=config_resolve.js.map
@@ -87,6 +87,12 @@ export declare function getConnectionTypeDisplayName(typeName: string): string |
87
87
  * Get the names of all registered connection types.
88
88
  */
89
89
  export declare function getRegisteredConnectionTypes(): string[];
90
+ /**
91
+ * Get the full definition (factory + properties + displayName) for a
92
+ * registered connection type. Used by the foundation layer's connection
93
+ * lookup to hand fully-resolved configs to the right factory.
94
+ */
95
+ export declare function getConnectionTypeDef(typeName: string): ConnectionTypeDef | undefined;
90
96
  /**
91
97
  * Parse a JSON config string into a ConnectionsConfig.
92
98
  * Entries without a valid `is` field are silently dropped.
@@ -9,6 +9,7 @@ exports.registerConnectionType = registerConnectionType;
9
9
  exports.getConnectionProperties = getConnectionProperties;
10
10
  exports.getConnectionTypeDisplayName = getConnectionTypeDisplayName;
11
11
  exports.getRegisteredConnectionTypes = getRegisteredConnectionTypes;
12
+ exports.getConnectionTypeDef = getConnectionTypeDef;
12
13
  exports.readConnectionsConfig = readConnectionsConfig;
13
14
  exports.writeConnectionsConfig = writeConnectionsConfig;
14
15
  exports.createConnectionsFromConfig = createConnectionsFromConfig;
@@ -57,6 +58,14 @@ function getConnectionTypeDisplayName(typeName) {
57
58
  function getRegisteredConnectionTypes() {
58
59
  return [...registry.keys()];
59
60
  }
61
+ /**
62
+ * Get the full definition (factory + properties + displayName) for a
63
+ * registered connection type. Used by the foundation layer's connection
64
+ * lookup to hand fully-resolved configs to the right factory.
65
+ */
66
+ function getConnectionTypeDef(typeName) {
67
+ return registry.get(typeName);
68
+ }
60
69
  /**
61
70
  * Parse a JSON config string into a ConnectionsConfig.
62
71
  * Entries without a valid `is` field are silently dropped.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.374";
1
+ export declare const MALLOY_VERSION = "0.0.375";
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MALLOY_VERSION = void 0;
4
4
  // generated with 'generate-version-file' script; do not edit manually
5
- exports.MALLOY_VERSION = '0.0.374';
5
+ exports.MALLOY_VERSION = '0.0.375';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.374",
3
+ "version": "0.0.375",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -47,9 +47,9 @@
47
47
  "generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
48
48
  },
49
49
  "dependencies": {
50
- "@malloydata/malloy-filter": "0.0.374",
51
- "@malloydata/malloy-interfaces": "0.0.374",
52
- "@malloydata/malloy-tag": "0.0.374",
50
+ "@malloydata/malloy-filter": "0.0.375",
51
+ "@malloydata/malloy-interfaces": "0.0.375",
52
+ "@malloydata/malloy-tag": "0.0.375",
53
53
  "@noble/hashes": "^1.8.0",
54
54
  "antlr4ts": "^0.5.0-alpha.4",
55
55
  "assert": "^2.0.0",