@malloydata/malloy 0.0.372 → 0.0.374
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.
- package/dist/api/foundation/config.d.ts +109 -108
- package/dist/api/foundation/config.js +205 -364
- package/dist/api/foundation/config_compile.d.ts +34 -0
- package/dist/api/foundation/config_compile.js +247 -0
- package/dist/api/foundation/config_discover.d.ts +37 -0
- package/dist/api/foundation/config_discover.js +133 -0
- package/dist/api/foundation/config_overlays.d.ts +54 -0
- package/dist/api/foundation/config_overlays.js +51 -0
- package/dist/api/foundation/config_resolve.d.ts +49 -0
- package/dist/api/foundation/config_resolve.js +230 -0
- package/dist/api/foundation/index.d.ts +3 -0
- package/dist/api/foundation/index.js +7 -1
- package/dist/api/foundation/runtime.d.ts +54 -9
- package/dist/api/foundation/runtime.js +98 -14
- package/dist/connection/registry.d.ts +14 -23
- package/dist/connection/registry.js +5 -30
- package/dist/index.d.ts +4 -3
- package/dist/index.js +5 -3
- package/dist/model/malloy_types.d.ts +8 -0
- package/dist/model/query_query.js +4 -1
- package/dist/model/sql_compiled.js +4 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LogMessage } from '../../lang/parse-log';
|
|
2
|
+
/**
|
|
3
|
+
* A compiled config node. The compiler produces a tree of these; the resolver
|
|
4
|
+
* walks the tree against a ConfigOverlays dict and produces a plain POJO.
|
|
5
|
+
*/
|
|
6
|
+
export type ConfigNode = ConfigDict | ConfigLiteral | ConfigReference;
|
|
7
|
+
export interface ConfigDict {
|
|
8
|
+
kind: 'dict';
|
|
9
|
+
entries: Record<string, ConfigNode>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A leaf node holding a literal — primitives for typed slots, arbitrary
|
|
13
|
+
* JSON for `json`-typed slots (no reference expansion, no type checking).
|
|
14
|
+
*/
|
|
15
|
+
export interface ConfigLiteral {
|
|
16
|
+
kind: 'value';
|
|
17
|
+
value: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface ConfigReference {
|
|
20
|
+
kind: 'reference';
|
|
21
|
+
/** Overlay name: "env", "config", "session", etc. */
|
|
22
|
+
source: string;
|
|
23
|
+
/** Path into the overlay. */
|
|
24
|
+
path: string[];
|
|
25
|
+
}
|
|
26
|
+
export type SectionCompiler = (value: unknown, log: LogMessage[]) => ConfigNode | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Compile a POJO into a typed dictionary tree, collecting validation warnings.
|
|
29
|
+
* Does not throw — caller inspects `log` for issues.
|
|
30
|
+
*/
|
|
31
|
+
export declare function compileConfig(pojo: unknown): {
|
|
32
|
+
compiled: ConfigDict;
|
|
33
|
+
log: LogMessage[];
|
|
34
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
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.compileConfig = compileConfig;
|
|
8
|
+
const registry_1 = require("../../connection/registry");
|
|
9
|
+
const TOP_LEVEL_SECTIONS = {
|
|
10
|
+
connections: compileConnections,
|
|
11
|
+
manifestPath: compileManifestPath,
|
|
12
|
+
virtualMap: compileVirtualMap,
|
|
13
|
+
includeDefaultConnections: compileIncludeDefaultConnections,
|
|
14
|
+
};
|
|
15
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(TOP_LEVEL_SECTIONS));
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Entry point
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Compile a POJO into a typed dictionary tree, collecting validation warnings.
|
|
21
|
+
* Does not throw — caller inspects `log` for issues.
|
|
22
|
+
*/
|
|
23
|
+
function compileConfig(pojo) {
|
|
24
|
+
const log = [];
|
|
25
|
+
const compiled = { kind: 'dict', entries: {} };
|
|
26
|
+
if (!isRecord(pojo)) {
|
|
27
|
+
log.push({
|
|
28
|
+
message: 'config is not a JSON object',
|
|
29
|
+
severity: 'error',
|
|
30
|
+
code: 'config-validation',
|
|
31
|
+
});
|
|
32
|
+
return { compiled, log };
|
|
33
|
+
}
|
|
34
|
+
for (const [key, value] of Object.entries(pojo)) {
|
|
35
|
+
const compiler = TOP_LEVEL_SECTIONS[key];
|
|
36
|
+
if (!compiler) {
|
|
37
|
+
const suggestion = closestMatch(key, [...KNOWN_TOP_LEVEL_KEYS]);
|
|
38
|
+
const hint = suggestion ? `. Did you mean "${suggestion}"?` : '';
|
|
39
|
+
log.push(makeWarning(key, `unknown config key "${key}"${hint}`));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const node = compiler(value, log);
|
|
43
|
+
if (node !== undefined) {
|
|
44
|
+
compiled.entries[key] = node;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { compiled, log };
|
|
48
|
+
}
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// connections
|
|
51
|
+
// =============================================================================
|
|
52
|
+
function compileConnections(value, log) {
|
|
53
|
+
if (!isRecord(value)) {
|
|
54
|
+
log.push(makeWarning('connections', '"connections" should be an object'));
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const registeredTypes = new Set((0, registry_1.getRegisteredConnectionTypes)());
|
|
58
|
+
const connections = { kind: 'dict', entries: {} };
|
|
59
|
+
for (const [name, rawEntry] of Object.entries(value)) {
|
|
60
|
+
const prefix = `connections.${name}`;
|
|
61
|
+
if (!isRecord(rawEntry)) {
|
|
62
|
+
log.push(makeWarning(prefix, 'should be an object'));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const is = rawEntry['is'];
|
|
66
|
+
if (is === undefined || is === null || is === '') {
|
|
67
|
+
log.push(makeWarning(prefix, 'missing required "is" field (connection type)'));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (typeof is !== 'string') {
|
|
71
|
+
log.push(makeWarning(`${prefix}.is`, '"is" should be a string'));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!registeredTypes.has(is)) {
|
|
75
|
+
const suggestion = closestMatch(is, [...registeredTypes]);
|
|
76
|
+
const hint = suggestion ? ` Did you mean "${suggestion}"?` : '';
|
|
77
|
+
log.push(makeWarning(`${prefix}.is`, `unknown connection type "${is}".${hint} Available types: ${[...registeredTypes].join(', ')}`));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const entry = compileConnectionEntry(prefix, is, rawEntry, log);
|
|
81
|
+
connections.entries[name] = entry;
|
|
82
|
+
}
|
|
83
|
+
return connections;
|
|
84
|
+
}
|
|
85
|
+
function compileConnectionEntry(prefix, typeName, rawEntry, log) {
|
|
86
|
+
var _a;
|
|
87
|
+
const props = (_a = (0, registry_1.getConnectionProperties)(typeName)) !== null && _a !== void 0 ? _a : [];
|
|
88
|
+
const propMap = new Map(props.map(p => [p.name, p]));
|
|
89
|
+
const entry = { kind: 'dict', entries: {} };
|
|
90
|
+
// Record the connection type as a plain value.
|
|
91
|
+
entry.entries['is'] = { kind: 'value', value: typeName };
|
|
92
|
+
for (const [key, value] of Object.entries(rawEntry)) {
|
|
93
|
+
if (key === 'is')
|
|
94
|
+
continue;
|
|
95
|
+
const propDef = propMap.get(key);
|
|
96
|
+
if (!propDef) {
|
|
97
|
+
const suggestion = closestMatch(key, [...propMap.keys()]);
|
|
98
|
+
const hint = suggestion ? `. Did you mean "${suggestion}"?` : '';
|
|
99
|
+
log.push(makeWarning(`${prefix}.${key}`, `unknown property "${key}" for connection type "${typeName}"${hint}`));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const node = compileConnectionProperty(`${prefix}.${key}`, propDef, value, log);
|
|
103
|
+
if (node !== undefined) {
|
|
104
|
+
entry.entries[key] = node;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compile a single connection property value. At non-`json` property slots,
|
|
111
|
+
* a single-key object whose value is a string or string[] is recognized as
|
|
112
|
+
* an overlay reference. `json`-typed slots always pass through as literal
|
|
113
|
+
* data — this is the security invariant that prevents reference injection
|
|
114
|
+
* into structured config.
|
|
115
|
+
*/
|
|
116
|
+
function compileConnectionProperty(path, propDef, value, log) {
|
|
117
|
+
if (value === undefined || value === null)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (propDef.type === 'json') {
|
|
120
|
+
return { kind: 'value', value };
|
|
121
|
+
}
|
|
122
|
+
const ref = asReferenceShape(value);
|
|
123
|
+
if (ref !== undefined) {
|
|
124
|
+
return ref;
|
|
125
|
+
}
|
|
126
|
+
const typeError = checkValueType(value, propDef.type);
|
|
127
|
+
if (typeError) {
|
|
128
|
+
log.push(makeWarning(path, `${typeError} (expected ${propDef.type})`));
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return { kind: 'value', value };
|
|
132
|
+
}
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// Pass-through sections
|
|
135
|
+
// =============================================================================
|
|
136
|
+
function compileManifestPath(value, log) {
|
|
137
|
+
if (typeof value !== 'string') {
|
|
138
|
+
log.push(makeWarning('manifestPath', '"manifestPath" should be a string'));
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return { kind: 'value', value };
|
|
142
|
+
}
|
|
143
|
+
function compileVirtualMap(value, _log) {
|
|
144
|
+
// virtualMap is a literal dict slot — no reference expansion, even if
|
|
145
|
+
// entries happen to look reference-shaped. The resolver will convert the
|
|
146
|
+
// plain structure into the runtime `VirtualMap` representation.
|
|
147
|
+
return { kind: 'value', value };
|
|
148
|
+
}
|
|
149
|
+
function compileIncludeDefaultConnections(value, _log) {
|
|
150
|
+
return { kind: 'value', value };
|
|
151
|
+
}
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// Reference-shape detection
|
|
154
|
+
// =============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Inspect a raw POJO value for the "overlay reference" shape: a single-key
|
|
157
|
+
* object whose value is a string (scalar path) or string[] (nested path).
|
|
158
|
+
* Returns a ConfigReference node if it matches, otherwise undefined.
|
|
159
|
+
*
|
|
160
|
+
* The single-key constraint distinguishes references from literal objects.
|
|
161
|
+
* This is a runtime invariant — TypeScript can't express "exactly one key".
|
|
162
|
+
*/
|
|
163
|
+
function asReferenceShape(value) {
|
|
164
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const keys = Object.keys(value);
|
|
168
|
+
if (keys.length !== 1)
|
|
169
|
+
return undefined;
|
|
170
|
+
const source = keys[0];
|
|
171
|
+
const inner = value[source];
|
|
172
|
+
if (typeof inner === 'string') {
|
|
173
|
+
return { kind: 'reference', source, path: [inner] };
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(inner) && inner.every(x => typeof x === 'string')) {
|
|
176
|
+
return { kind: 'reference', source, path: inner };
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// Helpers
|
|
182
|
+
// =============================================================================
|
|
183
|
+
function isRecord(value) {
|
|
184
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
185
|
+
}
|
|
186
|
+
function makeWarning(path, message) {
|
|
187
|
+
return {
|
|
188
|
+
message: `${path}: ${message}`,
|
|
189
|
+
severity: 'warn',
|
|
190
|
+
code: 'config-validation',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function checkValueType(value, expectedType) {
|
|
194
|
+
switch (expectedType) {
|
|
195
|
+
case 'number':
|
|
196
|
+
if (typeof value !== 'number')
|
|
197
|
+
return `should be a number, got ${typeof value}`;
|
|
198
|
+
break;
|
|
199
|
+
case 'boolean':
|
|
200
|
+
if (typeof value !== 'boolean')
|
|
201
|
+
return `should be a boolean, got ${typeof value}`;
|
|
202
|
+
break;
|
|
203
|
+
case 'string':
|
|
204
|
+
case 'password':
|
|
205
|
+
case 'secret':
|
|
206
|
+
case 'file':
|
|
207
|
+
case 'text':
|
|
208
|
+
if (typeof value !== 'string')
|
|
209
|
+
return `should be a string, got ${typeof value}`;
|
|
210
|
+
break;
|
|
211
|
+
case 'json':
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
function levenshtein(a, b) {
|
|
217
|
+
const m = a.length;
|
|
218
|
+
const n = b.length;
|
|
219
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
220
|
+
for (let i = 0; i <= m; i++)
|
|
221
|
+
dp[i][0] = i;
|
|
222
|
+
for (let j = 0; j <= n; j++)
|
|
223
|
+
dp[0][j] = j;
|
|
224
|
+
for (let i = 1; i <= m; i++) {
|
|
225
|
+
for (let j = 1; j <= n; j++) {
|
|
226
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
227
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return dp[m][n];
|
|
231
|
+
}
|
|
232
|
+
function closestMatch(input, candidates) {
|
|
233
|
+
if (candidates.length === 0)
|
|
234
|
+
return undefined;
|
|
235
|
+
let best = candidates[0];
|
|
236
|
+
let bestDist = Infinity;
|
|
237
|
+
for (const c of candidates) {
|
|
238
|
+
const dist = levenshtein(input.toLowerCase(), c.toLowerCase());
|
|
239
|
+
if (dist < bestDist) {
|
|
240
|
+
bestDist = dist;
|
|
241
|
+
best = c;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const maxDist = Math.max(1, Math.floor(Math.max(input.length, best.length) / 3));
|
|
245
|
+
return bestDist <= maxDist ? best : undefined;
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=config_compile.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { URLReader } from '../../runtime_types';
|
|
2
|
+
import { MalloyConfig } from './config';
|
|
3
|
+
import type { ConfigOverlays } from './config_overlays';
|
|
4
|
+
/**
|
|
5
|
+
* Walk upward from `startURL` toward `ceilingURL`, looking for a
|
|
6
|
+
* `malloy-config.json` (or `malloy-config-local.json`) at each directory
|
|
7
|
+
* level. On a hit, build a `MalloyConfig` from the parsed POJO with a
|
|
8
|
+
* `config` overlay carrying `rootDirectory` (the ceiling) and `configURL`
|
|
9
|
+
* (the actual location of the file that matched). Returns `null` if no
|
|
10
|
+
* config file is found between `startURL` and `ceilingURL` (inclusive).
|
|
11
|
+
*
|
|
12
|
+
* `ceilingURL` is the host-supplied project root — discovery stops at that
|
|
13
|
+
* level, and it is what `config.rootDirectory` binds to in the typical
|
|
14
|
+
* overlay wiring. The matched config file's actual location rides on
|
|
15
|
+
* `config.configURL` so that `MalloyConfig.manifestURL` can resolve
|
|
16
|
+
* `MANIFESTS/malloy-manifest.json` relative to the file the config came
|
|
17
|
+
* from (not the project root).
|
|
18
|
+
*
|
|
19
|
+
* Callers can inspect what discovery found via `readOverlay` on the
|
|
20
|
+
* returned config:
|
|
21
|
+
*
|
|
22
|
+
* const config = await discoverConfig(startURL, ceilingURL, urlReader);
|
|
23
|
+
* config?.readOverlay('config', 'rootDirectory') // the ceiling URL
|
|
24
|
+
* config?.readOverlay('config', 'configURL') // the matched file URL
|
|
25
|
+
*
|
|
26
|
+
* Hosts that want to layer additional overlays on top of discovery's
|
|
27
|
+
* `config` overlay (e.g. a `session` overlay for per-request data) pass
|
|
28
|
+
* them via `extraOverlays`. The merge is plain object-spread: extras with
|
|
29
|
+
* the same key as discovery's entries replace them wholesale. In
|
|
30
|
+
* particular, passing `{config: myConfigOverlay}` will clobber the
|
|
31
|
+
* `rootDirectory` + `configURL` discovery built — callers who want to
|
|
32
|
+
* extend discovery's `config` should read the keys back off and re-include
|
|
33
|
+
* them, or skip `discoverConfig` and build `MalloyConfig` directly.
|
|
34
|
+
*
|
|
35
|
+
* URL-based, so it works in browser-safe environments through `URLReader`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function discoverConfig(startURL: URL, ceilingURL: URL, urlReader: URLReader, extraOverlays?: ConfigOverlays): Promise<MalloyConfig | null>;
|
|
@@ -0,0 +1,133 @@
|
|
|
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.discoverConfig = discoverConfig;
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
const config_overlays_1 = require("./config_overlays");
|
|
10
|
+
const SHARED_FILENAME = 'malloy-config.json';
|
|
11
|
+
const LOCAL_FILENAME = 'malloy-config-local.json';
|
|
12
|
+
/**
|
|
13
|
+
* Walk upward from `startURL` toward `ceilingURL`, looking for a
|
|
14
|
+
* `malloy-config.json` (or `malloy-config-local.json`) at each directory
|
|
15
|
+
* level. On a hit, build a `MalloyConfig` from the parsed POJO with a
|
|
16
|
+
* `config` overlay carrying `rootDirectory` (the ceiling) and `configURL`
|
|
17
|
+
* (the actual location of the file that matched). Returns `null` if no
|
|
18
|
+
* config file is found between `startURL` and `ceilingURL` (inclusive).
|
|
19
|
+
*
|
|
20
|
+
* `ceilingURL` is the host-supplied project root — discovery stops at that
|
|
21
|
+
* level, and it is what `config.rootDirectory` binds to in the typical
|
|
22
|
+
* overlay wiring. The matched config file's actual location rides on
|
|
23
|
+
* `config.configURL` so that `MalloyConfig.manifestURL` can resolve
|
|
24
|
+
* `MANIFESTS/malloy-manifest.json` relative to the file the config came
|
|
25
|
+
* from (not the project root).
|
|
26
|
+
*
|
|
27
|
+
* Callers can inspect what discovery found via `readOverlay` on the
|
|
28
|
+
* returned config:
|
|
29
|
+
*
|
|
30
|
+
* const config = await discoverConfig(startURL, ceilingURL, urlReader);
|
|
31
|
+
* config?.readOverlay('config', 'rootDirectory') // the ceiling URL
|
|
32
|
+
* config?.readOverlay('config', 'configURL') // the matched file URL
|
|
33
|
+
*
|
|
34
|
+
* Hosts that want to layer additional overlays on top of discovery's
|
|
35
|
+
* `config` overlay (e.g. a `session` overlay for per-request data) pass
|
|
36
|
+
* them via `extraOverlays`. The merge is plain object-spread: extras with
|
|
37
|
+
* the same key as discovery's entries replace them wholesale. In
|
|
38
|
+
* particular, passing `{config: myConfigOverlay}` will clobber the
|
|
39
|
+
* `rootDirectory` + `configURL` discovery built — callers who want to
|
|
40
|
+
* extend discovery's `config` should read the keys back off and re-include
|
|
41
|
+
* them, or skip `discoverConfig` and build `MalloyConfig` directly.
|
|
42
|
+
*
|
|
43
|
+
* URL-based, so it works in browser-safe environments through `URLReader`.
|
|
44
|
+
*/
|
|
45
|
+
async function discoverConfig(startURL, ceilingURL, urlReader, extraOverlays) {
|
|
46
|
+
// Normalize both to directory form.
|
|
47
|
+
let current = new URL('.', startURL);
|
|
48
|
+
const ceiling = new URL('.', ceilingURL);
|
|
49
|
+
// If we're not under (or at) the ceiling, there's nothing meaningful to
|
|
50
|
+
// walk — bail.
|
|
51
|
+
if (!isWithinOrEqual(current, ceiling))
|
|
52
|
+
return null;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const found = await tryReadAtLevel(current, urlReader);
|
|
55
|
+
if (found)
|
|
56
|
+
return buildConfig(found, ceilingURL, extraOverlays);
|
|
57
|
+
if (current.toString() === ceiling.toString())
|
|
58
|
+
break;
|
|
59
|
+
const parent = new URL('..', current);
|
|
60
|
+
// URL scheme roots (`file:///`, `http://host/`) return themselves when
|
|
61
|
+
// you ask for their parent. Guard against the loop.
|
|
62
|
+
if (parent.toString() === current.toString())
|
|
63
|
+
break;
|
|
64
|
+
current = parent;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function buildConfig(hit, ceilingURL, extraOverlays) {
|
|
69
|
+
const discoveryOverlays = {
|
|
70
|
+
config: (0, config_overlays_1.contextOverlay)({
|
|
71
|
+
rootDirectory: ceilingURL.toString(),
|
|
72
|
+
configURL: hit.configURL.toString(),
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
const merged = { ...discoveryOverlays, ...extraOverlays };
|
|
76
|
+
return new config_1.MalloyConfig(hit.pojo, merged);
|
|
77
|
+
}
|
|
78
|
+
async function tryReadAtLevel(dirURL, urlReader) {
|
|
79
|
+
const sharedURL = new URL(SHARED_FILENAME, dirURL);
|
|
80
|
+
const localURL = new URL(LOCAL_FILENAME, dirURL);
|
|
81
|
+
const [sharedPOJO, localPOJO] = await Promise.all([
|
|
82
|
+
tryReadJSON(sharedURL, urlReader),
|
|
83
|
+
tryReadJSON(localURL, urlReader),
|
|
84
|
+
]);
|
|
85
|
+
// Local supersedes shared entirely when both exist — no merging of any
|
|
86
|
+
// kind. The person writing a local file is responsible for including
|
|
87
|
+
// everything they need.
|
|
88
|
+
if (localPOJO)
|
|
89
|
+
return { configURL: localURL, pojo: localPOJO };
|
|
90
|
+
if (sharedPOJO)
|
|
91
|
+
return { configURL: sharedURL, pojo: sharedPOJO };
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Try to read and parse a config file at `url`.
|
|
96
|
+
*
|
|
97
|
+
* Two failure modes are distinguished:
|
|
98
|
+
* - URL read fails (file not present) → returns `null`, signalling the
|
|
99
|
+
* walker to keep going up.
|
|
100
|
+
* - Read succeeds but contents are unparseable or not a JSON object →
|
|
101
|
+
* throws. A matched-but-broken config file is a hard error; silently
|
|
102
|
+
* skipping it would hide user mistakes and quietly fall through to a
|
|
103
|
+
* grandparent config.
|
|
104
|
+
*/
|
|
105
|
+
async function tryReadJSON(url, urlReader) {
|
|
106
|
+
let contents;
|
|
107
|
+
try {
|
|
108
|
+
const result = await urlReader.readURL(url);
|
|
109
|
+
contents = typeof result === 'string' ? result : result.contents;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(contents);
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
120
|
+
throw new Error(`Malformed JSON in ${url.toString()}: ${msg}`);
|
|
121
|
+
}
|
|
122
|
+
if (!isRecord(parsed)) {
|
|
123
|
+
throw new Error(`Config file ${url.toString()} must contain a JSON object at the top level`);
|
|
124
|
+
}
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
function isRecord(value) {
|
|
128
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
129
|
+
}
|
|
130
|
+
function isWithinOrEqual(child, ancestor) {
|
|
131
|
+
return child.toString().startsWith(ancestor.toString());
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=config_discover.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An overlay is a lambda that takes a compiled reference path and returns
|
|
3
|
+
* a value (or undefined if the path is not present in the overlay source).
|
|
4
|
+
*
|
|
5
|
+
* The path is an array because references can address nested data:
|
|
6
|
+
* {env: "HOME"} -> path = ["HOME"]
|
|
7
|
+
* {user: ["address", "zip"]} -> path = ["address", "zip"]
|
|
8
|
+
*
|
|
9
|
+
* Overlay values should resolve to ordinary JSON-compatible values.
|
|
10
|
+
*/
|
|
11
|
+
export type Overlay = (path: string[]) => unknown;
|
|
12
|
+
/**
|
|
13
|
+
* A dict keyed by overlay name. A config reference like
|
|
14
|
+
* `{env: "PG_PASSWORD"}` is resolved by looking up `overlays["env"]` and
|
|
15
|
+
* calling it with `["PG_PASSWORD"]`.
|
|
16
|
+
*
|
|
17
|
+
* Hosts provide additional overlays by passing a partial dict to the
|
|
18
|
+
* `MalloyConfig` constructor. The constructor merges it onto the default
|
|
19
|
+
* overlays by plain object spread, giving three behaviors:
|
|
20
|
+
*
|
|
21
|
+
* - Add: {session: sessionFn} — env and config stay, session added
|
|
22
|
+
* - Replace: {config: myConfigFn} — host replaces the default config
|
|
23
|
+
* - Disable: {config: () => undefined} — refs to {config: ...} drop
|
|
24
|
+
*/
|
|
25
|
+
export type ConfigOverlays = Record<string, Overlay>;
|
|
26
|
+
/**
|
|
27
|
+
* Built-in `env` overlay: reads `process.env[path[0]]`.
|
|
28
|
+
*
|
|
29
|
+
* Hardened against missing `process` so `defaultConfigOverlays()` is safe
|
|
30
|
+
* to call from any runtime. In the browser or a worker with no process.env,
|
|
31
|
+
* `{env: "X"}` references resolve to "not present" (property dropped),
|
|
32
|
+
* symmetric with the no-op default `config` overlay. Browser hosts can
|
|
33
|
+
* still swap in their own `env` overlay if they want a different source.
|
|
34
|
+
*/
|
|
35
|
+
export declare function envOverlay(): Overlay;
|
|
36
|
+
/**
|
|
37
|
+
* Helper that turns a plain dict into an overlay lambda. Used by hosts
|
|
38
|
+
* (and discovery wiring) to build a populated `config` overlay.
|
|
39
|
+
*
|
|
40
|
+
* const overlays = {
|
|
41
|
+
* config: contextOverlay({
|
|
42
|
+
* rootDirectory: ceilingURL,
|
|
43
|
+
* configURL: configURL,
|
|
44
|
+
* }),
|
|
45
|
+
* };
|
|
46
|
+
*/
|
|
47
|
+
export declare function contextOverlay(dict: Record<string, unknown>): Overlay;
|
|
48
|
+
/**
|
|
49
|
+
* The default config overlays: `env` wired to process.env, and a no-op
|
|
50
|
+
* `config` overlay. Hosts that want discovery-populated context replace
|
|
51
|
+
* the `config` entry; soloists leave it alone and `{config: ...}` refs
|
|
52
|
+
* resolve to "not present".
|
|
53
|
+
*/
|
|
54
|
+
export declare function defaultConfigOverlays(): ConfigOverlays;
|
|
@@ -0,0 +1,51 @@
|
|
|
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.envOverlay = envOverlay;
|
|
8
|
+
exports.contextOverlay = contextOverlay;
|
|
9
|
+
exports.defaultConfigOverlays = defaultConfigOverlays;
|
|
10
|
+
/**
|
|
11
|
+
* Built-in `env` overlay: reads `process.env[path[0]]`.
|
|
12
|
+
*
|
|
13
|
+
* Hardened against missing `process` so `defaultConfigOverlays()` is safe
|
|
14
|
+
* to call from any runtime. In the browser or a worker with no process.env,
|
|
15
|
+
* `{env: "X"}` references resolve to "not present" (property dropped),
|
|
16
|
+
* symmetric with the no-op default `config` overlay. Browser hosts can
|
|
17
|
+
* still swap in their own `env` overlay if they want a different source.
|
|
18
|
+
*/
|
|
19
|
+
function envOverlay() {
|
|
20
|
+
if (typeof process === 'undefined' || !process.env) {
|
|
21
|
+
return () => undefined;
|
|
22
|
+
}
|
|
23
|
+
return path => process.env[path[0]];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Helper that turns a plain dict into an overlay lambda. Used by hosts
|
|
27
|
+
* (and discovery wiring) to build a populated `config` overlay.
|
|
28
|
+
*
|
|
29
|
+
* const overlays = {
|
|
30
|
+
* config: contextOverlay({
|
|
31
|
+
* rootDirectory: ceilingURL,
|
|
32
|
+
* configURL: configURL,
|
|
33
|
+
* }),
|
|
34
|
+
* };
|
|
35
|
+
*/
|
|
36
|
+
function contextOverlay(dict) {
|
|
37
|
+
return path => dict[path[0]];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The default config overlays: `env` wired to process.env, and a no-op
|
|
41
|
+
* `config` overlay. Hosts that want discovery-populated context replace
|
|
42
|
+
* the `config` entry; soloists leave it alone and `{config: ...}` refs
|
|
43
|
+
* resolve to "not present".
|
|
44
|
+
*/
|
|
45
|
+
function defaultConfigOverlays() {
|
|
46
|
+
return {
|
|
47
|
+
env: envOverlay(),
|
|
48
|
+
config: () => undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=config_overlays.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { LogMessage } from '../../lang/parse-log';
|
|
2
|
+
import type { ConnectionConfigEntry } from '../../connection/registry';
|
|
3
|
+
import type { ConfigDict } from './config_compile';
|
|
4
|
+
import type { ConfigOverlays } from './config_overlays';
|
|
5
|
+
/**
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
13
|
+
*
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
export interface ResolvedConfig {
|
|
19
|
+
connections: Record<string, ConnectionConfigEntry>;
|
|
20
|
+
manifestPath?: string;
|
|
21
|
+
virtualMap?: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Walk a compiled tree against the overlay dict and produce a plain
|
|
25
|
+
* resolved POJO.
|
|
26
|
+
*
|
|
27
|
+
* Two distinct "defaults" mechanisms, deliberately separated:
|
|
28
|
+
*
|
|
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)
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveConfig(compiled: ConfigDict, overlays: ConfigOverlays, log: LogMessage[]): ResolvedConfig;
|