@malloy-publisher/server 0.0.229 → 0.0.230
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/app/api-doc.yaml +18 -9
- package/dist/app/assets/{EnvironmentPage-QOoHiVeJ.js → EnvironmentPage-wa_EPkwK.js} +1 -1
- package/dist/app/assets/{HomePage-C71GOfVW.js → HomePage-jnCrupQp.js} +1 -1
- package/dist/app/assets/{LightMode-CrgCAwLe.js → LightMode-DYbwNULZ.js} +1 -1
- package/dist/app/assets/{MainPage-BG5__FN3.js → MainPage-CuJLrPNI.js} +1 -1
- package/dist/app/assets/{MaterializationsPage-DE6PnrDR.js → MaterializationsPage-D_67x2ee.js} +1 -1
- package/dist/app/assets/{ModelPage-CcBjcbLm.js → ModelPage-D5JtAWqR.js} +1 -1
- package/dist/app/assets/{PackagePage-JTy3ztkB.js → PackagePage-BRwtqUSG.js} +1 -1
- package/dist/app/assets/{RouteError-Cymbp47a.js → RouteError-CBNNrnSD.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-C_nMnHr8.js → ThemeEditorPage-CTCeBneA.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CPQu-DQx.js → WorkbookPage-SN6f1RBm.js} +1 -1
- package/dist/app/assets/{core-Coi3caGs.es-CSOmajHS.js → core-Dp3q5Ieu.es-CD5FvM2s.js} +1 -1
- package/dist/app/assets/{index-DlWCXghy.js → index-B3Nn8Vm2.js} +459 -446
- package/dist/app/assets/index-BLCx1EdC.js +18 -0
- package/dist/app/assets/{index-DxArlgRD.js → index-C_tJstcx.js} +4 -4
- package/dist/app/assets/{index-CcuuST2X.js → index-CfmBVB6M.js} +1 -1
- package/dist/app/assets/{index-CkmABCAw.js → index-DU4r7GdU.js} +435 -422
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +108 -7
- package/dist/server.mjs +575 -49
- package/package.json +12 -12
- package/scripts/bake-duckdb-extensions.js +5 -2
- package/src/config.ts +40 -0
- package/src/ducklake_version.spec.ts +163 -0
- package/src/ducklake_version.ts +153 -0
- package/src/errors.ts +12 -0
- package/src/malloy_pin_prereqs.spec.ts +25 -0
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/server.ts +7 -0
- package/src/service/authorize.spec.ts +22 -0
- package/src/service/authorize.ts +267 -10
- package/src/service/authorize_integration.spec.ts +1068 -26
- package/src/service/connection.spec.ts +71 -0
- package/src/service/connection.ts +312 -25
- package/src/service/connection_config.spec.ts +21 -0
- package/src/service/connection_config.ts +15 -1
- package/src/service/ducklake_lazy_attach.spec.ts +110 -0
- package/src/service/environment.ts +43 -9
- package/src/service/environment_store.spec.ts +197 -12
- package/src/service/environment_store.ts +88 -14
- package/src/service/extension_fetch_policy.spec.ts +256 -0
- package/src/service/materialization_scheduler.spec.ts +29 -0
- package/src/service/materialization_service.spec.ts +119 -1
- package/src/service/model.spec.ts +67 -0
- package/src/service/model.ts +656 -31
- package/src/service/package.spec.ts +38 -0
- package/src/service/package.ts +12 -1
- package/src/storage/duckdb/MaterializationRepository.spec.ts +39 -2
- package/src/storage/duckdb/MaterializationRepository.ts +12 -0
- package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +256 -0
- package/dist/app/assets/index-CM2qhQCI.js +0 -18
package/dist/server.mjs
CHANGED
|
@@ -147403,6 +147403,8 @@ function internalErrorToHttpError(error) {
|
|
|
147403
147403
|
return httpError(404, error.message);
|
|
147404
147404
|
} else if (error instanceof ConnectionAuthError) {
|
|
147405
147405
|
return httpError(422, error.message);
|
|
147406
|
+
} else if (error instanceof UnsupportedCatalogFormatError) {
|
|
147407
|
+
return httpError(422, error.message);
|
|
147406
147408
|
} else if (error instanceof ModelCompilationError) {
|
|
147407
147409
|
return httpError(424, error.message);
|
|
147408
147410
|
} else if (error instanceof ConnectionError) {
|
|
@@ -147432,7 +147434,7 @@ function httpError(code, message) {
|
|
|
147432
147434
|
}
|
|
147433
147435
|
};
|
|
147434
147436
|
}
|
|
147435
|
-
var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, ModelCompilationError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
|
|
147437
|
+
var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
|
|
147436
147438
|
var init_errors = __esm(() => {
|
|
147437
147439
|
init_constants();
|
|
147438
147440
|
NotImplementedError = class NotImplementedError extends Error {
|
|
@@ -147475,6 +147477,11 @@ var init_errors = __esm(() => {
|
|
|
147475
147477
|
super(message);
|
|
147476
147478
|
}
|
|
147477
147479
|
};
|
|
147480
|
+
UnsupportedCatalogFormatError = class UnsupportedCatalogFormatError extends Error {
|
|
147481
|
+
constructor(message) {
|
|
147482
|
+
super(message);
|
|
147483
|
+
}
|
|
147484
|
+
};
|
|
147478
147485
|
ModelCompilationError = class ModelCompilationError extends Error {
|
|
147479
147486
|
constructor(error) {
|
|
147480
147487
|
super(error.message);
|
|
@@ -244084,6 +244091,16 @@ var getMaxConcurrentQueries = () => {
|
|
|
244084
244091
|
}
|
|
244085
244092
|
return raw;
|
|
244086
244093
|
};
|
|
244094
|
+
var getExtensionFetchPolicy = () => {
|
|
244095
|
+
const raw = process.env.EXTENSION_FETCH_POLICY;
|
|
244096
|
+
if (raw === undefined || raw.trim() === "")
|
|
244097
|
+
return "on-demand";
|
|
244098
|
+
const normalised = raw.trim().toLowerCase();
|
|
244099
|
+
if (normalised === "on-demand" || normalised === "local-only") {
|
|
244100
|
+
return normalised;
|
|
244101
|
+
}
|
|
244102
|
+
throw new Error(`Invalid value for EXTENSION_FETCH_POLICY: expected "on-demand" or "local-only", got "${raw}"`);
|
|
244103
|
+
};
|
|
244087
244104
|
function substituteEnvVars(value) {
|
|
244088
244105
|
const envVarPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
244089
244106
|
return value.replace(envVarPattern, (_match, varName) => {
|
|
@@ -247757,9 +247774,68 @@ var {
|
|
|
247757
247774
|
} = axios_default;
|
|
247758
247775
|
|
|
247759
247776
|
// src/service/connection.ts
|
|
247760
|
-
init_logger();
|
|
247761
247777
|
import fs2 from "fs/promises";
|
|
247762
247778
|
|
|
247779
|
+
// src/ducklake_version.ts
|
|
247780
|
+
var MIN_CATALOG_FORMAT = "1.0";
|
|
247781
|
+
var ENGINE_MAX_FORMAT = [{ engine: { major: 1, minor: 5 }, maxFormat: "1.0" }];
|
|
247782
|
+
var CATALOG_FORMAT_RE = /^(\d+)\.(\d+)(?:-([0-9A-Za-z.]+))?$/;
|
|
247783
|
+
function parseCatalogFormat(raw) {
|
|
247784
|
+
if (typeof raw !== "string")
|
|
247785
|
+
return null;
|
|
247786
|
+
const m = CATALOG_FORMAT_RE.exec(raw);
|
|
247787
|
+
if (!m)
|
|
247788
|
+
return null;
|
|
247789
|
+
const format = { major: Number(m[1]), minor: Number(m[2]) };
|
|
247790
|
+
if (m[3] !== undefined)
|
|
247791
|
+
format.prerelease = m[3];
|
|
247792
|
+
return format;
|
|
247793
|
+
}
|
|
247794
|
+
function compareCatalogFormat(a, b) {
|
|
247795
|
+
if (a.major !== b.major)
|
|
247796
|
+
return a.major < b.major ? -1 : 1;
|
|
247797
|
+
if (a.minor !== b.minor)
|
|
247798
|
+
return a.minor < b.minor ? -1 : 1;
|
|
247799
|
+
if (a.prerelease === b.prerelease)
|
|
247800
|
+
return 0;
|
|
247801
|
+
if (a.prerelease === undefined)
|
|
247802
|
+
return 1;
|
|
247803
|
+
if (b.prerelease === undefined)
|
|
247804
|
+
return -1;
|
|
247805
|
+
return a.prerelease < b.prerelease ? -1 : 1;
|
|
247806
|
+
}
|
|
247807
|
+
function parseEngineMajorMinor(raw) {
|
|
247808
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(String(raw));
|
|
247809
|
+
if (!m)
|
|
247810
|
+
return null;
|
|
247811
|
+
return { major: Number(m[1]), minor: Number(m[2]) };
|
|
247812
|
+
}
|
|
247813
|
+
function catalogFormatRangeForEngine(engineVersion) {
|
|
247814
|
+
const e = parseEngineMajorMinor(engineVersion);
|
|
247815
|
+
if (!e)
|
|
247816
|
+
return null;
|
|
247817
|
+
const entry = ENGINE_MAX_FORMAT.find((r) => r.engine.major === e.major && r.engine.minor === e.minor);
|
|
247818
|
+
if (!entry)
|
|
247819
|
+
return null;
|
|
247820
|
+
return {
|
|
247821
|
+
min: MIN_CATALOG_FORMAT,
|
|
247822
|
+
max: entry.maxFormat,
|
|
247823
|
+
engineVersion: `${e.major}.${e.minor}`
|
|
247824
|
+
};
|
|
247825
|
+
}
|
|
247826
|
+
function isCatalogFormatInRange(format, range) {
|
|
247827
|
+
const f = parseCatalogFormat(format);
|
|
247828
|
+
const min = parseCatalogFormat(range.min);
|
|
247829
|
+
const max = parseCatalogFormat(range.max);
|
|
247830
|
+
if (!f || !min || !max)
|
|
247831
|
+
return false;
|
|
247832
|
+
return compareCatalogFormat(f, min) >= 0 && compareCatalogFormat(f, max) <= 0;
|
|
247833
|
+
}
|
|
247834
|
+
|
|
247835
|
+
// src/service/connection.ts
|
|
247836
|
+
init_errors();
|
|
247837
|
+
init_logger();
|
|
247838
|
+
|
|
247763
247839
|
// src/pg_helpers.ts
|
|
247764
247840
|
function redactPgSecrets(s) {
|
|
247765
247841
|
return s.replace(/password=('[^']*'|"[^"]*"|\S+)/gi, "password=***");
|
|
@@ -247951,7 +248027,12 @@ function openSshProxy(ssh, target) {
|
|
|
247951
248027
|
}
|
|
247952
248028
|
|
|
247953
248029
|
// src/service/connection_config.ts
|
|
247954
|
-
var PROXIED_SSLMODES = [
|
|
248030
|
+
var PROXIED_SSLMODES = [
|
|
248031
|
+
"disable",
|
|
248032
|
+
"no-verify",
|
|
248033
|
+
"verify-ca",
|
|
248034
|
+
"verify-full"
|
|
248035
|
+
];
|
|
247955
248036
|
var PUBLISHER_DUCKDB_API_FIELDS = new Set(["attachedDatabases"]);
|
|
247956
248037
|
function normalizeSnowflakePrivateKey(privateKey) {
|
|
247957
248038
|
let privateKeyContent = privateKey.trim();
|
|
@@ -248436,16 +248517,55 @@ function validateAndBuildTrinoCoreConfig(trinoConfig) {
|
|
|
248436
248517
|
}
|
|
248437
248518
|
|
|
248438
248519
|
// src/service/connection.ts
|
|
248520
|
+
var extensionSessionPinned = new WeakSet;
|
|
248521
|
+
async function applyExtensionSessionSettings(connection, {
|
|
248522
|
+
alwaysDisableAutoinstall = false
|
|
248523
|
+
} = {}) {
|
|
248524
|
+
const policy = getExtensionFetchPolicy();
|
|
248525
|
+
const disableAutoinstall = alwaysDisableAutoinstall || policy === "local-only";
|
|
248526
|
+
if (!disableAutoinstall || extensionSessionPinned.has(connection)) {
|
|
248527
|
+
return;
|
|
248528
|
+
}
|
|
248529
|
+
try {
|
|
248530
|
+
await connection.runSQL("SET autoinstall_known_extensions=false;");
|
|
248531
|
+
await connection.runSQL("SET autoload_known_extensions=true;");
|
|
248532
|
+
extensionSessionPinned.add(connection);
|
|
248533
|
+
} catch (error) {
|
|
248534
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
248535
|
+
if (policy === "local-only") {
|
|
248536
|
+
throw new Error(`Failed to disable DuckDB implicit extension auto-install under ` + `EXTENSION_FETCH_POLICY=local-only; refusing to open a session that ` + `could still fetch from the network. Underlying error: ${detail}`);
|
|
248537
|
+
}
|
|
248538
|
+
logger.warn("Failed to pin DuckDB extension session settings", {
|
|
248539
|
+
error: detail
|
|
248540
|
+
});
|
|
248541
|
+
}
|
|
248542
|
+
}
|
|
248439
248543
|
async function installAndLoadExtension(connection, extensionName, fromCommunity = false) {
|
|
248544
|
+
const policy = getExtensionFetchPolicy();
|
|
248545
|
+
if (policy === "on-demand") {
|
|
248546
|
+
try {
|
|
248547
|
+
const installCommand = fromCommunity ? `INSTALL ${extensionName} FROM community;` : `INSTALL ${extensionName};`;
|
|
248548
|
+
await connection.runSQL(installCommand);
|
|
248549
|
+
logger.info(`${extensionName} extension installed`);
|
|
248550
|
+
} catch (error) {
|
|
248551
|
+
logger.info(`${extensionName} extension already installed or install skipped`, { error });
|
|
248552
|
+
}
|
|
248553
|
+
}
|
|
248440
248554
|
try {
|
|
248441
|
-
|
|
248442
|
-
|
|
248443
|
-
logger.info(`${extensionName} extension installed`);
|
|
248555
|
+
await connection.runSQL(`LOAD ${extensionName};`);
|
|
248556
|
+
logger.info(`${extensionName} extension loaded`);
|
|
248444
248557
|
} catch (error) {
|
|
248445
|
-
|
|
248558
|
+
if (policy === "local-only") {
|
|
248559
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
248560
|
+
logger.warn("DuckDB extension unavailable under local-only policy", {
|
|
248561
|
+
extension: extensionName,
|
|
248562
|
+
policy,
|
|
248563
|
+
error: detail
|
|
248564
|
+
});
|
|
248565
|
+
throw new Error(`DuckDB extension '${extensionName}' is not available locally and ` + `EXTENSION_FETCH_POLICY=local-only forbids fetching it. Bake the ` + `'${extensionName}' extension into the image (or a pre-populated ` + `~/.duckdb/extensions directory), or set ` + `EXTENSION_FETCH_POLICY=on-demand to allow fetching missing ` + `extensions on first use. Underlying LOAD error: ${detail}`);
|
|
248566
|
+
}
|
|
248567
|
+
throw error;
|
|
248446
248568
|
}
|
|
248447
|
-
await connection.runSQL(`LOAD ${extensionName};`);
|
|
248448
|
-
logger.info(`${extensionName} extension loaded`);
|
|
248449
248569
|
}
|
|
248450
248570
|
async function isDatabaseAttached(connection, dbName) {
|
|
248451
248571
|
try {
|
|
@@ -248617,7 +248737,67 @@ async function attachPostgres(connection, attachedDb) {
|
|
|
248617
248737
|
await connection.runSQL(attachCommand);
|
|
248618
248738
|
logger.info(`Successfully attached PostgreSQL database: ${attachedDb.name}`);
|
|
248619
248739
|
}
|
|
248740
|
+
function runSQLRows(result) {
|
|
248741
|
+
if (Array.isArray(result))
|
|
248742
|
+
return result;
|
|
248743
|
+
const rows = result.rows;
|
|
248744
|
+
return Array.isArray(rows) ? rows : [];
|
|
248745
|
+
}
|
|
248746
|
+
var ducklakePreflightSeq = 0;
|
|
248747
|
+
async function preflightDuckLakeCatalogFormat(connection, dbName, pgConnString) {
|
|
248748
|
+
const tempDb = `${dbName}_fmt_preflight_${++ducklakePreflightSeq}`;
|
|
248749
|
+
let catalogFormat;
|
|
248750
|
+
try {
|
|
248751
|
+
await connection.runSQL(`ATTACH '${escapeSQL(pgConnString)}' AS ${tempDb} (TYPE postgres, READ_ONLY);`);
|
|
248752
|
+
const result = await connection.runSQL(`SELECT value FROM ${tempDb}.ducklake_metadata WHERE key = 'version' LIMIT 1;`);
|
|
248753
|
+
const value = runSQLRows(result)[0]?.value;
|
|
248754
|
+
catalogFormat = typeof value === "string" ? value : undefined;
|
|
248755
|
+
} catch (error) {
|
|
248756
|
+
logger.warn("DuckLake catalog-format preflight read failed; falling back to ATTACH", {
|
|
248757
|
+
dbName,
|
|
248758
|
+
error: redactPgSecrets(error instanceof Error ? error.message : String(error))
|
|
248759
|
+
});
|
|
248760
|
+
return;
|
|
248761
|
+
} finally {
|
|
248762
|
+
try {
|
|
248763
|
+
await connection.runSQL(`DETACH ${tempDb};`);
|
|
248764
|
+
} catch {}
|
|
248765
|
+
}
|
|
248766
|
+
if (!catalogFormat) {
|
|
248767
|
+
return;
|
|
248768
|
+
}
|
|
248769
|
+
let engineVersion = "";
|
|
248770
|
+
try {
|
|
248771
|
+
const versionResult = await connection.runSQL("SELECT version() AS v;");
|
|
248772
|
+
engineVersion = String(runSQLRows(versionResult)[0]?.v ?? "");
|
|
248773
|
+
} catch (error) {
|
|
248774
|
+
logger.warn("DuckLake catalog-format preflight: could not read engine version; skipping", {
|
|
248775
|
+
dbName,
|
|
248776
|
+
error: error instanceof Error ? error.message : String(error)
|
|
248777
|
+
});
|
|
248778
|
+
return;
|
|
248779
|
+
}
|
|
248780
|
+
const range = catalogFormatRangeForEngine(engineVersion);
|
|
248781
|
+
if (!range) {
|
|
248782
|
+
logger.warn("DuckLake catalog-format preflight: engine not covered by the format matrix; skipping", { dbName, engineVersion });
|
|
248783
|
+
return;
|
|
248784
|
+
}
|
|
248785
|
+
if (!isCatalogFormatInRange(catalogFormat, range)) {
|
|
248786
|
+
logger.warn("DuckLake catalog format outside supported range; rejecting", {
|
|
248787
|
+
dbName,
|
|
248788
|
+
catalogFormat,
|
|
248789
|
+
supportedMin: range.min,
|
|
248790
|
+
supportedMax: range.max,
|
|
248791
|
+
engineVersion: range.engineVersion
|
|
248792
|
+
});
|
|
248793
|
+
throw new UnsupportedCatalogFormatError(`DuckLake catalog '${dbName}' has format version ${catalogFormat}, ` + `which is outside the range this Publisher supports ` + `(${range.min} ≤ format ≤ ${range.max}). The DuckLake ` + `extension bundled with DuckDB v${range.engineVersion} attaches only ` + `catalogs in that range without migration. Migrate the catalog to a ` + `supported format (see the DuckLake catalog-migration docs at ` + `https://ducklake.select/docs), or run a Publisher built against a ` + `DuckDB engine whose supported range includes ${catalogFormat}.`);
|
|
248794
|
+
}
|
|
248795
|
+
logger.info(`DuckLake catalog '${dbName}' format ${catalogFormat} is within supported range ` + `${range.min}..${range.max} (engine v${range.engineVersion})`);
|
|
248796
|
+
}
|
|
248620
248797
|
async function attachDuckLake(connection, dbName, ducklakeConfig) {
|
|
248798
|
+
await applyExtensionSessionSettings(connection, {
|
|
248799
|
+
alwaysDisableAutoinstall: true
|
|
248800
|
+
});
|
|
248621
248801
|
await installAndLoadExtension(connection, "ducklake");
|
|
248622
248802
|
await installAndLoadExtension(connection, "postgres");
|
|
248623
248803
|
await installAndLoadExtension(connection, "aws");
|
|
@@ -248650,6 +248830,7 @@ async function attachDuckLake(connection, dbName, ducklakeConfig) {
|
|
|
248650
248830
|
logger.info(`Final escaped connection string: ${redactPgSecrets(escapedPgConnString)}`);
|
|
248651
248831
|
const escapedBucketUrl = escapeSQL(ducklakeConfig.storage.bucketUrl);
|
|
248652
248832
|
logger.info(`escapedBucketUrl: ${escapedBucketUrl}`);
|
|
248833
|
+
await preflightDuckLakeCatalogFormat(connection, dbName, pgConnString);
|
|
248653
248834
|
const attachCommand = `ATTACH OR REPLACE 'ducklake:postgres:${escapedPgConnString}' AS ${dbName} (DATA_PATH '${escapedBucketUrl}', OVERRIDE_DATA_PATH true, READ_ONLY true);`;
|
|
248654
248835
|
logger.info(`Attaching DuckLake database using command: ${redactPgSecrets(attachCommand)}`);
|
|
248655
248836
|
try {
|
|
@@ -248815,6 +248996,7 @@ async function attachDatabasesToDuckDB(duckdbConnection, attachedDatabases) {
|
|
|
248815
248996
|
s3: attachCloudStorage,
|
|
248816
248997
|
azure: attachAzureStorage
|
|
248817
248998
|
};
|
|
248999
|
+
await applyExtensionSessionSettings(duckdbConnection);
|
|
248818
249000
|
const hasAzure = attachedDatabases.some((db) => db.type === "azure");
|
|
248819
249001
|
if (hasAzure) {
|
|
248820
249002
|
await installAndLoadExtension(duckdbConnection, "azure");
|
|
@@ -249001,8 +249183,17 @@ function buildProxiedSslQuery(name, sslmode) {
|
|
|
249001
249183
|
}
|
|
249002
249184
|
return `?uselibpqcompat=true&sslmode=verify-ca&sslrootcert=${encodeURIComponent(caBundle)}`;
|
|
249003
249185
|
default:
|
|
249004
|
-
throw new Error(`Connection proxy on '${name}' has unsupported sslmode '${mode}' (expected disable | no-verify | verify-ca).`);
|
|
249186
|
+
throw new Error(`Connection proxy on '${name}' has unsupported sslmode '${mode}' (expected disable | no-verify | verify-ca; verify-full is applied via the ssl object in resolveProxiedTls, not this query).`);
|
|
249187
|
+
}
|
|
249188
|
+
}
|
|
249189
|
+
function resolveProxiedTls(name, host, sslmode) {
|
|
249190
|
+
if (sslmode === "verify-full") {
|
|
249191
|
+
if (!host) {
|
|
249192
|
+
throw new Error(`Connection proxy on '${name}' uses sslmode 'verify-full' but has no host to verify against.`);
|
|
249193
|
+
}
|
|
249194
|
+
return { query: "", ssl: { servername: host, rejectUnauthorized: true } };
|
|
249005
249195
|
}
|
|
249196
|
+
return { query: buildProxiedSslQuery(name, sslmode) };
|
|
249006
249197
|
}
|
|
249007
249198
|
function buildProxiedPostgresConnection(metadata, endpoint) {
|
|
249008
249199
|
const name = metadata.apiConnection.name;
|
|
@@ -249013,11 +249204,12 @@ function buildProxiedPostgresConnection(metadata, endpoint) {
|
|
|
249013
249204
|
const enc = encodeURIComponent;
|
|
249014
249205
|
const auth = pg.userName ? `${enc(pg.userName)}${pg.password ? `:${enc(pg.password)}` : ""}@` : "";
|
|
249015
249206
|
const db = pg.databaseName ? `/${enc(pg.databaseName)}` : "";
|
|
249016
|
-
const ssl =
|
|
249017
|
-
const connectionString = `postgresql://${auth}${endpoint.host}:${endpoint.port}${db}${
|
|
249207
|
+
const { query, ssl } = resolveProxiedTls(name, pg.host, pg.sslmode);
|
|
249208
|
+
const connectionString = `postgresql://${auth}${endpoint.host}:${endpoint.port}${db}${query}`;
|
|
249018
249209
|
return new PooledPostgresConnection({
|
|
249019
249210
|
name,
|
|
249020
249211
|
connectionString,
|
|
249212
|
+
...ssl ? { ssl } : {},
|
|
249021
249213
|
poolMin: 1,
|
|
249022
249214
|
poolMax: 20
|
|
249023
249215
|
});
|
|
@@ -249140,7 +249332,11 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
|
|
|
249140
249332
|
return {
|
|
249141
249333
|
lookupConnection: async (name) => {
|
|
249142
249334
|
const metadata = getMetadataForLookup(assembled.metadata, name);
|
|
249143
|
-
|
|
249335
|
+
const connection = await resolveConnection(name, metadata);
|
|
249336
|
+
if (isDuckDBConnection(connection)) {
|
|
249337
|
+
await applyExtensionSessionSettings(connection);
|
|
249338
|
+
}
|
|
249339
|
+
return applyConnectionFingerprint(connection, metadata);
|
|
249144
249340
|
}
|
|
249145
249341
|
};
|
|
249146
249342
|
});
|
|
@@ -258062,6 +258258,7 @@ async function fetchManifestEntries(uri) {
|
|
|
258062
258258
|
import * as fs6 from "fs/promises";
|
|
258063
258259
|
import * as path7 from "path";
|
|
258064
258260
|
import"@malloydata/db-duckdb/native";
|
|
258261
|
+
import { DuckDBConnection as DuckDBConnection3 } from "@malloydata/db-duckdb";
|
|
258065
258262
|
import {
|
|
258066
258263
|
ConnectionRuntime,
|
|
258067
258264
|
contextOverlay as contextOverlay2,
|
|
@@ -258073,8 +258270,8 @@ import {
|
|
|
258073
258270
|
init_package_load_pool();
|
|
258074
258271
|
init_constants();
|
|
258075
258272
|
init_errors();
|
|
258076
|
-
init_logger();
|
|
258077
258273
|
var import_recursive_readdir = __toESM(require_recursive_readdir(), 1);
|
|
258274
|
+
init_logger();
|
|
258078
258275
|
|
|
258079
258276
|
// src/package_load_metrics.ts
|
|
258080
258277
|
var LOAD_DURATION_BUCKETS_MS = [
|
|
@@ -258135,6 +258332,10 @@ import {
|
|
|
258135
258332
|
Annotations as Annotations2,
|
|
258136
258333
|
API,
|
|
258137
258334
|
FixedConnectionMap,
|
|
258335
|
+
isBasicArray,
|
|
258336
|
+
isJoined,
|
|
258337
|
+
isRepeatedRecord,
|
|
258338
|
+
isSourceDef as isSourceDef2,
|
|
258138
258339
|
MalloyConfig as MalloyConfig2,
|
|
258139
258340
|
MalloyError as MalloyError2,
|
|
258140
258341
|
modelDefToModelInfo,
|
|
@@ -258245,24 +258446,95 @@ function annotationTexts(annote) {
|
|
|
258245
258446
|
init_errors();
|
|
258246
258447
|
var SOURCE_PREFIX = "#(authorize)";
|
|
258247
258448
|
var FILE_PREFIX = "##(authorize)";
|
|
258248
|
-
function buildAuthorizeProbe(exprs) {
|
|
258449
|
+
function buildAuthorizeProbe(exprs, givenDecls = []) {
|
|
258249
258450
|
const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
|
|
258250
258451
|
`);
|
|
258251
|
-
|
|
258452
|
+
const givenBlock = givenDecls.length > 0 ? `given:
|
|
258453
|
+
${givenDecls.map((g) => ` ${g.name} :: ${g.type}`).join(`
|
|
258454
|
+
`)}
|
|
258455
|
+
|
|
258456
|
+
` : "";
|
|
258457
|
+
return `${givenBlock}run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
|
|
258252
258458
|
select:
|
|
258253
258459
|
${selects}
|
|
258254
258460
|
limit: 1
|
|
258255
258461
|
}`;
|
|
258256
258462
|
}
|
|
258463
|
+
var GIVEN_REF_PATTERN = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
258464
|
+
var STRING_LITERAL_PATTERN = /'(?:\\.|[^'\\])*'/g;
|
|
258465
|
+
function referencedGivenNames(expr) {
|
|
258466
|
+
const scanned = expr.replace(STRING_LITERAL_PATTERN, "''");
|
|
258467
|
+
const names = [];
|
|
258468
|
+
const seen = new Set;
|
|
258469
|
+
for (const match of scanned.matchAll(GIVEN_REF_PATTERN)) {
|
|
258470
|
+
const name = match[1];
|
|
258471
|
+
if (!seen.has(name)) {
|
|
258472
|
+
seen.add(name);
|
|
258473
|
+
names.push(name);
|
|
258474
|
+
}
|
|
258475
|
+
}
|
|
258476
|
+
return names;
|
|
258477
|
+
}
|
|
258478
|
+
function inferGivenType(value) {
|
|
258479
|
+
if (typeof value === "string")
|
|
258480
|
+
return "string";
|
|
258481
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
258482
|
+
return "number";
|
|
258483
|
+
if (typeof value === "boolean")
|
|
258484
|
+
return "boolean";
|
|
258485
|
+
if (value instanceof Date)
|
|
258486
|
+
return "timestamp";
|
|
258487
|
+
if (Array.isArray(value)) {
|
|
258488
|
+
if (value.length === 0)
|
|
258489
|
+
return null;
|
|
258490
|
+
const elementType = inferGivenType(value[0]);
|
|
258491
|
+
return elementType ? `${elementType}[]` : null;
|
|
258492
|
+
}
|
|
258493
|
+
return null;
|
|
258494
|
+
}
|
|
258495
|
+
function bindProbeGivens(expr, givens, declaredTypes) {
|
|
258496
|
+
const decls = [];
|
|
258497
|
+
const bound = {};
|
|
258498
|
+
for (const name of referencedGivenNames(expr)) {
|
|
258499
|
+
if (!(name in givens))
|
|
258500
|
+
continue;
|
|
258501
|
+
const value = givens[name];
|
|
258502
|
+
const type = declaredTypes?.get(name) ?? inferGivenType(value);
|
|
258503
|
+
if (!type)
|
|
258504
|
+
continue;
|
|
258505
|
+
decls.push({ name, type });
|
|
258506
|
+
bound[name] = value;
|
|
258507
|
+
}
|
|
258508
|
+
return { decls, bound };
|
|
258509
|
+
}
|
|
258257
258510
|
function isProbeTrue(cell) {
|
|
258258
258511
|
return cell === true || cell === 1 || cell === "true";
|
|
258259
258512
|
}
|
|
258260
|
-
async function
|
|
258513
|
+
async function runProbe(executor, probeText, givens) {
|
|
258514
|
+
const result = await executor.loadQuery(probeText).run({ rowLimit: 1, givens });
|
|
258515
|
+
const row = result?.data?.value?.[0];
|
|
258516
|
+
return !!(row && isProbeTrue(row.__auth_0));
|
|
258517
|
+
}
|
|
258518
|
+
async function evaluateAuthorize(executor, exprs, givens, declaredTypes, options) {
|
|
258519
|
+
const selfContainedFirst = options?.selfContainedFirst ?? false;
|
|
258261
258520
|
for (const expr of exprs) {
|
|
258521
|
+
if (selfContainedFirst) {
|
|
258522
|
+
if (await evaluateSelfContainedFirst(executor, expr, givens, declaredTypes)) {
|
|
258523
|
+
return true;
|
|
258524
|
+
}
|
|
258525
|
+
continue;
|
|
258526
|
+
}
|
|
258527
|
+
try {
|
|
258528
|
+
if (await runProbe(executor, buildAuthorizeProbe([expr]), givens)) {
|
|
258529
|
+
return true;
|
|
258530
|
+
}
|
|
258531
|
+
continue;
|
|
258532
|
+
} catch {}
|
|
258262
258533
|
try {
|
|
258263
|
-
const
|
|
258264
|
-
|
|
258265
|
-
|
|
258534
|
+
const { decls, bound } = bindProbeGivens(expr, givens, declaredTypes);
|
|
258535
|
+
if (decls.length === 0)
|
|
258536
|
+
continue;
|
|
258537
|
+
if (await runProbe(executor, buildAuthorizeProbe([expr], decls), bound)) {
|
|
258266
258538
|
return true;
|
|
258267
258539
|
}
|
|
258268
258540
|
} catch {
|
|
@@ -258271,6 +258543,29 @@ async function evaluateAuthorize(executor, exprs, givens) {
|
|
|
258271
258543
|
}
|
|
258272
258544
|
return false;
|
|
258273
258545
|
}
|
|
258546
|
+
async function evaluateSelfContainedFirst(executor, expr, givens, declaredTypes) {
|
|
258547
|
+
const referenced = referencedGivenNames(expr);
|
|
258548
|
+
if (referenced.length === 0) {
|
|
258549
|
+
try {
|
|
258550
|
+
return await runProbe(executor, buildAuthorizeProbe([expr]), {});
|
|
258551
|
+
} catch {
|
|
258552
|
+
return false;
|
|
258553
|
+
}
|
|
258554
|
+
}
|
|
258555
|
+
const { decls, bound } = bindProbeGivens(expr, givens, declaredTypes);
|
|
258556
|
+
if (decls.length !== referenced.length) {
|
|
258557
|
+
return false;
|
|
258558
|
+
}
|
|
258559
|
+
try {
|
|
258560
|
+
return await runProbe(executor, buildAuthorizeProbe([expr], decls), bound);
|
|
258561
|
+
} catch {
|
|
258562
|
+
try {
|
|
258563
|
+
return await runProbe(executor, buildAuthorizeProbe([expr]), givens);
|
|
258564
|
+
} catch {
|
|
258565
|
+
return false;
|
|
258566
|
+
}
|
|
258567
|
+
}
|
|
258568
|
+
}
|
|
258274
258569
|
async function validateAuthorizeProbes(compiler, sources) {
|
|
258275
258570
|
for (const source of sources) {
|
|
258276
258571
|
const exprs = source.authorize;
|
|
@@ -258665,6 +258960,9 @@ function extractQueriesFromModelDef(modelDef) {
|
|
|
258665
258960
|
|
|
258666
258961
|
// src/service/model.ts
|
|
258667
258962
|
var MALLOY_VERSION = createRequire2(import.meta.url)("@malloydata/malloy/package.json").version;
|
|
258963
|
+
function isRecordOrArrayField(field) {
|
|
258964
|
+
return field.type === "record" || isBasicArray(field) || isRepeatedRecord(field);
|
|
258965
|
+
}
|
|
258668
258966
|
function quoteMalloyIdentifier(name) {
|
|
258669
258967
|
return "`" + (name ?? "").replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
|
|
258670
258968
|
}
|
|
@@ -258685,6 +258983,7 @@ class Model {
|
|
|
258685
258983
|
filterMap;
|
|
258686
258984
|
givens;
|
|
258687
258985
|
fileLevelAuthorize = [];
|
|
258986
|
+
authorizeReferencedGivenNames = new Set;
|
|
258688
258987
|
discoveryCurationEnabled = false;
|
|
258689
258988
|
queryBoundary = { mode: "all", exploresDeclared: false, isQueryEntryPoint: true };
|
|
258690
258989
|
freshnessResolver;
|
|
@@ -258712,6 +259011,11 @@ class Model {
|
|
|
258712
259011
|
} catch {
|
|
258713
259012
|
this.fileLevelAuthorize = [];
|
|
258714
259013
|
}
|
|
259014
|
+
try {
|
|
259015
|
+
this.authorizeReferencedGivenNames = this.computeAuthorizeReferencedGivenNames();
|
|
259016
|
+
} catch {
|
|
259017
|
+
this.authorizeReferencedGivenNames = new Set;
|
|
259018
|
+
}
|
|
258715
259019
|
this.modelInfo = modelInfo ?? (this.modelDef ? modelDefToModelInfo(this.modelDef) : undefined);
|
|
258716
259020
|
if (this.filterMap.size > 0) {
|
|
258717
259021
|
logger.warn(`Model "${packageName}/${modelPath}" uses deprecated #(filter) annotations. Migrate to given: — see https://github.com/malloydata/publisher/blob/main/docs/givens.md`, {
|
|
@@ -258724,9 +259028,45 @@ class Model {
|
|
|
258724
259028
|
getFilters(sourceName) {
|
|
258725
259029
|
return this.filterMap.get(sourceName) ?? [];
|
|
258726
259030
|
}
|
|
259031
|
+
givenDeclaredTypes() {
|
|
259032
|
+
return new Map((this.givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
|
|
259033
|
+
}
|
|
258727
259034
|
getAuthorize(sourceName) {
|
|
258728
259035
|
return this.sources?.find((source) => source.name === sourceName)?.authorize ?? [];
|
|
258729
259036
|
}
|
|
259037
|
+
filterGivensToModelSurface(givens) {
|
|
259038
|
+
if (!givens)
|
|
259039
|
+
return givens;
|
|
259040
|
+
const surfaceNames = new Set((this.givens ?? []).map((g) => g.name));
|
|
259041
|
+
const filtered = {};
|
|
259042
|
+
for (const [name, value] of Object.entries(givens)) {
|
|
259043
|
+
const authorizeOnly = !surfaceNames.has(name) && this.authorizeReferencedGivenNames.has(name);
|
|
259044
|
+
if (!authorizeOnly)
|
|
259045
|
+
filtered[name] = value;
|
|
259046
|
+
}
|
|
259047
|
+
return filtered;
|
|
259048
|
+
}
|
|
259049
|
+
computeAuthorizeReferencedGivenNames() {
|
|
259050
|
+
const names = new Set;
|
|
259051
|
+
const addExprs = (exprs) => {
|
|
259052
|
+
for (const expr of exprs) {
|
|
259053
|
+
for (const name of referencedGivenNames(expr))
|
|
259054
|
+
names.add(name);
|
|
259055
|
+
}
|
|
259056
|
+
};
|
|
259057
|
+
addExprs(this.fileLevelAuthorize);
|
|
259058
|
+
const modelDef = this.modelDef;
|
|
259059
|
+
if (!modelDef)
|
|
259060
|
+
return names;
|
|
259061
|
+
for (const entry of Object.values(modelDef.contents)) {
|
|
259062
|
+
if (!isSourceDef2(entry))
|
|
259063
|
+
continue;
|
|
259064
|
+
for (const { exprs } of this.collectAllReachableGates(entry, modelDef, new Set)) {
|
|
259065
|
+
addExprs(exprs);
|
|
259066
|
+
}
|
|
259067
|
+
}
|
|
259068
|
+
return names;
|
|
259069
|
+
}
|
|
258730
259070
|
hasAuthorize() {
|
|
258731
259071
|
return this.fileLevelAuthorize.length > 0 || (this.sources?.some((s) => (s.authorize?.length ?? 0) > 0) ?? false);
|
|
258732
259072
|
}
|
|
@@ -258737,10 +259077,11 @@ class Model {
|
|
|
258737
259077
|
return this.fileLevelAuthorize;
|
|
258738
259078
|
}
|
|
258739
259079
|
async assertAuthorized(sourceName, givens) {
|
|
258740
|
-
|
|
259080
|
+
await this.assertAuthorizedExprs(sourceName ?? "(query)", this.effectiveAuthorizeFor(sourceName), givens);
|
|
259081
|
+
}
|
|
259082
|
+
async assertAuthorizedExprs(label, exprs, givens, selfContainedFirst = false) {
|
|
258741
259083
|
if (exprs.length === 0)
|
|
258742
259084
|
return;
|
|
258743
|
-
const label = sourceName ?? "(query)";
|
|
258744
259085
|
const deny = () => {
|
|
258745
259086
|
throw new AccessDeniedError(`Access denied for source "${label}".`);
|
|
258746
259087
|
};
|
|
@@ -258748,7 +259089,7 @@ class Model {
|
|
|
258748
259089
|
deny();
|
|
258749
259090
|
let passed = false;
|
|
258750
259091
|
try {
|
|
258751
|
-
passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens);
|
|
259092
|
+
passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens, this.givenDeclaredTypes(), { selfContainedFirst });
|
|
258752
259093
|
} catch (err) {
|
|
258753
259094
|
logger.debug("Authorize probe failed; denying", {
|
|
258754
259095
|
sourceName: label,
|
|
@@ -258760,11 +259101,155 @@ class Model {
|
|
|
258760
259101
|
if (!passed)
|
|
258761
259102
|
deny();
|
|
258762
259103
|
}
|
|
259104
|
+
async assertAuthorizedForAllSources(runnable, givens) {
|
|
259105
|
+
const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
|
|
259106
|
+
await this.assertAuthorized(ownSourceName, givens);
|
|
259107
|
+
const { struct, modelDef, compositeResolvedSourceDef, extendSources } = await this.resolveRunTargetStruct(runnable);
|
|
259108
|
+
const seen = new Set;
|
|
259109
|
+
const joinedGates = this.collectAllReachableGates(struct, modelDef, seen, true);
|
|
259110
|
+
if (modelDef) {
|
|
259111
|
+
const runTargetLabel = ownSourceName ?? "(run target)";
|
|
259112
|
+
for (const field of extendSources) {
|
|
259113
|
+
const { resolved, denyGate } = this.classifyJoinedField(field, runTargetLabel);
|
|
259114
|
+
if (denyGate) {
|
|
259115
|
+
joinedGates.push(denyGate);
|
|
259116
|
+
continue;
|
|
259117
|
+
}
|
|
259118
|
+
if (!resolved)
|
|
259119
|
+
continue;
|
|
259120
|
+
joinedGates.push(...this.collectAllReachableGates(resolved, modelDef, seen));
|
|
259121
|
+
}
|
|
259122
|
+
}
|
|
259123
|
+
if (compositeResolvedSourceDef && modelDef) {
|
|
259124
|
+
joinedGates.push(...this.collectAllReachableGates(compositeResolvedSourceDef, modelDef, seen, true));
|
|
259125
|
+
}
|
|
259126
|
+
for (const { label, exprs, selfContained } of joinedGates) {
|
|
259127
|
+
await this.assertAuthorizedExprs(label, exprs, givens, selfContained);
|
|
259128
|
+
}
|
|
259129
|
+
}
|
|
259130
|
+
async resolveRunTargetStruct(runnable) {
|
|
259131
|
+
try {
|
|
259132
|
+
const prepared = await runnable.getPreparedQuery();
|
|
259133
|
+
const modelDef = prepared._modelDef ?? this.modelDef;
|
|
259134
|
+
if (!modelDef)
|
|
259135
|
+
return {
|
|
259136
|
+
struct: undefined,
|
|
259137
|
+
modelDef: undefined,
|
|
259138
|
+
compositeResolvedSourceDef: undefined,
|
|
259139
|
+
extendSources: []
|
|
259140
|
+
};
|
|
259141
|
+
const structRef = prepared._query?.structRef;
|
|
259142
|
+
const struct = typeof structRef === "string" ? modelDef.contents[structRef] : structRef;
|
|
259143
|
+
const extendSources = (prepared._query?.pipeline ?? []).flatMap((segment) => segment.extendSource ?? []);
|
|
259144
|
+
return {
|
|
259145
|
+
struct: struct && typeof struct === "object" ? struct : undefined,
|
|
259146
|
+
modelDef,
|
|
259147
|
+
compositeResolvedSourceDef: prepared._query?.compositeResolvedSourceDef,
|
|
259148
|
+
extendSources
|
|
259149
|
+
};
|
|
259150
|
+
} catch {
|
|
259151
|
+
return {
|
|
259152
|
+
struct: undefined,
|
|
259153
|
+
modelDef: undefined,
|
|
259154
|
+
compositeResolvedSourceDef: undefined,
|
|
259155
|
+
extendSources: []
|
|
259156
|
+
};
|
|
259157
|
+
}
|
|
259158
|
+
}
|
|
259159
|
+
gateExprsForOwnAnnotations(struct) {
|
|
259160
|
+
const ownNotes = (struct.annotations?.blockNotes ?? []).map((note) => note.text);
|
|
259161
|
+
try {
|
|
259162
|
+
return [
|
|
259163
|
+
...this.fileLevelAuthorize,
|
|
259164
|
+
...collectAuthorizeExprs(ownNotes)
|
|
259165
|
+
];
|
|
259166
|
+
} catch {
|
|
259167
|
+
return ["false"];
|
|
259168
|
+
}
|
|
259169
|
+
}
|
|
259170
|
+
classifyJoinedField(field, parentLabel) {
|
|
259171
|
+
if (!isJoined(field))
|
|
259172
|
+
return {};
|
|
259173
|
+
if (isRecordOrArrayField(field))
|
|
259174
|
+
return {};
|
|
259175
|
+
if (!isSourceDef2(field)) {
|
|
259176
|
+
logger.error("authorize: joined field failed to resolve to a walkable SourceDef; denying rather than silently skipping its gate (possible Malloy struct-shape drift)", {
|
|
259177
|
+
modelPath: this.modelPath,
|
|
259178
|
+
parentSource: parentLabel,
|
|
259179
|
+
fieldName: field.name,
|
|
259180
|
+
fieldType: field.type
|
|
259181
|
+
});
|
|
259182
|
+
return {
|
|
259183
|
+
denyGate: {
|
|
259184
|
+
label: `${parentLabel} (unresolvable joined source)`,
|
|
259185
|
+
exprs: ["false"],
|
|
259186
|
+
selfContained: false
|
|
259187
|
+
}
|
|
259188
|
+
};
|
|
259189
|
+
}
|
|
259190
|
+
return { resolved: field };
|
|
259191
|
+
}
|
|
259192
|
+
collectAllReachableGates(struct, modelDef, seen = new Set, treatAsOwnGate = false) {
|
|
259193
|
+
if (!struct || !modelDef || seen.has(struct))
|
|
259194
|
+
return [];
|
|
259195
|
+
seen.add(struct);
|
|
259196
|
+
const results = [];
|
|
259197
|
+
const label = struct.as ?? struct.name;
|
|
259198
|
+
const ownExprs = this.gateExprsForOwnAnnotations(struct);
|
|
259199
|
+
if (ownExprs.length > 0) {
|
|
259200
|
+
results.push({
|
|
259201
|
+
label,
|
|
259202
|
+
exprs: ownExprs,
|
|
259203
|
+
selfContained: !treatAsOwnGate
|
|
259204
|
+
});
|
|
259205
|
+
}
|
|
259206
|
+
for (const field of struct.fields) {
|
|
259207
|
+
const { resolved, denyGate } = this.classifyJoinedField(field, label);
|
|
259208
|
+
if (denyGate) {
|
|
259209
|
+
results.push(denyGate);
|
|
259210
|
+
continue;
|
|
259211
|
+
}
|
|
259212
|
+
if (!resolved)
|
|
259213
|
+
continue;
|
|
259214
|
+
const joinedSource = resolved;
|
|
259215
|
+
results.push(...this.collectAllReachableGates(joinedSource, modelDef, seen));
|
|
259216
|
+
if (joinedSource.type === "composite") {
|
|
259217
|
+
const members = joinedSource.sources;
|
|
259218
|
+
for (const member of members) {
|
|
259219
|
+
results.push(...this.collectAllReachableGates(member, modelDef, seen));
|
|
259220
|
+
}
|
|
259221
|
+
}
|
|
259222
|
+
}
|
|
259223
|
+
const duck = struct;
|
|
259224
|
+
if (duck.type === "query_source") {
|
|
259225
|
+
const ref = duck.query?.structRef;
|
|
259226
|
+
const base = typeof ref === "string" ? modelDef.contents[ref] : ref;
|
|
259227
|
+
if (base && isSourceDef2(base)) {
|
|
259228
|
+
results.push(...this.collectAllReachableGates(base, modelDef, seen));
|
|
259229
|
+
}
|
|
259230
|
+
const resolved = duck.query?.compositeResolvedSourceDef;
|
|
259231
|
+
if (resolved) {
|
|
259232
|
+
results.push(...this.collectAllReachableGates(resolved, modelDef, seen));
|
|
259233
|
+
}
|
|
259234
|
+
const innerJoins = (duck.query?.pipeline ?? []).flatMap((segment) => segment.extendSource ?? []);
|
|
259235
|
+
for (const field of innerJoins) {
|
|
259236
|
+
const { resolved: innerJoinSource, denyGate: innerJoinDenyGate } = this.classifyJoinedField(field, label);
|
|
259237
|
+
if (innerJoinDenyGate) {
|
|
259238
|
+
results.push(innerJoinDenyGate);
|
|
259239
|
+
continue;
|
|
259240
|
+
}
|
|
259241
|
+
if (!innerJoinSource)
|
|
259242
|
+
continue;
|
|
259243
|
+
results.push(...this.collectAllReachableGates(innerJoinSource, modelDef, seen));
|
|
259244
|
+
}
|
|
259245
|
+
}
|
|
259246
|
+
return results;
|
|
259247
|
+
}
|
|
258763
259248
|
async assertAuthorizedForText(text, givens) {
|
|
258764
259249
|
await this.assertAuthorized(extractRunTargetSourceName(text), givens);
|
|
258765
259250
|
}
|
|
258766
259251
|
async assertAuthorizedForRunnable(runnable, givens) {
|
|
258767
|
-
await this.
|
|
259252
|
+
await this.assertAuthorizedForAllSources(runnable, givens);
|
|
258768
259253
|
}
|
|
258769
259254
|
async resolveAuthorizeSourceFromRunnable(runnable) {
|
|
258770
259255
|
try {
|
|
@@ -259146,21 +259631,23 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
259146
259631
|
if (boundary === "deferred") {
|
|
259147
259632
|
this.assertQueryBoundaryCompiled(compiledSource, query);
|
|
259148
259633
|
}
|
|
259149
|
-
|
|
259150
|
-
await this.assertAuthorized(compiledSource, givens ?? {});
|
|
259151
|
-
}
|
|
259634
|
+
await this.assertAuthorizedForAllSources(runnable, givens ?? {});
|
|
259152
259635
|
const maxRows = getMaxQueryRows();
|
|
259153
259636
|
const maxBytes = getMaxResponseBytes();
|
|
259154
259637
|
const buildManifest = this.resolveFreshBuildManifest();
|
|
259155
259638
|
let rowLimit = 0;
|
|
259156
259639
|
let executionTime = 0;
|
|
259157
259640
|
let queryResults;
|
|
259641
|
+
const querySurfaceGivens = this.filterGivensToModelSurface(givens);
|
|
259158
259642
|
try {
|
|
259159
|
-
rowLimit = resolveModelQueryRowLimit((await runnable.getPreparedResult({
|
|
259643
|
+
rowLimit = resolveModelQueryRowLimit((await runnable.getPreparedResult({
|
|
259644
|
+
givens: querySurfaceGivens,
|
|
259645
|
+
buildManifest
|
|
259646
|
+
})).resultExplore.limit, { defaultLimit: getDefaultQueryRowLimit(), maxRows });
|
|
259160
259647
|
executionTime = performance.now() - startTime;
|
|
259161
259648
|
queryResults = await runnable.run({
|
|
259162
259649
|
rowLimit,
|
|
259163
|
-
givens,
|
|
259650
|
+
givens: querySurfaceGivens,
|
|
259164
259651
|
abortSignal,
|
|
259165
259652
|
buildManifest
|
|
259166
259653
|
});
|
|
@@ -259276,8 +259763,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
259276
259763
|
};
|
|
259277
259764
|
}
|
|
259278
259765
|
if (cell.runnable) {
|
|
259279
|
-
|
|
259280
|
-
await this.assertAuthorized(authorizeSource, givens ?? {});
|
|
259766
|
+
await this.assertAuthorizedForAllSources(cell.runnable, givens ?? {});
|
|
259281
259767
|
}
|
|
259282
259768
|
let queryName = undefined;
|
|
259283
259769
|
let queryResult = undefined;
|
|
@@ -259300,8 +259786,9 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
259300
259786
|
const cellMaxRows = getMaxQueryRows();
|
|
259301
259787
|
const cellMaxBytes = getMaxResponseBytes();
|
|
259302
259788
|
const buildManifest = this.resolveFreshBuildManifest();
|
|
259789
|
+
const cellSurfaceGivens = this.filterGivensToModelSurface(givens);
|
|
259303
259790
|
const rowLimit = resolveModelQueryRowLimit((await runnableToExecute.getPreparedResult({
|
|
259304
|
-
givens,
|
|
259791
|
+
givens: cellSurfaceGivens,
|
|
259305
259792
|
buildManifest
|
|
259306
259793
|
})).resultExplore.limit, {
|
|
259307
259794
|
defaultLimit: getDefaultQueryRowLimit(),
|
|
@@ -259309,7 +259796,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
259309
259796
|
});
|
|
259310
259797
|
const result = await runnableToExecute.run({
|
|
259311
259798
|
rowLimit,
|
|
259312
|
-
givens,
|
|
259799
|
+
givens: cellSurfaceGivens,
|
|
259313
259800
|
abortSignal,
|
|
259314
259801
|
buildManifest
|
|
259315
259802
|
});
|
|
@@ -260491,7 +260978,11 @@ class Package {
|
|
|
260491
260978
|
malloyConfig.wrapConnections((base) => ({
|
|
260492
260979
|
lookupConnection: async (name) => {
|
|
260493
260980
|
if (!name || name === "duckdb") {
|
|
260494
|
-
|
|
260981
|
+
const connection = await base.lookupConnection(name);
|
|
260982
|
+
if (connection instanceof DuckDBConnection3) {
|
|
260983
|
+
await applyExtensionSessionSettings(connection);
|
|
260984
|
+
}
|
|
260985
|
+
return connection;
|
|
260495
260986
|
}
|
|
260496
260987
|
return getEnvironmentMalloyConfig().connections.lookupConnection(name);
|
|
260497
260988
|
}
|
|
@@ -260593,6 +261084,7 @@ class Environment {
|
|
|
260593
261084
|
packageMutexes = new Map;
|
|
260594
261085
|
packageStatuses = new Map;
|
|
260595
261086
|
failedPackages = new Map;
|
|
261087
|
+
mountErrors = new Map;
|
|
260596
261088
|
malloyConfig;
|
|
260597
261089
|
connectionMutex = new Mutex;
|
|
260598
261090
|
retiredConnectionGenerations = new Set;
|
|
@@ -260729,7 +261221,7 @@ ${source}` : source;
|
|
|
260729
261221
|
if (queryMaterializer && gateModel) {
|
|
260730
261222
|
await gateModel.assertQueryBoundaryForRunnable(queryMaterializer, source);
|
|
260731
261223
|
}
|
|
260732
|
-
if (queryMaterializer && gateModel
|
|
261224
|
+
if (queryMaterializer && gateModel) {
|
|
260733
261225
|
await gateModel.assertAuthorizedForRunnable(queryMaterializer, givens ?? {});
|
|
260734
261226
|
}
|
|
260735
261227
|
let sql;
|
|
@@ -260915,7 +261407,7 @@ ${source}` : source;
|
|
|
260915
261407
|
}
|
|
260916
261408
|
this.packages.set(packageName, _package);
|
|
260917
261409
|
this.setPackageStatus(packageName, "serving" /* SERVING */);
|
|
260918
|
-
this.
|
|
261410
|
+
this.clearPackageLoadFailure(packageName);
|
|
260919
261411
|
logger.debug(`Successfully loaded package ${packageName}`);
|
|
260920
261412
|
return _package;
|
|
260921
261413
|
} catch (error) {
|
|
@@ -260956,7 +261448,7 @@ ${source}` : source;
|
|
|
260956
261448
|
throw error;
|
|
260957
261449
|
}
|
|
260958
261450
|
this.setPackageStatus(packageName, "serving" /* SERVING */);
|
|
260959
|
-
this.
|
|
261451
|
+
this.clearPackageLoadFailure(packageName);
|
|
260960
261452
|
return this.packages.get(packageName);
|
|
260961
261453
|
}
|
|
260962
261454
|
async installPackage(packageName, downloader, validate) {
|
|
@@ -261051,7 +261543,7 @@ ${source}` : source;
|
|
|
261051
261543
|
await this.bindManifestIfConfigured(newPackage);
|
|
261052
261544
|
this.packages.set(packageName, newPackage);
|
|
261053
261545
|
this.setPackageStatus(packageName, "serving" /* SERVING */);
|
|
261054
|
-
this.
|
|
261546
|
+
this.clearPackageLoadFailure(packageName);
|
|
261055
261547
|
if (oldPackage) {
|
|
261056
261548
|
this.retireConnectionGeneration(`package ${packageName}`, () => oldPackage.getMalloyConfig().shutdown("close"));
|
|
261057
261549
|
}
|
|
@@ -261229,8 +261721,17 @@ ${source}` : source;
|
|
|
261229
261721
|
getPackageStatus(packageName) {
|
|
261230
261722
|
return this.packageStatuses.get(packageName);
|
|
261231
261723
|
}
|
|
261724
|
+
setPackageMountError(packageName, message) {
|
|
261725
|
+
this.mountErrors.set(packageName, message);
|
|
261726
|
+
}
|
|
261727
|
+
clearPackageLoadFailure(packageName) {
|
|
261728
|
+
this.failedPackages.delete(packageName);
|
|
261729
|
+
this.mountErrors.delete(packageName);
|
|
261730
|
+
}
|
|
261232
261731
|
getFailedPackages() {
|
|
261233
|
-
|
|
261732
|
+
if (this.mountErrors.size === 0)
|
|
261733
|
+
return this.failedPackages;
|
|
261734
|
+
return new Map([...this.failedPackages, ...this.mountErrors]);
|
|
261234
261735
|
}
|
|
261235
261736
|
setPackageStatus(packageName, status) {
|
|
261236
261737
|
const currentStatus = this.packageStatuses.get(packageName);
|
|
@@ -261246,7 +261747,7 @@ ${source}` : source;
|
|
|
261246
261747
|
async deletePackage(packageName) {
|
|
261247
261748
|
assertSafePackageName(packageName);
|
|
261248
261749
|
return this.withPackageLock(packageName, async () => {
|
|
261249
|
-
this.
|
|
261750
|
+
this.clearPackageLoadFailure(packageName);
|
|
261250
261751
|
const _package = this.packages.get(packageName);
|
|
261251
261752
|
if (!_package) {
|
|
261252
261753
|
return;
|
|
@@ -261900,9 +262401,12 @@ class EnvironmentStore {
|
|
|
261900
262401
|
const environmentConfig = environmentManifest.environments.find((e) => e.name === environmentName);
|
|
261901
262402
|
const hasPackages = environment?.packages && environment.packages.length > 0 || environmentConfig?.packages && environmentConfig.packages.length > 0;
|
|
261902
262403
|
let absoluteEnvironmentPath;
|
|
262404
|
+
let mountErrors = new Map;
|
|
261903
262405
|
if (hasPackages) {
|
|
261904
262406
|
const packagesToProcess = environment?.packages || environmentConfig?.packages || [];
|
|
261905
|
-
|
|
262407
|
+
const loaded = await this.loadEnvironmentIntoDisk(environmentName, packagesToProcess);
|
|
262408
|
+
absoluteEnvironmentPath = loaded.path;
|
|
262409
|
+
mountErrors = loaded.mountErrors;
|
|
261906
262410
|
if (absoluteEnvironmentPath.endsWith(".zip")) {
|
|
261907
262411
|
absoluteEnvironmentPath = await this.unzipEnvironment(absoluteEnvironmentPath);
|
|
261908
262412
|
}
|
|
@@ -261914,6 +262418,9 @@ class EnvironmentStore {
|
|
|
261914
262418
|
if (!newEnvironment.metadata)
|
|
261915
262419
|
newEnvironment.metadata = {};
|
|
261916
262420
|
newEnvironment.metadata.location = absoluteEnvironmentPath;
|
|
262421
|
+
for (const [packageName, message] of mountErrors) {
|
|
262422
|
+
newEnvironment.setPackageMountError(packageName, message);
|
|
262423
|
+
}
|
|
261917
262424
|
this.environments.set(environmentName, newEnvironment);
|
|
261918
262425
|
this.failedEnvironments.delete(environmentName);
|
|
261919
262426
|
environment?.packages?.forEach((_package) => {
|
|
@@ -262058,9 +262565,17 @@ class EnvironmentStore {
|
|
|
262058
262565
|
isS3URL(location) {
|
|
262059
262566
|
return location.startsWith("s3://");
|
|
262060
262567
|
}
|
|
262568
|
+
recordMountFailure(error, location, packageNames, mountErrors) {
|
|
262569
|
+
logger.error(`Failed to download or mount location "${location}"`, this.extractErrorDataFromError(error));
|
|
262570
|
+
const message = redactPgSecrets(error instanceof Error ? error.message : String(error));
|
|
262571
|
+
for (const packageName of packageNames) {
|
|
262572
|
+
mountErrors.set(packageName, message);
|
|
262573
|
+
}
|
|
262574
|
+
}
|
|
262061
262575
|
async loadEnvironmentIntoDisk(environmentName, packages) {
|
|
262062
262576
|
assertSafePackageName(environmentName);
|
|
262063
262577
|
const absoluteTargetPath = safeJoinUnderRoot(this.serverRootPath, PUBLISHER_DATA_DIR, environmentName);
|
|
262578
|
+
const mountErrors = new Map;
|
|
262064
262579
|
await fs8.promises.mkdir(absoluteTargetPath, { recursive: true });
|
|
262065
262580
|
if (!packages || packages.length === 0) {
|
|
262066
262581
|
throw new PackageNotFoundError(`No packages found for environment ${environmentName}`);
|
|
@@ -262093,12 +262608,19 @@ class EnvironmentStore {
|
|
|
262093
262608
|
const tempDownloadPath = safeJoinUnderRoot(absoluteTargetPath, `.temp_${locationHash}`);
|
|
262094
262609
|
await fs8.promises.mkdir(tempDownloadPath, { recursive: true });
|
|
262095
262610
|
logger.info(`Created temporary directory: ${tempDownloadPath}`);
|
|
262611
|
+
let downloaded = true;
|
|
262096
262612
|
try {
|
|
262097
262613
|
await this.downloadOrMountLocation(groupedLocation, tempDownloadPath, environmentName, "shared");
|
|
262098
|
-
|
|
262614
|
+
} catch (error) {
|
|
262615
|
+
downloaded = false;
|
|
262616
|
+
this.recordMountFailure(error, groupedLocation, packagesForLocation.map((p) => p.name), mountErrors);
|
|
262617
|
+
}
|
|
262618
|
+
for (const _package of downloaded ? packagesForLocation : []) {
|
|
262619
|
+
let absolutePackagePath;
|
|
262620
|
+
try {
|
|
262099
262621
|
const packageDir = _package.name;
|
|
262100
262622
|
assertSafePackageName(packageDir);
|
|
262101
|
-
|
|
262623
|
+
absolutePackagePath = safeJoinUnderRoot(absoluteTargetPath, packageDir);
|
|
262102
262624
|
let sourcePath;
|
|
262103
262625
|
if (this.isGitHubURL(_package.location)) {
|
|
262104
262626
|
const githubInfo = this.parseGitHubUrl(_package.location);
|
|
@@ -262163,11 +262685,12 @@ class EnvironmentStore {
|
|
|
262163
262685
|
});
|
|
262164
262686
|
logger.info(`Copied entire download as package "${packageDir}"`);
|
|
262165
262687
|
}
|
|
262688
|
+
} catch (error) {
|
|
262689
|
+
this.recordMountFailure(error, groupedLocation, [_package.name], mountErrors);
|
|
262690
|
+
if (absolutePackagePath) {
|
|
262691
|
+
await clearMountTarget(absolutePackagePath);
|
|
262692
|
+
}
|
|
262166
262693
|
}
|
|
262167
|
-
} catch (error) {
|
|
262168
|
-
const errorData = this.extractErrorDataFromError(error);
|
|
262169
|
-
logger.error(`Failed to download or mount location "${groupedLocation}"`, errorData);
|
|
262170
|
-
throw new PackageNotFoundError(`Failed to download or mount location: ${groupedLocation}`);
|
|
262171
262694
|
}
|
|
262172
262695
|
try {
|
|
262173
262696
|
await fs8.promises.rm(tempDownloadPath, {
|
|
@@ -262180,7 +262703,7 @@ class EnvironmentStore {
|
|
|
262180
262703
|
});
|
|
262181
262704
|
}
|
|
262182
262705
|
}
|
|
262183
|
-
return absoluteTargetPath;
|
|
262706
|
+
return { path: absoluteTargetPath, mountErrors };
|
|
262184
262707
|
}
|
|
262185
262708
|
async downloadOrMountLocation(location, targetPath, environmentName, packageName) {
|
|
262186
262709
|
const isCompressedFile = location.endsWith(".zip");
|
|
@@ -266948,6 +267471,8 @@ my-package/
|
|
|
266948
267471
|
public/ # ONLY this directory is web-served
|
|
266949
267472
|
index.html
|
|
266950
267473
|
app.js
|
|
267474
|
+
vendor/ # chart library, vendored rather than loaded from a CDN
|
|
267475
|
+
chart.umd.js
|
|
266951
267476
|
\`\`\`
|
|
266952
267477
|
|
|
266953
267478
|
Only \`public/\` is reachable over the web, at \`/environments/<env>/packages/<pkg>/<file>\`. Models, data, and \`publisher.json\` are private and reached only through the query API, which still applies the model's filters, access modifiers, and authorize rules. There is no flag to set: a \`public/\` directory is what makes a package an app.
|
|
@@ -266959,7 +267484,7 @@ The agent orchestrates these. Each query and chart step hands off to a focused s
|
|
|
266959
267484
|
1. READ THE MODEL FIRST. Get the model's real source and view names, through your environment's context tool if it has one, or by opening the \`.malloy\` file directly. Never guess field or view names.
|
|
266960
267485
|
2. SCAFFOLD the package (template below).
|
|
266961
267486
|
3. WRITE THE QUERIES with \`skill:malloy-html-data-app-runtime\`. Validate each before pasting it into the page, using whatever query tool your environment provides or a running Publisher (see \`skill:malloy-html-data-app-runtime\`). Malloy syntax questions go to \`skill:malloy-queries\`.
|
|
266962
|
-
4. CHOOSE CHARTS with \`skill:malloy-charts\` when rendering through \`<malloy-render>\`; otherwise it is your own chart library drawing the returned rows. Vendor any chart library into \`public/\` and load it locally, not from a CDN
|
|
267487
|
+
4. CHOOSE CHARTS with \`skill:malloy-charts\` when rendering through \`<malloy-render>\`; otherwise it is your own chart library drawing the returned rows. Vendor any chart library into \`public/\` and load it locally, not from a CDN. Two reasons: embedded author JavaScript runs with the viewing user's data authority, and a blocked CDN (agent sandboxes and many corporate networks block them) is easy to miss, because the script never runs and the charts come up empty. The \`html-data-app\` and \`storefront\` examples both ship their chart library in \`public/vendor/\` and load it from \`public/index.html\` as \`./vendor/chart.umd.js\`. Copy that, but resolve the path against the page's own directory: a page in a subdirectory (\`public/reports/index.html\`) needs \`../vendor/chart.umd.js\`. A wrong relative path 404s and leaves the charts blank, which is the failure you are trying to avoid.
|
|
266963
267488
|
5. EMBED (optional) with \`skill:malloy-html-data-app-embedding\`.
|
|
266964
267489
|
6. PREVIEW with the local authoring loop (below).
|
|
266965
267490
|
7. VERIFY before you call it done (see "What 'done' means" below). This step is not optional.
|
|
@@ -269451,6 +269976,7 @@ var environmentStore = new EnvironmentStore(SERVER_ROOT);
|
|
|
269451
269976
|
var watchModeController = new WatchModeController(environmentStore);
|
|
269452
269977
|
var connectionController = new ConnectionController(environmentStore);
|
|
269453
269978
|
var modelController = new ModelController(environmentStore);
|
|
269979
|
+
logger.info(`DuckDB extension-fetch policy: ${getExtensionFetchPolicy()}`);
|
|
269454
269980
|
var memoryGovernorConfig = getMemoryGovernorConfig();
|
|
269455
269981
|
var memoryGovernor = memoryGovernorConfig ? new PackageMemoryGovernor(memoryGovernorConfig) : null;
|
|
269456
269982
|
memoryGovernor?.start();
|