@malloydata/malloy 0.0.431 → 0.0.432

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.
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.MalloyConfig = exports.Manifest = void 0;
8
8
  exports.isBuildManifestEntry = isBuildManifestEntry;
9
+ const registry_1 = require("../../connection/registry");
9
10
  const config_compile_1 = require("./config_compile");
10
11
  const config_resolve_1 = require("./config_resolve");
11
12
  const config_lookup_1 = require("./config_lookup");
@@ -165,7 +166,7 @@ class MalloyConfig {
165
166
  // it happens async at `lookupConnection` time, so overlays that touch
166
167
  // IO (secret stores, session reads) have a natural async seam.
167
168
  const prepared = (0, config_resolve_1.prepareConfig)(compiled.compiled, mergedOverlays, log);
168
- this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, mergedOverlays, log);
169
+ this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, rawConnectionEntries(pojo), mergedOverlays, log);
169
170
  this._connections = this._managedLookup;
170
171
  this._overlays = mergedOverlays;
171
172
  this.virtualMap = toVirtualMap(prepared.virtualMap, log);
@@ -476,6 +477,27 @@ function toVirtualMap(raw, log) {
476
477
  function isRecord(value) {
477
478
  return typeof value === 'object' && value !== null && !Array.isArray(value);
478
479
  }
480
+ /**
481
+ * The connection entries exactly as authored, kept alongside the compiled tree
482
+ * and handed to each factory as `rawConfigData`. Compilation replaces an
483
+ * overlay reference with a `ConfigReference` node and resolution replaces that
484
+ * with the value, so this is the only place a connector can still see *which*
485
+ * overlay a value came from — which is what BigQuery's `getDigest` needs to
486
+ * tell two impersonated identities apart.
487
+ *
488
+ * No validation here beyond the shape: entries the compiler rejected are
489
+ * absent from `compiledConnections` and so are never looked up.
490
+ */
491
+ function rawConnectionEntries(pojo) {
492
+ const entries = {};
493
+ if (!isRecord(pojo) || !isRecord(pojo['connections']))
494
+ return entries;
495
+ for (const [name, entry] of Object.entries(pojo['connections'])) {
496
+ if ((0, registry_1.isConnectionConfigEntry)(entry))
497
+ entries[name] = entry;
498
+ }
499
+ return entries;
500
+ }
479
501
  function isBuildManifestEntry(value) {
480
502
  return isRecord(value) && typeof value['tableName'] === 'string';
481
503
  }
@@ -3,7 +3,7 @@ import type { LogMessage } from '../../lang/parse-log';
3
3
  * A compiled config node. The compiler produces a tree of these; the resolver
4
4
  * walks the tree against a ConfigOverlays dict and produces a plain POJO.
5
5
  */
6
- export type ConfigNode = ConfigDict | ConfigLiteral | ConfigReference;
6
+ export type ConfigNode = ConfigDict | ConfigLiteral | ConfigReference | ConfigInvalid;
7
7
  export interface ConfigDict {
8
8
  kind: 'dict';
9
9
  entries: Record<string, ConfigNode>;
@@ -23,6 +23,17 @@ export interface ConfigReference {
23
23
  /** Path into the overlay. */
24
24
  path: string[];
25
25
  }
26
+ /**
27
+ * A property the user wrote that can never produce a value — it failed its
28
+ * type check. Compilation records it rather than dropping it so that a
29
+ * `mustHaveValue` property can tell "the user wrote something unusable" apart
30
+ * from "the user wrote nothing." It resolves to `undefined` like any other
31
+ * failure; the difference is only that the key is still visible in the
32
+ * compiled tree.
33
+ */
34
+ export interface ConfigInvalid {
35
+ kind: 'invalid';
36
+ }
26
37
  export type SectionCompiler = (value: unknown, log: LogMessage[]) => ConfigNode | undefined;
27
38
  /**
28
39
  * Compile a POJO into a typed dictionary tree, collecting validation warnings.
@@ -110,34 +110,49 @@ function compileConnectionEntry(prefix, typeName, rawEntry, log) {
110
110
  return entry;
111
111
  }
112
112
  /**
113
- * Compile a single connection property value. At non-`json` property slots,
114
- * a single-key object whose value is a string or string[] is recognized as
115
- * an overlay reference. `json`-typed slots always pass through as literal
116
- * data this is the security invariant that prevents reference injection
117
- * into structured config.
113
+ * Compile a single connection property value.
114
+ *
115
+ * At non-`json` property slots, a single-key object whose value is a string or
116
+ * string[] is recognized as an overlay reference. `json`-typed slots always
117
+ * pass through as literal data — this is the security invariant that prevents
118
+ * reference injection into structured config.
119
+ *
120
+ * A property's `source` overrides that reading in both directions. `'literal'`
121
+ * refuses a reference; `'overlay'` refuses a literal, which is also what lets
122
+ * a reference be recognized in a slot that would otherwise hold a dictionary.
118
123
  */
119
124
  function compileConnectionProperty(path, propDef, value, log) {
120
- if (value === undefined || value === null)
121
- return undefined;
122
- if (propDef.type === 'json') {
123
- return { kind: 'value', value };
125
+ if (value === undefined || value === null) {
126
+ return propDef.mustHaveValue ? { kind: 'invalid' } : undefined;
124
127
  }
125
128
  const ref = asReferenceShape(value);
126
- if (ref !== undefined) {
127
- if (propDef.requireLiteralString) {
128
- log.push(makeWarning(path, 'must be a literal string and cannot use an overlay reference'));
129
- return { kind: 'value', value };
130
- }
131
- return ref;
129
+ switch (propDef.source) {
130
+ case 'overlay':
131
+ if (ref === undefined) {
132
+ log.push(makeWarning(path, 'must name an overlay, as in {env: "NAME"}; a value written here ' +
133
+ 'directly is not allowed'));
134
+ return { kind: 'invalid' };
135
+ }
136
+ return ref;
137
+ case 'literal':
138
+ if (ref !== undefined) {
139
+ log.push(makeWarning(path, 'must be written here directly and cannot name an overlay'));
140
+ return { kind: 'invalid' };
141
+ }
142
+ break;
143
+ default:
144
+ if (propDef.type === 'json') {
145
+ return { kind: 'value', value };
146
+ }
147
+ if (ref !== undefined) {
148
+ return ref;
149
+ }
150
+ break;
132
151
  }
133
152
  const typeError = checkValueType(value, propDef.type);
134
153
  if (typeError) {
135
- if (propDef.requireLiteralString) {
136
- log.push(makeWarning(path, `must be a literal string, got ${describeConfigValue(value)}`));
137
- return { kind: 'value', value };
138
- }
139
154
  log.push(makeWarning(path, `${typeError} (expected ${propDef.type})`));
140
- return undefined;
155
+ return propDef.mustHaveValue ? { kind: 'invalid' } : undefined;
141
156
  }
142
157
  return { kind: 'value', value };
143
158
  }
@@ -221,11 +236,6 @@ function makeWarning(path, message) {
221
236
  code: 'config-validation',
222
237
  };
223
238
  }
224
- function describeConfigValue(value) {
225
- if (Array.isArray(value))
226
- return 'array';
227
- return typeof value;
228
- }
229
239
  function checkValueType(value, expectedType) {
230
240
  switch (expectedType) {
231
241
  case 'number':
@@ -245,6 +255,7 @@ function checkValueType(value, expectedType) {
245
255
  return `should be a string, got ${typeof value}`;
246
256
  break;
247
257
  case 'json':
258
+ case 'opaque':
248
259
  break;
249
260
  }
250
261
  return undefined;
@@ -1,5 +1,5 @@
1
1
  import type { LogMessage } from '../../lang/parse-log';
2
- import type { ManagedConnectionLookup } from '../../connection/registry';
2
+ import type { ConnectionConfigEntry, ManagedConnectionLookup } from '../../connection/registry';
3
3
  import type { ConfigDict } from './config_compile';
4
4
  import type { ConfigOverlays } from './config_overlays';
5
5
  /**
@@ -28,4 +28,4 @@ import type { ConfigOverlays } from './config_overlays';
28
28
  * intentional consequence of deferred resolution: we don't pay for
29
29
  * warnings on connections nobody asks about.
30
30
  */
31
- export declare function buildManagedLookup(compiledConnections: Record<string, ConfigDict>, overlays: ConfigOverlays, log: LogMessage[]): ManagedConnectionLookup;
31
+ export declare function buildManagedLookup(compiledConnections: Record<string, ConfigDict>, rawConnections: Record<string, ConnectionConfigEntry>, overlays: ConfigOverlays, log: LogMessage[]): ManagedConnectionLookup;
@@ -32,7 +32,7 @@ const registry_1 = require("../../connection/registry");
32
32
  * intentional consequence of deferred resolution: we don't pay for
33
33
  * warnings on connections nobody asks about.
34
34
  */
35
- function buildManagedLookup(compiledConnections, overlays, log) {
35
+ function buildManagedLookup(compiledConnections, rawConnections, overlays, log) {
36
36
  const entries = Object.entries(compiledConnections);
37
37
  const firstConnectionName = entries.length > 0 ? entries[0][0] : undefined;
38
38
  const cache = new Map();
@@ -51,7 +51,7 @@ function buildManagedLookup(compiledConnections, overlays, log) {
51
51
  if (!compiledEntry) {
52
52
  throw new Error(`No connection named "${connectionName}" found in config`);
53
53
  }
54
- const resolved = await resolveCompiledEntry(compiledEntry, overlays, log);
54
+ const resolved = await resolveCompiledEntry(connectionName, compiledEntry, overlays, log);
55
55
  // compileConnections guarantees `is` is present and a string-valued
56
56
  // literal node — resolveCompiledEntry preserves it. Defensive check
57
57
  // in case a compiler bug sneaks through.
@@ -71,8 +71,7 @@ function buildManagedLookup(compiledConnections, overlays, log) {
71
71
  connConfig[key] = value;
72
72
  }
73
73
  }
74
- (0, registry_1.validateConnectionConfigProperties)(connectionName, resolved.is, connConfig);
75
- const connection = await typeDef.factory(connConfig);
74
+ const connection = await typeDef.factory(connConfig, rawConnections[connectionName]);
76
75
  cache.set(connectionName, connection);
77
76
  return connection;
78
77
  },
@@ -97,7 +96,7 @@ function buildManagedLookup(compiledConnections, overlays, log) {
97
96
  * followed by property-default application. Returns the plain POJO that
98
97
  * gets handed to the factory.
99
98
  */
100
- async function resolveCompiledEntry(entry, overlays, log) {
99
+ async function resolveCompiledEntry(connectionName, entry, overlays, log) {
101
100
  const resolved = await resolveNode(entry, overlays, log);
102
101
  // resolveNode returns `unknown`. The compileConnections pipeline
103
102
  // guarantees every connection entry is an object dict with `is` set to
@@ -106,9 +105,35 @@ async function resolveCompiledEntry(entry, overlays, log) {
106
105
  if (!(0, registry_1.isConnectionConfigEntry)(resolved)) {
107
106
  throw new Error('Connection entry did not resolve to a valid {is: string, ...} dict');
108
107
  }
108
+ // Before defaults, so that a declared default can't quietly satisfy a
109
+ // promise the user's own value failed to keep.
110
+ requireAuthoredValues(connectionName, entry, resolved);
109
111
  await applyPropertyDefaults(resolved, overlays);
110
112
  return resolved;
111
113
  }
114
+ /**
115
+ * Enforce `mustHaveValue`: a property the user wrote must survive resolution.
116
+ *
117
+ * The compiled entry is the record of what was authored — a reference that
118
+ * resolved to nothing, or a value that failed its type check, is gone from
119
+ * `resolved` but still present here. Absence from both is fine; that's the
120
+ * user declining to set the property, which is what `optional` already means.
121
+ */
122
+ function requireAuthoredValues(connectionName, entry, resolved) {
123
+ var _a;
124
+ for (const prop of (_a = (0, registry_1.getConnectionProperties)(resolved.is)) !== null && _a !== void 0 ? _a : []) {
125
+ if (!prop.mustHaveValue)
126
+ continue;
127
+ if (entry.entries[prop.name] === undefined)
128
+ continue;
129
+ if (resolved[prop.name] !== undefined)
130
+ continue;
131
+ throw new Error(`Connection "${connectionName}" sets "${prop.name}", but no value ` +
132
+ 'arrived — an overlay reference that did not resolve, or a value that ' +
133
+ 'was rejected. Omitting the property is allowed; this one cannot fall ' +
134
+ 'back silently.');
135
+ }
136
+ }
112
137
  /**
113
138
  * Walk a node, awaiting any overlay calls. References that fail to resolve
114
139
  * return `undefined`; the parent dict walker drops the corresponding
@@ -119,6 +144,8 @@ async function resolveNode(node, overlays, log) {
119
144
  switch (node.kind) {
120
145
  case 'value':
121
146
  return node.value;
147
+ case 'invalid':
148
+ return undefined;
122
149
  case 'reference':
123
150
  return resolveReference(node, overlays, log);
124
151
  case 'dict': {
@@ -1,12 +1,29 @@
1
1
  import type { Connection, ConnectionConfig, LookupConnection } from './types';
2
2
  /**
3
3
  * A factory function that creates a Connection from a config object.
4
+ *
5
+ * `rawConfigData` is the connection's entry exactly as the user wrote it,
6
+ * before overlay references were resolved — so an overlay-supplied property
7
+ * still reads as `{tenantAuth: "acme"}` rather than as the value it produced.
8
+ * A connector needs it when a resolved value has no stable identity of its own
9
+ * but the connection's digest depends on which one it is; BigQuery's
10
+ * `authClient` is the case that exists today. It is undefined for hosts that
11
+ * build connections from an already-resolved config.
12
+ *
13
+ * Being unresolved, it is the *non-secret* twin of `config`: it holds
14
+ * `{env: "PGPASSWORD"}` where `config` holds the password.
4
15
  */
5
- export type ConnectionTypeFactory = (config: ConnectionConfig) => Promise<Connection>;
16
+ export type ConnectionTypeFactory = (config: ConnectionConfig, rawConfigData?: ConnectionConfigEntry) => Promise<Connection>;
6
17
  /**
7
18
  * The type of a connection property value.
8
19
  */
9
- export type ConnectionPropertyType = 'string' | 'number' | 'boolean' | 'password' | 'secret' | 'file' | 'json' | 'text';
20
+ export type ConnectionPropertyType = 'string' | 'number' | 'boolean' | 'password' | 'secret' | 'file' | 'json' | 'text'
21
+ /**
22
+ * A value with no declared shape that Malloy never inspects. Only reachable
23
+ * through an overlay — pair it with `source: 'overlay'`, since a config file
24
+ * has no way to write a live object down.
25
+ */
26
+ | 'opaque';
10
27
  /**
11
28
  * Describes a single configuration property for a connection type.
12
29
  *
@@ -35,12 +52,31 @@ export interface ConnectionPropertyDefinition {
35
52
  /** For type 'file': extension filters for picker dialogs. */
36
53
  fileFilters?: Record<string, string[]>;
37
54
  /**
38
- * For security-sensitive string slots, preserve malformed/reference-shaped
39
- * raw values so registry lookup can fail closed instead of silently dropping
40
- * the property during generic compilation. Factories must not rely on this
41
- * metadata as their only validation layer.
55
+ * Where this property's value is allowed to come from. Unset means either —
56
+ * a literal in the config file, or an overlay reference.
57
+ *
58
+ * `'literal'` refuses overlay references. For a property whose value is only
59
+ * meaningful if it was written down deliberately.
60
+ *
61
+ * `'overlay'` refuses literals: the config file can only *name* an overlay
62
+ * the host registered, never supply the value itself. This is also what
63
+ * makes a reference unambiguous in a slot that would otherwise hold a
64
+ * dictionary — if a literal isn't legal there, a single-key object can only
65
+ * be a reference.
42
66
  */
43
- requireLiteralString?: true;
67
+ source?: 'literal' | 'overlay';
68
+ /**
69
+ * Writing this property is a promise that it will produce a value. Omitting
70
+ * it is fine; setting it and having nothing survive resolution is an error
71
+ * rather than a silent drop.
72
+ *
73
+ * For properties whose absence triggers a fallback that isn't safe — DuckDB's
74
+ * `securityPolicy`, where absent means the `none` policy, or a credential
75
+ * whose absence means the ambient one. Without this, a misspelled overlay
76
+ * source or an unset environment variable removes the property and the
77
+ * connection is built as if the user had never asked for it.
78
+ */
79
+ mustHaveValue?: true;
44
80
  }
45
81
  /**
46
82
  * A connection type definition: factory plus property metadata.
@@ -106,11 +142,6 @@ export declare function getRegisteredConnectionTypes(): string[];
106
142
  * lookup to hand fully-resolved configs to the right factory.
107
143
  */
108
144
  export declare function getConnectionTypeDef(typeName: string): ConnectionTypeDef | undefined;
109
- /**
110
- * Enforce registry-level literal-string requirements after overlay resolution
111
- * and before a connection factory sees the config.
112
- */
113
- export declare function validateConnectionConfigProperties(connectionName: string, typeName: string, config: ConnectionConfig): void;
114
145
  /**
115
146
  * Parse a JSON config string into a ConnectionsConfig.
116
147
  * Entries without a valid `is` field are silently dropped.
@@ -10,7 +10,6 @@ exports.getConnectionProperties = getConnectionProperties;
10
10
  exports.getConnectionTypeDisplayName = getConnectionTypeDisplayName;
11
11
  exports.getRegisteredConnectionTypes = getRegisteredConnectionTypes;
12
12
  exports.getConnectionTypeDef = getConnectionTypeDef;
13
- exports.validateConnectionConfigProperties = validateConnectionConfigProperties;
14
13
  exports.readConnectionsConfig = readConnectionsConfig;
15
14
  exports.writeConnectionsConfig = writeConnectionsConfig;
16
15
  exports.createConnectionsFromConfig = createConnectionsFromConfig;
@@ -67,22 +66,6 @@ function getRegisteredConnectionTypes() {
67
66
  function getConnectionTypeDef(typeName) {
68
67
  return registry.get(typeName);
69
68
  }
70
- /**
71
- * Enforce registry-level literal-string requirements after overlay resolution
72
- * and before a connection factory sees the config.
73
- */
74
- function validateConnectionConfigProperties(connectionName, typeName, config) {
75
- var _a, _b;
76
- const props = (_b = (_a = registry.get(typeName)) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : [];
77
- for (const prop of props) {
78
- if (!prop.requireLiteralString)
79
- continue;
80
- const value = config[prop.name];
81
- if (value !== undefined && typeof value !== 'string') {
82
- throw new Error(`Connection "${connectionName}" property "${prop.name}" must be a literal string`);
83
- }
84
- }
85
- }
86
69
  /**
87
70
  * Parse a JSON config string into a ConnectionsConfig.
88
71
  * Entries without a valid `is` field are silently dropped.
@@ -144,7 +127,6 @@ function createConnectionsFromConfig(config, onConnectionCreated) {
144
127
  connConfig[key] = value;
145
128
  }
146
129
  }
147
- validateConnectionConfigProperties(connectionName, entry.is, connConfig);
148
130
  const connection = await typeDef.factory(connConfig);
149
131
  if (onConnectionCreated) {
150
132
  onConnectionCreated(connectionName, connection);
@@ -49,9 +49,15 @@ export interface InfoConnection {
49
49
  export type ConnectionParameterValue = string | number | boolean | null | Array<ConnectionParameterValue> | {
50
50
  [key: string]: ConnectionParameterValue;
51
51
  };
52
+ /**
53
+ * A value with no declared shape, supplied by a host overlay rather than
54
+ * written in a config file — an auth client, a session handle, anything live.
55
+ * Malloy never inspects one; it carries it from the overlay to the factory.
56
+ */
57
+ export type OpaqueConnectionValue = object;
52
58
  export interface ConnectionConfig {
53
59
  name: string;
54
- [key: string]: ConnectionParameterValue | undefined;
60
+ [key: string]: ConnectionParameterValue | OpaqueConnectionValue | undefined;
55
61
  }
56
62
  export interface ConnectionMetadata {
57
63
  url?: string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.431";
1
+ export declare const MALLOY_VERSION = "0.0.432";
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.431';
5
+ exports.MALLOY_VERSION = '0.0.432';
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.431",
3
+ "version": "0.0.432",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -51,9 +51,9 @@
51
51
  "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"
52
52
  },
53
53
  "dependencies": {
54
- "@malloydata/malloy-filter": "0.0.431",
55
- "@malloydata/malloy-interfaces": "0.0.431",
56
- "@malloydata/malloy-tag": "0.0.431",
54
+ "@malloydata/malloy-filter": "0.0.432",
55
+ "@malloydata/malloy-interfaces": "0.0.432",
56
+ "@malloydata/malloy-tag": "0.0.432",
57
57
  "@noble/hashes": "^1.8.0",
58
58
  "antlr4ts": "^0.5.0-alpha.4",
59
59
  "assert": "^2.0.0",