@malloy-publisher/server 0.8.0 → 0.8.1
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 +105 -70
- package/dist/app/assets/{EnvironmentPage-Be-JAMS6.js → EnvironmentPage-dgZIJhMw.js} +1 -1
- package/dist/app/assets/{HomePage-DsdQpZuT.js → HomePage-D2FGM4cV.js} +1 -1
- package/dist/app/assets/{LightMode-DyjqECoH.js → LightMode-emBuHCdz.js} +1 -1
- package/dist/app/assets/{MainPage-DAaQnh6y.js → MainPage-BVqdH0TB.js} +1 -1
- package/dist/app/assets/{ModelPage-B7yaRDmM.js → ModelPage-DAJxkTmk.js} +2 -2
- package/dist/app/assets/{PackagePage-7b0klbWy.js → PackagePage-BGGcyDeJ.js} +1 -1
- package/dist/app/assets/{RenderedResult-CYyhQekT.es-DJ3BRrLl.js → RenderedResult-CDA9mN_6.es-C4f_ZzvB.js} +2 -2
- package/dist/app/assets/{RouteError-kdHl1qpH.js → RouteError-CtRFTa_q.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DIt45eWv.js → ThemeEditorPage-Bnd5dgI0.js} +1 -1
- package/dist/app/assets/{ToggleButtonGroup-Dl_oTC8r.js → ToggleButtonGroup-CxU1OIGN.js} +1 -1
- package/dist/app/assets/{index-t9-rAE4l.js → index-BWGMOIxQ.js} +1 -1
- package/dist/app/assets/{index-C1qGlR2a.js → index-CNQ_NxFx.js} +4 -4
- package/dist/app/assets/{index-T1Cl-rQv.js → index-OsUlLBi_.js} +1 -1
- package/dist/app/assets/{index-C5hv_y6G.js → index-aXk5ykLp.js} +1 -1
- package/dist/app/assets/{index-Bzqs2UuN.js → index-xgxo45Xn.js} +1 -1
- package/dist/app/assets/{index.es-BlAu_Rj1.js → index.es-BCvB2VgL.js} +22 -22
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +1328 -776
- package/dist/server.mjs +2469 -397
- package/package.json +1 -1
|
@@ -12905,6 +12905,8 @@ function internalErrorToHttpError(error) {
|
|
|
12905
12905
|
return httpError(422, error.message);
|
|
12906
12906
|
} else if (error instanceof ModelCompilationError) {
|
|
12907
12907
|
return httpError(424, error.message);
|
|
12908
|
+
} else if (error instanceof PackageManifestError) {
|
|
12909
|
+
return httpError(424, error.message);
|
|
12908
12910
|
} else if (error instanceof ConnectionError) {
|
|
12909
12911
|
if (error.callerSafe) {
|
|
12910
12912
|
return httpError(502, error.message);
|
|
@@ -12945,7 +12947,7 @@ function httpError(code, message, reason) {
|
|
|
12945
12947
|
}
|
|
12946
12948
|
};
|
|
12947
12949
|
}
|
|
12948
|
-
var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, MaterializationEligibilityError, PublisherConfigError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
|
|
12950
|
+
var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, PublisherConfigError, FrozenConfigError, AccessDeniedError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
|
|
12949
12951
|
var init_errors = __esm(() => {
|
|
12950
12952
|
init_constants();
|
|
12951
12953
|
init_logger();
|
|
@@ -13020,6 +13022,12 @@ var init_errors = __esm(() => {
|
|
|
13020
13022
|
super(error.message);
|
|
13021
13023
|
}
|
|
13022
13024
|
};
|
|
13025
|
+
PackageManifestError = class PackageManifestError extends Error {
|
|
13026
|
+
constructor(message) {
|
|
13027
|
+
super(message);
|
|
13028
|
+
this.name = "PackageManifestError";
|
|
13029
|
+
}
|
|
13030
|
+
};
|
|
13023
13031
|
MaterializationEligibilityError = class MaterializationEligibilityError extends Error {
|
|
13024
13032
|
reason;
|
|
13025
13033
|
constructor(error) {
|
|
@@ -13052,6 +13060,12 @@ var init_errors = __esm(() => {
|
|
|
13052
13060
|
this.name = "NotQueryableError";
|
|
13053
13061
|
}
|
|
13054
13062
|
};
|
|
13063
|
+
OffSurfaceError = class OffSurfaceError extends NotQueryableError {
|
|
13064
|
+
constructor(message) {
|
|
13065
|
+
super(message);
|
|
13066
|
+
this.name = "OffSurfaceError";
|
|
13067
|
+
}
|
|
13068
|
+
};
|
|
13055
13069
|
MaterializationNotFoundError = class MaterializationNotFoundError extends Error {
|
|
13056
13070
|
constructor(message) {
|
|
13057
13071
|
super(message);
|
|
@@ -13419,11 +13433,11 @@ function recordAuthorizeBypass(entryPoint) {
|
|
|
13419
13433
|
});
|
|
13420
13434
|
bypassCounter.add(1, { entry_point: entryPoint });
|
|
13421
13435
|
}
|
|
13422
|
-
function recordRowLevelGateDecision(decision) {
|
|
13436
|
+
function recordRowLevelGateDecision(decision, site = "entry_point") {
|
|
13423
13437
|
rowLevelDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_total", {
|
|
13424
|
-
description: "How a row-level `#(access_filter)` gate resolved a request.
|
|
13438
|
+
description: "How a row-level `#(access_filter)` gate resolved a request. Labels: decision ('denied_by_gate'|'empty_after_filter'), site ('entry_point'|'caller_join'). 'denied_by_gate' is the fail-closed refusal when the gate could not be applied; 'empty_after_filter' is a successful response with zero rows after the filter matched none, which is NOT an error. site says whether the gate was the query's own run target or a source the caller's text joined."
|
|
13425
13439
|
});
|
|
13426
|
-
rowLevelDecisionCounter.add(1, { decision });
|
|
13440
|
+
rowLevelDecisionCounter.add(1, { decision, site });
|
|
13427
13441
|
}
|
|
13428
13442
|
function recordRowLevelGateRejected(cause) {
|
|
13429
13443
|
rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
|
|
@@ -13437,11 +13451,11 @@ function recordAuthorizeAdmitAllGate(route) {
|
|
|
13437
13451
|
});
|
|
13438
13452
|
admitAllCounter.add(1, { route });
|
|
13439
13453
|
}
|
|
13440
|
-
function recordLockDecision(decision) {
|
|
13454
|
+
function recordLockDecision(decision, site = "entry_point") {
|
|
13441
13455
|
lockDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_lock_total", {
|
|
13442
|
-
description: "How a `#(authorize)` lock resolved a request.
|
|
13456
|
+
description: "How a `#(authorize)` lock resolved a request. Labels: decision ('admitted'|'denied_by_lock'|'denied_unresolvable'), site ('entry_point'|'caller_join'). Both denials are a 403; 'denied_by_lock' is the gate refusing a caller it does not admit (routine), 'denied_unresolvable' is the fail-closed refusal when the gate could not be decided at all. site says whether the lock was on the query's own run target or on a source the caller's text joined."
|
|
13443
13457
|
});
|
|
13444
|
-
lockDecisionCounter.add(1, { decision });
|
|
13458
|
+
lockDecisionCounter.add(1, { decision, site });
|
|
13445
13459
|
}
|
|
13446
13460
|
var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null, admitAllCounter = null, lockDecisionCounter = null;
|
|
13447
13461
|
var init_authorize_metrics = __esm(() => {
|
|
@@ -14465,858 +14479,1370 @@ var init_gate_classification = __esm(() => {
|
|
|
14465
14479
|
init_gate_registry_walk();
|
|
14466
14480
|
});
|
|
14467
14481
|
|
|
14468
|
-
// src/service/
|
|
14469
|
-
|
|
14470
|
-
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
switch (key) {
|
|
14487
|
-
case "name":
|
|
14488
|
-
name = value;
|
|
14489
|
-
break;
|
|
14490
|
-
case "dimension":
|
|
14491
|
-
dimension = value;
|
|
14492
|
-
break;
|
|
14493
|
-
case "type":
|
|
14494
|
-
if (!VALID_FILTER_TYPES.has(value)) {
|
|
14495
|
-
throw new Error(`Invalid filter type "${value}". Must be one of: ${[...VALID_FILTER_TYPES].join(", ")}`);
|
|
14496
|
-
}
|
|
14497
|
-
type = value;
|
|
14498
|
-
break;
|
|
14499
|
-
default:
|
|
14500
|
-
throw new Error(`Unknown filter parameter "${key}"`);
|
|
14501
|
-
}
|
|
14482
|
+
// src/service/motly.ts
|
|
14483
|
+
import { parseAnnotation } from "@malloydata/malloy-tag";
|
|
14484
|
+
function onMotlyRoute(text) {
|
|
14485
|
+
const afterSigil = text.replace(/^##?\|?/, "");
|
|
14486
|
+
return afterSigil === "" || /^[ \t\r\n]/.test(afterSigil);
|
|
14487
|
+
}
|
|
14488
|
+
function hasEnvReference(annotation) {
|
|
14489
|
+
return annotation.includes("@env.");
|
|
14490
|
+
}
|
|
14491
|
+
function pollutionTargets() {
|
|
14492
|
+
const targets = [Object.prototype];
|
|
14493
|
+
for (const key of Object.getOwnPropertyNames(Object.prototype)) {
|
|
14494
|
+
const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, key);
|
|
14495
|
+
if (!descriptor)
|
|
14496
|
+
continue;
|
|
14497
|
+
let value;
|
|
14498
|
+
if ("value" in descriptor) {
|
|
14499
|
+
value = descriptor.value;
|
|
14502
14500
|
} else {
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
implicit = true;
|
|
14506
|
-
} else if (flag === "required") {
|
|
14507
|
-
required = true;
|
|
14508
|
-
} else {
|
|
14509
|
-
throw new Error(`Unknown filter flag "${token}"`);
|
|
14501
|
+
if (BUILT_IN_PROTO_GETTER === undefined || descriptor.get !== BUILT_IN_PROTO_GETTER) {
|
|
14502
|
+
return;
|
|
14510
14503
|
}
|
|
14504
|
+
continue;
|
|
14505
|
+
}
|
|
14506
|
+
if ((typeof value === "object" || typeof value === "function") && value !== null && !targets.includes(value)) {
|
|
14507
|
+
targets.push(value);
|
|
14511
14508
|
}
|
|
14512
14509
|
}
|
|
14513
|
-
|
|
14514
|
-
throw new Error("filter annotation missing required 'dimension' parameter");
|
|
14515
|
-
}
|
|
14516
|
-
if (!type) {
|
|
14517
|
-
throw new Error("filter annotation missing required 'type' parameter");
|
|
14518
|
-
}
|
|
14519
|
-
return {
|
|
14520
|
-
name: name ?? dimension,
|
|
14521
|
-
dimension,
|
|
14522
|
-
type,
|
|
14523
|
-
implicit,
|
|
14524
|
-
required
|
|
14525
|
-
};
|
|
14510
|
+
return targets;
|
|
14526
14511
|
}
|
|
14527
|
-
function
|
|
14528
|
-
const
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14512
|
+
function parseGuarded(texts) {
|
|
14513
|
+
const targets = pollutionTargets();
|
|
14514
|
+
if (targets === undefined) {
|
|
14515
|
+
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
14516
|
+
}
|
|
14517
|
+
let unreadable = false;
|
|
14518
|
+
const ownNames = (target) => {
|
|
14519
|
+
try {
|
|
14520
|
+
return Object.getOwnPropertyNames(target);
|
|
14521
|
+
} catch {
|
|
14522
|
+
unreadable = true;
|
|
14523
|
+
return [];
|
|
14533
14524
|
}
|
|
14525
|
+
};
|
|
14526
|
+
const before = targets.map(ownNames);
|
|
14527
|
+
if (unreadable)
|
|
14528
|
+
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
14529
|
+
const undoPollution = () => {
|
|
14530
|
+
let polluted = false;
|
|
14531
|
+
targets.forEach((target, index) => {
|
|
14532
|
+
for (const key of ownNames(target)) {
|
|
14533
|
+
if (before[index].includes(key))
|
|
14534
|
+
continue;
|
|
14535
|
+
polluted = true;
|
|
14536
|
+
try {
|
|
14537
|
+
delete target[key];
|
|
14538
|
+
} catch {}
|
|
14539
|
+
}
|
|
14540
|
+
});
|
|
14541
|
+
return polluted || unreadable;
|
|
14542
|
+
};
|
|
14543
|
+
if (texts.some((text) => text.length > MAX_ANNOTATION_CHARS)) {
|
|
14544
|
+
return { tag: undefined, messages: [ANNOTATION_TOO_LONG] };
|
|
14545
|
+
}
|
|
14546
|
+
try {
|
|
14547
|
+
const result = parseAnnotation([...texts]);
|
|
14548
|
+
if (undoPollution())
|
|
14549
|
+
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
14550
|
+
return {
|
|
14551
|
+
tag: result.tag,
|
|
14552
|
+
messages: result.log.map((error) => error.message)
|
|
14553
|
+
};
|
|
14554
|
+
} catch {
|
|
14555
|
+
undoPollution();
|
|
14556
|
+
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
14534
14557
|
}
|
|
14535
|
-
return [...byName.values()];
|
|
14536
|
-
}
|
|
14537
|
-
function escapeMalloyString(value) {
|
|
14538
|
-
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
14539
14558
|
}
|
|
14540
|
-
function
|
|
14541
|
-
const
|
|
14542
|
-
return
|
|
14559
|
+
function docCommentText(texts) {
|
|
14560
|
+
const lines = texts.filter((text) => /^##?\|?"([ \t\r\n]|$)/.test(text)).map((text) => text.replace(/^##?\|?"[ \t\r\n]?/, "").trimEnd());
|
|
14561
|
+
return lines.some((text) => text.trim().length > 0) ? lines.join(`
|
|
14562
|
+
`) : undefined;
|
|
14543
14563
|
}
|
|
14544
|
-
function
|
|
14545
|
-
|
|
14564
|
+
function splitDocComment(texts) {
|
|
14565
|
+
const text = docCommentText(texts);
|
|
14566
|
+
if (text === undefined)
|
|
14567
|
+
return {};
|
|
14568
|
+
const lines = text.split(`
|
|
14569
|
+
`);
|
|
14570
|
+
const titleLine = lines.findIndex((line) => line.trim().length > 0);
|
|
14571
|
+
if (titleLine === -1)
|
|
14572
|
+
return {};
|
|
14573
|
+
const body = lines.slice(titleLine + 1).join(`
|
|
14574
|
+
`).replace(/^[ \t\r\n]+/, "").trimEnd();
|
|
14575
|
+
return {
|
|
14576
|
+
title: lines[titleLine].trim(),
|
|
14577
|
+
body: body.length > 0 ? body : undefined
|
|
14578
|
+
};
|
|
14546
14579
|
}
|
|
14547
|
-
function
|
|
14548
|
-
if (
|
|
14549
|
-
return
|
|
14550
|
-
}
|
|
14551
|
-
if (isDateLiteral(v)) {
|
|
14552
|
-
return `@${v.slice(0, 10)}`;
|
|
14580
|
+
function docCommentTitleAndDescription(texts, explicitTitle) {
|
|
14581
|
+
if (explicitTitle !== undefined) {
|
|
14582
|
+
return { title: explicitTitle, description: docCommentText(texts) };
|
|
14553
14583
|
}
|
|
14554
|
-
|
|
14584
|
+
const { title, body } = splitDocComment(texts);
|
|
14585
|
+
return { title, description: body };
|
|
14555
14586
|
}
|
|
14556
|
-
function
|
|
14557
|
-
const
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14587
|
+
function quoteFilterLiterals(annotation) {
|
|
14588
|
+
const VALUE_START = /[=[,]/;
|
|
14589
|
+
const BARE_FILTER_LITERAL = /^([ \t]*)f(['"])((?:\\.|(?!\2)[^\\])*)\2/;
|
|
14590
|
+
let out = "";
|
|
14591
|
+
let i = 0;
|
|
14592
|
+
while (i < annotation.length) {
|
|
14593
|
+
const char = annotation[i];
|
|
14594
|
+
const delimited = endOfDelimited(annotation, i);
|
|
14595
|
+
if (delimited > i) {
|
|
14596
|
+
out += annotation.slice(i, delimited);
|
|
14597
|
+
i = delimited;
|
|
14598
|
+
continue;
|
|
14562
14599
|
}
|
|
14563
|
-
|
|
14564
|
-
const
|
|
14565
|
-
if (
|
|
14566
|
-
|
|
14600
|
+
if (VALUE_START.test(char)) {
|
|
14601
|
+
const match = BARE_FILTER_LITERAL.exec(annotation.slice(i + 1));
|
|
14602
|
+
if (match) {
|
|
14603
|
+
const body = match[3].replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
14604
|
+
out += `${char}${match[1]}"f'${body}'"`;
|
|
14605
|
+
i += 1 + match[0].length;
|
|
14606
|
+
continue;
|
|
14567
14607
|
}
|
|
14568
|
-
const conditions = values.map((v) => `${dim} = ${malloyLiteral(v)}`);
|
|
14569
|
-
return `(${conditions.join(" or ")})`;
|
|
14570
|
-
}
|
|
14571
|
-
case "like": {
|
|
14572
|
-
const v = Array.isArray(value) ? value[0] : value;
|
|
14573
|
-
const escaped = escapeMalloyString(v.toLowerCase());
|
|
14574
|
-
const pattern = escaped.startsWith("%") || escaped.endsWith("%") ? escaped : `%${escaped}%`;
|
|
14575
|
-
return `lower(${dim}) ~ '${pattern}'`;
|
|
14576
|
-
}
|
|
14577
|
-
case "greater_than": {
|
|
14578
|
-
const v = Array.isArray(value) ? value[0] : value;
|
|
14579
|
-
return `${dim} > ${malloyLiteral(v)}`;
|
|
14580
|
-
}
|
|
14581
|
-
case "less_than": {
|
|
14582
|
-
const v = Array.isArray(value) ? value[0] : value;
|
|
14583
|
-
return `${dim} < ${malloyLiteral(v)}`;
|
|
14584
14608
|
}
|
|
14609
|
+
out += char;
|
|
14610
|
+
i += 1;
|
|
14585
14611
|
}
|
|
14612
|
+
return out;
|
|
14586
14613
|
}
|
|
14587
|
-
function
|
|
14588
|
-
const
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14614
|
+
function endOfDelimited(text, start) {
|
|
14615
|
+
const char = text[start];
|
|
14616
|
+
if (char === "`") {
|
|
14617
|
+
let i = start + 1;
|
|
14618
|
+
while (i < text.length) {
|
|
14619
|
+
if (text[i] === "\\") {
|
|
14620
|
+
i += 2;
|
|
14621
|
+
continue;
|
|
14595
14622
|
}
|
|
14596
|
-
|
|
14623
|
+
if (text[i] === "`")
|
|
14624
|
+
return i + 1;
|
|
14625
|
+
if (text[i] === `
|
|
14626
|
+
`)
|
|
14627
|
+
return text.length;
|
|
14628
|
+
i += 1;
|
|
14597
14629
|
}
|
|
14598
|
-
|
|
14599
|
-
}
|
|
14600
|
-
if (predicates.length === 0) {
|
|
14601
|
-
return "";
|
|
14602
|
-
}
|
|
14603
|
-
return predicates.join(" and ");
|
|
14604
|
-
}
|
|
14605
|
-
function injectFilterRefinement(query, filterClause) {
|
|
14606
|
-
if (!filterClause) {
|
|
14607
|
-
return query;
|
|
14630
|
+
return text.length;
|
|
14608
14631
|
}
|
|
14609
|
-
|
|
14610
|
-
+
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14632
|
+
if (text.startsWith("<<<", start)) {
|
|
14633
|
+
let lineStart = start + 3;
|
|
14634
|
+
for (;; ) {
|
|
14635
|
+
const newline = text.indexOf(`
|
|
14636
|
+
`, lineStart);
|
|
14637
|
+
const lineEnd = newline === -1 ? text.length : newline;
|
|
14638
|
+
if (text.slice(lineStart, lineEnd).trim() === ">>>")
|
|
14639
|
+
return lineEnd;
|
|
14640
|
+
if (newline === -1)
|
|
14641
|
+
return text.length;
|
|
14642
|
+
lineStart = newline + 1;
|
|
14643
|
+
}
|
|
14644
|
+
}
|
|
14645
|
+
if (char === '"' || char === "'") {
|
|
14646
|
+
const triple = char.repeat(3);
|
|
14647
|
+
if (text.startsWith(triple, start)) {
|
|
14648
|
+
let i2 = start + triple.length;
|
|
14649
|
+
while (i2 < text.length) {
|
|
14650
|
+
if (text[i2] === "\\") {
|
|
14651
|
+
i2 += 2;
|
|
14652
|
+
continue;
|
|
14653
|
+
}
|
|
14654
|
+
if (text.startsWith(triple, i2))
|
|
14655
|
+
return i2 + triple.length;
|
|
14656
|
+
i2 += 1;
|
|
14623
14657
|
}
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
if (
|
|
14629
|
-
|
|
14630
|
-
|
|
14658
|
+
return text.length;
|
|
14659
|
+
}
|
|
14660
|
+
let i = start + 1;
|
|
14661
|
+
while (i < text.length) {
|
|
14662
|
+
if (text[i] === "\\") {
|
|
14663
|
+
i += 2;
|
|
14664
|
+
continue;
|
|
14631
14665
|
}
|
|
14632
|
-
|
|
14633
|
-
|
|
14666
|
+
if (text[i] === char)
|
|
14667
|
+
return i + 1;
|
|
14668
|
+
if (text[i] === `
|
|
14669
|
+
`)
|
|
14670
|
+
return i;
|
|
14671
|
+
i += 1;
|
|
14634
14672
|
}
|
|
14673
|
+
return text.length;
|
|
14635
14674
|
}
|
|
14636
|
-
|
|
14637
|
-
|
|
14675
|
+
return start;
|
|
14676
|
+
}
|
|
14677
|
+
function unwrapFilterLiteral(value) {
|
|
14678
|
+
const match = /^f(['"])([\s\S]*)\1$/.exec(value);
|
|
14679
|
+
return match ? match[2] : value;
|
|
14680
|
+
}
|
|
14681
|
+
function parseMotly(texts) {
|
|
14682
|
+
const onRoute = texts.filter(onMotlyRoute);
|
|
14683
|
+
const motly = onRoute.filter((text) => !hasEnvReference(text));
|
|
14684
|
+
const envDropped = onRoute.length - motly.length;
|
|
14685
|
+
const envErrors = envDropped > 0 ? [ENV_REFERENCE_DROPPED] : [];
|
|
14686
|
+
if (motly.length === 0)
|
|
14687
|
+
return { tag: undefined, errors: envErrors };
|
|
14688
|
+
const direct = parseGuarded(motly);
|
|
14689
|
+
if (direct.messages.length === 0)
|
|
14690
|
+
return { tag: direct.tag, errors: envErrors };
|
|
14691
|
+
const rescued = motly.map((text) => {
|
|
14692
|
+
if (parseGuarded([text]).messages.length === 0)
|
|
14693
|
+
return text;
|
|
14694
|
+
const rewritten = quoteFilterLiterals(text);
|
|
14695
|
+
return parseGuarded([rewritten]).messages.length === 0 ? rewritten : text;
|
|
14696
|
+
});
|
|
14697
|
+
const after = parseGuarded(rescued);
|
|
14698
|
+
return { tag: after.tag, errors: [...envErrors, ...after.messages] };
|
|
14699
|
+
}
|
|
14700
|
+
function tagText(tag, ...path) {
|
|
14701
|
+
try {
|
|
14702
|
+
return tag?.text(...path);
|
|
14703
|
+
} catch {
|
|
14704
|
+
return;
|
|
14638
14705
|
}
|
|
14639
|
-
return tokens;
|
|
14640
14706
|
}
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14707
|
+
function tagNumeric(tag, ...path) {
|
|
14708
|
+
try {
|
|
14709
|
+
const raw = tagText(tag, ...path);
|
|
14710
|
+
if (raw === undefined)
|
|
14711
|
+
return;
|
|
14712
|
+
const DECIMAL = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/;
|
|
14713
|
+
const text = raw.trim();
|
|
14714
|
+
if (!DECIMAL.test(text))
|
|
14715
|
+
return;
|
|
14716
|
+
const value = Number(text);
|
|
14717
|
+
return Number.isFinite(value) ? value : undefined;
|
|
14718
|
+
} catch {
|
|
14719
|
+
return;
|
|
14720
|
+
}
|
|
14721
|
+
}
|
|
14722
|
+
function motlyTag(texts) {
|
|
14723
|
+
return parseMotly(texts).tag;
|
|
14724
|
+
}
|
|
14725
|
+
function motlyParseErrors(texts) {
|
|
14726
|
+
return parseMotly(texts).errors;
|
|
14727
|
+
}
|
|
14728
|
+
function readAutorun(tag) {
|
|
14729
|
+
return tagText(tag, "autorun") !== "false";
|
|
14730
|
+
}
|
|
14731
|
+
function scalarTypeOf(tag) {
|
|
14732
|
+
try {
|
|
14733
|
+
return tag?.scalarType();
|
|
14734
|
+
} catch {
|
|
14735
|
+
return;
|
|
14736
|
+
}
|
|
14737
|
+
}
|
|
14738
|
+
function authoredDay(isoText) {
|
|
14739
|
+
const value = new Date(isoText);
|
|
14740
|
+
if (Number.isNaN(value.getTime()))
|
|
14741
|
+
return isoText.slice(0, 10);
|
|
14742
|
+
const atUtcMidnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0;
|
|
14743
|
+
const year = atUtcMidnight ? value.getUTCFullYear() : value.getFullYear();
|
|
14744
|
+
const month = atUtcMidnight ? value.getUTCMonth() : value.getMonth();
|
|
14745
|
+
const day = atUtcMidnight ? value.getUTCDate() : value.getDate();
|
|
14746
|
+
return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
14747
|
+
}
|
|
14748
|
+
function readStartingGivens(tag, declaredType) {
|
|
14749
|
+
const entries = tag?.tag("givens");
|
|
14750
|
+
if (!entries)
|
|
14751
|
+
return;
|
|
14752
|
+
const collected = {};
|
|
14753
|
+
for (const [name, value] of entries.entries()) {
|
|
14754
|
+
const tag2 = value;
|
|
14755
|
+
const text = tagText(tag2);
|
|
14756
|
+
if (text === undefined)
|
|
14757
|
+
continue;
|
|
14758
|
+
const declared = declaredType(name);
|
|
14759
|
+
collected[name] = scalarTypeOf(tag2) === "date" && declared !== "timestamp" && declared !== "timestamptz" ? authoredDay(text) : declared?.startsWith("filter<") ? unwrapFilterLiteral(text) : text;
|
|
14760
|
+
}
|
|
14761
|
+
return Object.keys(collected).length > 0 ? collected : undefined;
|
|
14762
|
+
}
|
|
14763
|
+
var BUILT_IN_PROTO_GETTER, MAX_ANNOTATION_CHARS = 8192, ANNOTATION_TOO_LONG, ENV_REFERENCE_DROPPED = "annotation dropped for carrying an @env. reference", UNSAFE_TO_PARSE = "annotation could not be parsed safely";
|
|
14764
|
+
var init_motly = __esm(() => {
|
|
14765
|
+
BUILT_IN_PROTO_GETTER = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__")?.get;
|
|
14766
|
+
ANNOTATION_TOO_LONG = `annotation exceeds ${MAX_ANNOTATION_CHARS} characters and was not parsed`;
|
|
14658
14767
|
});
|
|
14659
14768
|
|
|
14660
|
-
// src/service/
|
|
14769
|
+
// src/service/given.ts
|
|
14661
14770
|
import {
|
|
14662
|
-
|
|
14663
|
-
isSourceDef as isSourceDef4,
|
|
14664
|
-
modelDefToModelInfo
|
|
14771
|
+
isSourceDef as isSourceDef4
|
|
14665
14772
|
} from "@malloydata/malloy";
|
|
14666
|
-
function
|
|
14667
|
-
const
|
|
14668
|
-
|
|
14773
|
+
function presentText(tag, ...path) {
|
|
14774
|
+
const raw = tagText(tag, ...path);
|
|
14775
|
+
return raw !== undefined && raw.trim() !== "" ? raw : undefined;
|
|
14776
|
+
}
|
|
14777
|
+
function readGivenControlSpec(annotationTexts2) {
|
|
14778
|
+
const spec = {};
|
|
14779
|
+
const tag = motlyTag(annotationTexts2);
|
|
14780
|
+
if (!tag)
|
|
14781
|
+
return spec;
|
|
14782
|
+
const label = presentText(tag, "label");
|
|
14783
|
+
if (label !== undefined)
|
|
14784
|
+
spec.label = label;
|
|
14785
|
+
const description = presentText(tag, "description");
|
|
14786
|
+
if (description !== undefined)
|
|
14787
|
+
spec.description = description;
|
|
14788
|
+
const control = tagText(tag, "control");
|
|
14789
|
+
if (control === "select" || control === "multiselect") {
|
|
14790
|
+
spec.control = control;
|
|
14791
|
+
}
|
|
14792
|
+
const rangeMin = tagNumeric(tag, "range_min");
|
|
14793
|
+
if (rangeMin !== undefined)
|
|
14794
|
+
spec.rangeMin = rangeMin;
|
|
14795
|
+
const rangeMax = tagNumeric(tag, "range_max");
|
|
14796
|
+
if (rangeMax !== undefined)
|
|
14797
|
+
spec.rangeMax = rangeMax;
|
|
14798
|
+
const suggest = tag.tag("suggest");
|
|
14799
|
+
if (suggest) {
|
|
14800
|
+
const parsed = {};
|
|
14801
|
+
const query = presentText(suggest, "query");
|
|
14802
|
+
if (query !== undefined)
|
|
14803
|
+
parsed.query = query;
|
|
14804
|
+
const source = presentText(suggest, "source");
|
|
14805
|
+
if (source !== undefined)
|
|
14806
|
+
parsed.source = source;
|
|
14807
|
+
const dimension = presentText(suggest, "dimension");
|
|
14808
|
+
if (dimension !== undefined)
|
|
14809
|
+
parsed.dimension = dimension;
|
|
14810
|
+
const runnable = parsed.query !== undefined || parsed.source !== undefined && parsed.dimension !== undefined;
|
|
14811
|
+
if (runnable)
|
|
14812
|
+
spec.suggest = parsed;
|
|
14813
|
+
}
|
|
14814
|
+
return spec;
|
|
14815
|
+
}
|
|
14816
|
+
function malloyGivenToApi(given) {
|
|
14817
|
+
const type = given.type;
|
|
14818
|
+
const renderedType = type.type === "filter expression" ? `filter<${type.filterType}>` : type.type === "array" && type.elementTypeDef?.type ? `${type.elementTypeDef.type}[]` : type.type;
|
|
14819
|
+
const allNotes = given.annotations.forRoute(undefined);
|
|
14820
|
+
return {
|
|
14821
|
+
name: given.name,
|
|
14822
|
+
type: renderedType,
|
|
14823
|
+
annotations: allNotes.filter((note) => !isReservedRoute(note.route)).map((note) => note.text),
|
|
14824
|
+
...readGivenControlSpec(allNotes.map((note) => note.text)),
|
|
14825
|
+
default: given._internal?.defaultText
|
|
14826
|
+
};
|
|
14827
|
+
}
|
|
14828
|
+
function collectGivenRefs(value, into) {
|
|
14829
|
+
if (Array.isArray(value)) {
|
|
14830
|
+
for (const item of value)
|
|
14831
|
+
collectGivenRefs(item, into);
|
|
14832
|
+
return;
|
|
14833
|
+
}
|
|
14834
|
+
if (value === null || typeof value !== "object")
|
|
14835
|
+
return;
|
|
14836
|
+
const node = value;
|
|
14837
|
+
if (node.node === "given" && typeof node.refName === "string") {
|
|
14838
|
+
into.add(node.refName);
|
|
14839
|
+
}
|
|
14840
|
+
for (const child of Object.values(node))
|
|
14841
|
+
collectGivenRefs(child, into);
|
|
14842
|
+
}
|
|
14843
|
+
function gateGivenSource(sources, name) {
|
|
14844
|
+
const source = sources.find((candidate) => candidate.name === name);
|
|
14845
|
+
if (!source)
|
|
14846
|
+
return;
|
|
14847
|
+
return [...source.authorize ?? [], ...source.accessFilter ?? []];
|
|
14848
|
+
}
|
|
14849
|
+
function suggestGivenLookup(modelDef, gatesBySource, surfaced) {
|
|
14850
|
+
const registry = modelDef.givens ?? {};
|
|
14851
|
+
const bySource = new Map;
|
|
14852
|
+
const byQuery = new Map;
|
|
14853
|
+
for (const obj of Object.values(modelDef.contents)) {
|
|
14854
|
+
if (isSourceDef4(obj)) {
|
|
14855
|
+
const name = obj.as || obj.name;
|
|
14856
|
+
const refs = new Set;
|
|
14857
|
+
collectGivenRefs(obj.filterList, refs);
|
|
14858
|
+
for (const expr of gatesBySource(name) ?? []) {
|
|
14859
|
+
for (const given of referencedGivenNames(expr))
|
|
14860
|
+
refs.add(given);
|
|
14861
|
+
}
|
|
14862
|
+
bySource.set(name, Array.from(refs));
|
|
14863
|
+
} else if (obj.type === "query") {
|
|
14864
|
+
const query = obj;
|
|
14865
|
+
byQuery.set(query.as || query.name, {
|
|
14866
|
+
own: (query.givenUsage ?? []).map((usage) => registry[usage.id]?.name).filter((n) => n !== undefined),
|
|
14867
|
+
source: typeof query.structRef === "string" ? query.structRef : undefined
|
|
14868
|
+
});
|
|
14869
|
+
}
|
|
14870
|
+
}
|
|
14871
|
+
const narrow = (names) => Array.from(new Set(names)).filter((name) => surfaced === undefined || surfaced.has(name));
|
|
14872
|
+
return {
|
|
14873
|
+
forSource: (name) => {
|
|
14874
|
+
const found = bySource.get(name);
|
|
14875
|
+
return found && narrow(found);
|
|
14876
|
+
},
|
|
14877
|
+
forQuery: (name) => {
|
|
14878
|
+
const found = byQuery.get(name);
|
|
14879
|
+
if (!found)
|
|
14880
|
+
return;
|
|
14881
|
+
return narrow([
|
|
14882
|
+
...found.own,
|
|
14883
|
+
...found.source ? bySource.get(found.source) ?? [] : []
|
|
14884
|
+
]);
|
|
14885
|
+
}
|
|
14886
|
+
};
|
|
14887
|
+
}
|
|
14888
|
+
function suggestGivenNames(suggest, lookup) {
|
|
14889
|
+
const names = suggest.query !== undefined ? lookup.forQuery(suggest.query) : suggest.source !== undefined ? lookup.forSource(suggest.source) : undefined;
|
|
14890
|
+
return names && names.length > 0 ? names : undefined;
|
|
14891
|
+
}
|
|
14892
|
+
function attachSuggestGivenNames(givens, lookup) {
|
|
14893
|
+
for (const given of givens ?? []) {
|
|
14894
|
+
if (!given.suggest)
|
|
14895
|
+
continue;
|
|
14896
|
+
const names = suggestGivenNames(given.suggest, lookup);
|
|
14897
|
+
if (names)
|
|
14898
|
+
given.suggest.givenNames = names;
|
|
14899
|
+
else
|
|
14900
|
+
delete given.suggest.givenNames;
|
|
14901
|
+
}
|
|
14902
|
+
}
|
|
14903
|
+
var init_given = __esm(() => {
|
|
14904
|
+
init_annotations();
|
|
14905
|
+
init_authorize();
|
|
14906
|
+
init_motly();
|
|
14907
|
+
});
|
|
14908
|
+
|
|
14909
|
+
// src/service/dashboard.ts
|
|
14910
|
+
import { isSourceDef as isSourceDef5 } from "@malloydata/malloy";
|
|
14911
|
+
function filterPublisherOwnedRenderLogs(logs, modelPath) {
|
|
14912
|
+
const couldBeATile = isDashboardModelPath(modelPath);
|
|
14913
|
+
return logs.filter((log) => {
|
|
14914
|
+
const match = UNKNOWN_RENDER_TAG.exec(log.message ?? "");
|
|
14915
|
+
if (!match)
|
|
14916
|
+
return true;
|
|
14917
|
+
const [, tag, field] = match;
|
|
14918
|
+
const root = tag.split(".")[0];
|
|
14919
|
+
if (PUBLISHER_OWNED_TAGS.includes(root))
|
|
14920
|
+
return false;
|
|
14921
|
+
return !(couldBeATile && RENDERER_CHILD_TAGS.includes(root) && field === "root");
|
|
14922
|
+
});
|
|
14923
|
+
}
|
|
14924
|
+
function isDashboardModelPath(modelPath) {
|
|
14925
|
+
if (!modelPath.endsWith(MODEL_FILE_SUFFIX2))
|
|
14669
14926
|
return false;
|
|
14670
|
-
|
|
14671
|
-
|
|
14672
|
-
|
|
14927
|
+
const segments = modelPath.split("/");
|
|
14928
|
+
return segments.length === 2 && segments[0] === DASHBOARDS_DIR;
|
|
14929
|
+
}
|
|
14930
|
+
function dashboardSlug(modelPath) {
|
|
14931
|
+
const basename = modelPath.slice(modelPath.lastIndexOf("/") + 1);
|
|
14932
|
+
return basename.slice(0, -MODEL_FILE_SUFFIX2.length);
|
|
14933
|
+
}
|
|
14934
|
+
function matchesDocumentedDashboardName(slug) {
|
|
14935
|
+
return /^[a-zA-Z0-9_-]+$/.test(slug);
|
|
14936
|
+
}
|
|
14937
|
+
function readArtifactTag(tag, declaredType) {
|
|
14938
|
+
const artifact = tag.tag("artifact");
|
|
14939
|
+
if (!artifact)
|
|
14940
|
+
return;
|
|
14941
|
+
const givens = readStartingGivens(artifact, declaredType);
|
|
14942
|
+
const autorun = readAutorun(artifact);
|
|
14943
|
+
return {
|
|
14944
|
+
title: tagText(artifact, "title"),
|
|
14945
|
+
tiles: artifact.array("tiles")?.map((tile) => tagText(tile)).filter((tile) => tile !== undefined),
|
|
14946
|
+
dashboardColumns: positiveInteger(tagNumeric(tag.tag("dashboard"), "columns")),
|
|
14947
|
+
givens,
|
|
14948
|
+
autorun
|
|
14949
|
+
};
|
|
14950
|
+
}
|
|
14951
|
+
function positiveInteger(value) {
|
|
14952
|
+
return value !== undefined && Number.isInteger(value) && value >= 1 ? value : undefined;
|
|
14953
|
+
}
|
|
14954
|
+
function normalizeTileExpression(tile) {
|
|
14955
|
+
return tile.split("->").map((part) => part.trim().replace(/\s+/g, " ")).join(" -> ").replace(/\s+/g, " ");
|
|
14956
|
+
}
|
|
14957
|
+
function readDashboardModelFacts(modelPath, modelDef, surfacedGivenNames, gatesBySource) {
|
|
14958
|
+
const registry = modelDef.givens ?? {};
|
|
14959
|
+
const surfaced = new Set(surfacedGivenNames);
|
|
14960
|
+
const givens = new Map;
|
|
14961
|
+
for (const given of Object.values(registry)) {
|
|
14962
|
+
if (!surfaced.has(given.name))
|
|
14963
|
+
continue;
|
|
14964
|
+
const type = given.type;
|
|
14965
|
+
const isFilter = type.type === "filter expression";
|
|
14966
|
+
givens.set(given.name, {
|
|
14967
|
+
name: given.name,
|
|
14968
|
+
type: isFilter ? `filter<${type.filterType}>` : type.type,
|
|
14969
|
+
default: given.defaultText,
|
|
14970
|
+
annotations: (given.annotations?.blockNotes ?? []).concat(given.annotations?.notes ?? []).map((note) => note.text)
|
|
14971
|
+
});
|
|
14972
|
+
}
|
|
14973
|
+
const isNamedQuery = (obj) => obj.type === "query";
|
|
14974
|
+
const queries = Object.values(modelDef.contents).filter(isNamedQuery).map((query) => ({
|
|
14975
|
+
name: query.as || query.name,
|
|
14976
|
+
annotations: [
|
|
14977
|
+
...query.annotations?.blockNotes ?? [],
|
|
14978
|
+
...query.annotations?.notes ?? []
|
|
14979
|
+
].map((note) => note.text),
|
|
14980
|
+
givens: (query.givenUsage ?? []).map((usage) => registry[usage.id]?.name).filter((name) => name !== undefined),
|
|
14981
|
+
sourceName: typeof query.structRef === "string" ? query.structRef : undefined
|
|
14982
|
+
}));
|
|
14983
|
+
const viewGivens = new Map;
|
|
14984
|
+
const viewAnnotations = new Map;
|
|
14985
|
+
const sourceFields = new Map;
|
|
14986
|
+
const drills = [];
|
|
14987
|
+
for (const obj of Object.values(modelDef.contents)) {
|
|
14988
|
+
if (!isSourceDef5(obj))
|
|
14989
|
+
continue;
|
|
14990
|
+
const sourceName = obj.as || obj.name;
|
|
14991
|
+
const fieldNames = new Set;
|
|
14992
|
+
sourceFields.set(sourceName, fieldNames);
|
|
14993
|
+
const sourceGivens = new Set;
|
|
14994
|
+
collectGivenRefs(obj.filterList, sourceGivens);
|
|
14995
|
+
for (const field of obj.fields) {
|
|
14996
|
+
const fieldName = field.as || field.name;
|
|
14997
|
+
fieldNames.add(fieldName);
|
|
14998
|
+
if (field.type === "turtle") {
|
|
14999
|
+
const refs = new Set(sourceGivens);
|
|
15000
|
+
collectGivenRefs(field.pipeline, refs);
|
|
15001
|
+
const key = normalizeTileExpression(`${sourceName} -> ${fieldName}`);
|
|
15002
|
+
viewGivens.set(key, Array.from(refs));
|
|
15003
|
+
viewAnnotations.set(key, [
|
|
15004
|
+
...field.annotations?.blockNotes ?? [],
|
|
15005
|
+
...field.annotations?.notes ?? []
|
|
15006
|
+
].map((note) => note.text));
|
|
15007
|
+
continue;
|
|
15008
|
+
}
|
|
15009
|
+
const drill = readDrillTag([
|
|
15010
|
+
...field.annotations?.blockNotes ?? [],
|
|
15011
|
+
...field.annotations?.notes ?? []
|
|
15012
|
+
].map((note) => note.text));
|
|
15013
|
+
if (drill) {
|
|
15014
|
+
drills.push({ ...drill, source: sourceName, dimension: fieldName });
|
|
15015
|
+
}
|
|
15016
|
+
}
|
|
15017
|
+
}
|
|
15018
|
+
return {
|
|
15019
|
+
modelPath,
|
|
15020
|
+
modelAnnotations: ownModelNotes(modelDef),
|
|
15021
|
+
queries,
|
|
15022
|
+
givens,
|
|
15023
|
+
viewGivens,
|
|
15024
|
+
viewAnnotations,
|
|
15025
|
+
sourceFields,
|
|
15026
|
+
drills,
|
|
15027
|
+
suggestGivens: suggestGivenLookup(modelDef, (source) => gatesBySource?.get(source), surfaced)
|
|
15028
|
+
};
|
|
15029
|
+
}
|
|
15030
|
+
function readTileLayout(annotations) {
|
|
15031
|
+
const tag = motlyTag(annotations);
|
|
15032
|
+
if (!tag)
|
|
15033
|
+
return;
|
|
15034
|
+
const layout = {};
|
|
15035
|
+
const label = tagText(tag, "label");
|
|
15036
|
+
if (label !== undefined)
|
|
15037
|
+
layout.label = label;
|
|
15038
|
+
const subtitle = tagText(tag, "subtitle");
|
|
15039
|
+
if (subtitle !== undefined)
|
|
15040
|
+
layout.subtitle = subtitle;
|
|
15041
|
+
const colspan = positiveInteger(tagNumeric(tag, "colspan"));
|
|
15042
|
+
if (colspan !== undefined)
|
|
15043
|
+
layout.colspan = colspan;
|
|
15044
|
+
if (tag.has("break"))
|
|
15045
|
+
layout.break = true;
|
|
15046
|
+
if (tag.has("borderless"))
|
|
15047
|
+
layout.borderless = true;
|
|
15048
|
+
return Object.keys(layout).length > 0 ? layout : undefined;
|
|
15049
|
+
}
|
|
15050
|
+
function readDrillTag(annotations) {
|
|
15051
|
+
const drill = motlyTag(annotations)?.tag("drill");
|
|
15052
|
+
if (!drill)
|
|
15053
|
+
return;
|
|
15054
|
+
const to = tagTextArray(drill, "to") ?? [tagText(drill, "to")].filter(isString);
|
|
15055
|
+
const given = tagText(drill, "given");
|
|
15056
|
+
return given === undefined ? { to } : { to, given };
|
|
15057
|
+
}
|
|
15058
|
+
function isString(value) {
|
|
15059
|
+
return value !== undefined;
|
|
15060
|
+
}
|
|
15061
|
+
function tagTextArray(tag, ...path) {
|
|
15062
|
+
const items = tag?.array(...path);
|
|
15063
|
+
return items?.map((item) => tagText(item)).filter(isString);
|
|
15064
|
+
}
|
|
15065
|
+
function resolveTileGivens(tile, facts) {
|
|
15066
|
+
return referencedTileGivens(tile, facts)?.filter((name) => facts.givens.has(name));
|
|
15067
|
+
}
|
|
15068
|
+
function resolveTileLayout(tile, facts) {
|
|
15069
|
+
const annotations = tileAnnotations(tile, facts);
|
|
15070
|
+
return annotations && readTileLayout(annotations);
|
|
15071
|
+
}
|
|
15072
|
+
function tileAnnotations(tile, facts) {
|
|
15073
|
+
const normalized = normalizeTileExpression(tile);
|
|
15074
|
+
return facts.viewAnnotations.get(normalized) ?? facts.queries.find((query) => query.name === normalized)?.annotations;
|
|
15075
|
+
}
|
|
15076
|
+
function referencedTileGivens(tile, facts) {
|
|
15077
|
+
const normalized = normalizeTileExpression(tile);
|
|
15078
|
+
return facts.viewGivens.get(normalized) ?? facts.queries.find((query) => query.name === normalized)?.givens;
|
|
15079
|
+
}
|
|
15080
|
+
function buildGivenSpecs(names, facts) {
|
|
15081
|
+
const declarations = facts.givens;
|
|
15082
|
+
const specs = [];
|
|
15083
|
+
for (const name of new Set(names)) {
|
|
15084
|
+
const declaration = declarations.get(name);
|
|
15085
|
+
if (!declaration)
|
|
14673
15086
|
continue;
|
|
14674
|
-
|
|
14675
|
-
if (declared && isSourceDef4(declared))
|
|
14676
|
-
return false;
|
|
15087
|
+
specs.push(givenSpec(declaration, facts));
|
|
14677
15088
|
}
|
|
14678
|
-
return
|
|
15089
|
+
return specs;
|
|
14679
15090
|
}
|
|
14680
|
-
function
|
|
14681
|
-
|
|
15091
|
+
function givenSpec(declaration, facts) {
|
|
15092
|
+
const control = readGivenControlSpec(declaration.annotations);
|
|
15093
|
+
if (control.suggest) {
|
|
15094
|
+
const names = suggestGivenNames(control.suggest, facts.suggestGivens);
|
|
15095
|
+
if (names)
|
|
15096
|
+
control.suggest.givenNames = names;
|
|
15097
|
+
}
|
|
15098
|
+
return {
|
|
15099
|
+
name: declaration.name,
|
|
15100
|
+
type: declaration.type,
|
|
15101
|
+
default: declaration.type.startsWith("filter<") && declaration.default !== undefined ? unwrapFilterLiteral(declaration.default) : declaration.default,
|
|
15102
|
+
annotations: declaration.annotations.filter((text) => /^##?\(/.test(text)),
|
|
15103
|
+
...control
|
|
15104
|
+
};
|
|
14682
15105
|
}
|
|
14683
|
-
function
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
15106
|
+
function factsCarryArtifactTag(facts) {
|
|
15107
|
+
if (motlyTag(facts.modelAnnotations)?.tag("artifact"))
|
|
15108
|
+
return true;
|
|
15109
|
+
return facts.queries.some((query) => Boolean(motlyTag(query.annotations)?.tag("artifact")));
|
|
15110
|
+
}
|
|
15111
|
+
function buildDashboardManifest(facts) {
|
|
15112
|
+
const name = dashboardSlug(facts.modelPath);
|
|
15113
|
+
const base = {
|
|
15114
|
+
name,
|
|
15115
|
+
entryFile: facts.modelPath
|
|
15116
|
+
};
|
|
15117
|
+
const modelTag = motlyTag(facts.modelAnnotations);
|
|
15118
|
+
const declaredType = (name2) => facts.givens.get(name2)?.type;
|
|
15119
|
+
const composite = modelTag ? readArtifactTag(modelTag, declaredType) : undefined;
|
|
15120
|
+
if (composite?.tiles?.length) {
|
|
15121
|
+
const tiles = composite.tiles.map((query) => {
|
|
15122
|
+
const givenNames = resolveTileGivens(query, facts);
|
|
15123
|
+
const layout = resolveTileLayout(query, facts);
|
|
15124
|
+
return {
|
|
15125
|
+
query,
|
|
15126
|
+
...givenNames ? { givenNames } : {},
|
|
15127
|
+
...layout
|
|
15128
|
+
};
|
|
15129
|
+
});
|
|
15130
|
+
const doc = docCommentTitleAndDescription(facts.modelAnnotations, composite.title);
|
|
15131
|
+
return {
|
|
15132
|
+
...base,
|
|
15133
|
+
title: doc.title ?? name,
|
|
15134
|
+
description: doc.description,
|
|
15135
|
+
tiles,
|
|
15136
|
+
dashboardColumns: composite.dashboardColumns,
|
|
15137
|
+
startingGivens: composite.givens,
|
|
15138
|
+
autorun: composite.autorun,
|
|
15139
|
+
givens: buildGivenSpecs(tiles.some((tile) => tile.givenNames === undefined) ? Array.from(facts.givens.keys()) : tiles.flatMap((tile) => tile.givenNames ?? []), facts)
|
|
15140
|
+
};
|
|
15141
|
+
}
|
|
15142
|
+
for (const query of facts.queries) {
|
|
15143
|
+
const tag = motlyTag(query.annotations);
|
|
15144
|
+
const artifact = tag ? readArtifactTag(tag, declaredType) : undefined;
|
|
15145
|
+
if (!artifact)
|
|
15146
|
+
continue;
|
|
15147
|
+
const doc = docCommentTitleAndDescription(query.annotations, artifact.title);
|
|
15148
|
+
return {
|
|
15149
|
+
...base,
|
|
15150
|
+
title: doc.title ?? name,
|
|
15151
|
+
description: doc.description,
|
|
15152
|
+
query: query.name,
|
|
15153
|
+
dashboardColumns: artifact.dashboardColumns,
|
|
15154
|
+
startingGivens: artifact.givens,
|
|
15155
|
+
autorun: artifact.autorun,
|
|
15156
|
+
givens: buildGivenSpecs(query.givens, facts)
|
|
15157
|
+
};
|
|
14690
15158
|
}
|
|
15159
|
+
return;
|
|
14691
15160
|
}
|
|
14692
|
-
function
|
|
14693
|
-
const
|
|
14694
|
-
|
|
14695
|
-
|
|
15161
|
+
function plainTileParts(tile) {
|
|
15162
|
+
const piped = /^(\w+)\s*->\s*(\w+)$/.exec(tile.trim());
|
|
15163
|
+
if (piped)
|
|
15164
|
+
return { source: piped[1], view: piped[2] };
|
|
15165
|
+
const bare = /^\w+$/.exec(tile.trim());
|
|
15166
|
+
return bare ? { name: tile.trim() } : undefined;
|
|
14696
15167
|
}
|
|
14697
|
-
function
|
|
14698
|
-
const
|
|
14699
|
-
const
|
|
14700
|
-
const
|
|
14701
|
-
const
|
|
14702
|
-
const
|
|
14703
|
-
const
|
|
14704
|
-
const
|
|
14705
|
-
for (const
|
|
14706
|
-
|
|
15168
|
+
function lintDashboard(facts, manifest) {
|
|
15169
|
+
const findings = [];
|
|
15170
|
+
const slug = manifest.name;
|
|
15171
|
+
const add = (message, severity = "warn") => findings.push({ subject: slug, message, severity });
|
|
15172
|
+
const ownAnnotations = manifest.tiles ? facts.modelAnnotations : facts.queries.find((query) => query.name === manifest.query)?.annotations ?? [];
|
|
15173
|
+
const ownTag = motlyTag(ownAnnotations);
|
|
15174
|
+
const artifactTag = ownTag?.tag("artifact");
|
|
15175
|
+
const lintTargets = new Set([...facts.modelAnnotations, ...ownAnnotations]);
|
|
15176
|
+
for (const message of new Set(motlyParseErrors([...lintTargets]))) {
|
|
15177
|
+
add(`Annotation ${describeParseFailure(message)}, so that line had no ` + `effect on this dashboard.`);
|
|
15178
|
+
}
|
|
15179
|
+
if (!manifest.tiles) {
|
|
15180
|
+
const modelArtifact = motlyTag(facts.modelAnnotations)?.tag("artifact");
|
|
15181
|
+
if (modelArtifact) {
|
|
15182
|
+
add(`${describeTilelessModelArtifact(modelArtifact)}, so it builds ` + `nothing and this dashboard is the query's own '# artifact' ` + `instead. ${NAME_THE_TILES}`);
|
|
15183
|
+
}
|
|
15184
|
+
}
|
|
15185
|
+
const knownProperties = manifest.tiles ? COMPOSITE_ARTIFACT_PROPERTIES : QUERY_ARTIFACT_PROPERTIES;
|
|
15186
|
+
for (const property of Object.keys(artifactTag?.dict ?? {})) {
|
|
15187
|
+
if (knownProperties.includes(property))
|
|
14707
15188
|
continue;
|
|
14708
|
-
|
|
14709
|
-
sweptStructs.push(obj);
|
|
14710
|
-
for (const note of ownLevelNotes(struct.annotations)) {
|
|
14711
|
-
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
14712
|
-
gatedSourceOwnAuthorizeNotes.add(note);
|
|
14713
|
-
considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
|
|
14714
|
-
}
|
|
14715
|
-
}
|
|
14716
|
-
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
|
|
14717
|
-
...ownLevelNotes(struct.annotations),
|
|
14718
|
-
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
14719
|
-
].map((note) => note.text)));
|
|
15189
|
+
add(`\`${property}\` in the artifact tag does nothing in Publisher` + (ARTIFACT_PROPERTY_REPLACEMENTS[property] ? `. ${ARTIFACT_PROPERTY_REPLACEMENTS[property]}` : property === "tiles" ? `: a dashboard whose tag sits on a query is that one query's ` + `result. Move the tag to model level (\`## artifact\`) to ` + `combine separate queries into a grid.` : `.`));
|
|
14720
15190
|
}
|
|
14721
|
-
for (const
|
|
14722
|
-
const
|
|
14723
|
-
if (
|
|
15191
|
+
for (const entry of artifactTag?.array("tiles") ?? []) {
|
|
15192
|
+
const carried = Object.keys(entry.dict ?? {});
|
|
15193
|
+
if (carried.length === 0)
|
|
14724
15194
|
continue;
|
|
14725
|
-
|
|
15195
|
+
const named = carried.map((property) => `\`${property}\``).join(", ");
|
|
15196
|
+
add(`\`${tagText(entry) ?? "a tile"}\` in \`tiles=[…]\` carries ${named}, ` + `which Publisher does not read: a tile entry is the run expression ` + `alone. Per-tile presentation goes on the view the tile names ` + `(\`# colspan\`, \`# break\`, \`# label\`, \`# subtitle\`, ` + `\`# borderless\`).`);
|
|
15197
|
+
}
|
|
15198
|
+
for (const { query: tile } of manifest.tiles ?? []) {
|
|
15199
|
+
const tag = motlyTag(tileAnnotations(tile, facts) ?? []);
|
|
15200
|
+
if (!tag?.has("colspan"))
|
|
15201
|
+
continue;
|
|
15202
|
+
const colspan = positiveInteger(tagNumeric(tag, "colspan"));
|
|
15203
|
+
if (colspan === undefined) {
|
|
15204
|
+
const raw = tagText(tag, "colspan");
|
|
15205
|
+
add(`# colspan on "${tile}" must be a positive integer, got ` + `${raw === undefined ? "a value that could not be read" : JSON.stringify(raw)}. ` + `The tile spans one column.`);
|
|
14726
15206
|
continue;
|
|
14727
|
-
sweptStructs.push(entry);
|
|
14728
|
-
for (const note of ownLevelNotes(entry.annotations)) {
|
|
14729
|
-
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
14730
|
-
gatedSourceOwnAuthorizeNotes.add(note);
|
|
14731
|
-
considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
|
|
14732
|
-
}
|
|
14733
15207
|
}
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
14740
|
-
].map((note) => note.text)));
|
|
14741
|
-
}
|
|
14742
|
-
{
|
|
14743
|
-
const folded = modelAnnotations(modelDef);
|
|
14744
|
-
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
|
|
14745
|
-
}
|
|
14746
|
-
assertNoAuthorizeNearMisses(nearMissAuthorize);
|
|
14747
|
-
const authorizeOwnNotes = new Map;
|
|
14748
|
-
const attributedAuthorizeOwnNotes = new Map;
|
|
14749
|
-
{
|
|
14750
|
-
const fileNoteTexts = ownLevelNoteTexts(modelAnnotations(modelDef));
|
|
14751
|
-
if (containsAuthorizeAnnotationTag(fileNoteTexts)) {
|
|
14752
|
-
misplacedAuthorize.push({
|
|
14753
|
-
kind: "file",
|
|
14754
|
-
route: fileNoteTexts.map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14755
|
-
});
|
|
15208
|
+
const columns = manifest.dashboardColumns;
|
|
15209
|
+
if (columns === undefined) {
|
|
15210
|
+
add(`# colspan=${colspan} on "${tile}" needs a grid to span. This ` + `dashboard declares no # dashboard { columns=N }, so it lays out ` + `at the viewer's default width and every colspan is clamped to it.`);
|
|
15211
|
+
} else if (colspan > columns) {
|
|
15212
|
+
add(`# colspan=${colspan} on "${tile}" exceeds the ${columns} columns ` + `of this dashboard and is clamped to ${columns}. Widen ` + `# dashboard { columns=… } or lower the colspan.`);
|
|
14756
15213
|
}
|
|
14757
15214
|
}
|
|
14758
|
-
const
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
let cur = struct.annotations;
|
|
14764
|
-
while (cur) {
|
|
14765
|
-
if (cur.blockNotes) {
|
|
14766
|
-
collected.push(cur.blockNotes.map((note) => note.text));
|
|
14767
|
-
}
|
|
14768
|
-
cur = cur.inherits;
|
|
14769
|
-
}
|
|
14770
|
-
const allAnnotations = collected.reverse().flat();
|
|
14771
|
-
let filters;
|
|
14772
|
-
if (allAnnotations.length > 0) {
|
|
14773
|
-
try {
|
|
14774
|
-
const parsed = parseFilters(allAnnotations);
|
|
14775
|
-
if (parsed.length > 0) {
|
|
14776
|
-
filterMap.set(sourceName, parsed);
|
|
14777
|
-
const fields = struct.fields;
|
|
14778
|
-
filters = parsed.map((f) => {
|
|
14779
|
-
const field = fields.find((fd) => (fd.as || fd.name) === f.dimension);
|
|
14780
|
-
return {
|
|
14781
|
-
name: f.name,
|
|
14782
|
-
dimension: f.dimension,
|
|
14783
|
-
type: f.type,
|
|
14784
|
-
implicit: f.implicit,
|
|
14785
|
-
required: f.required,
|
|
14786
|
-
dimensionType: field?.type
|
|
14787
|
-
};
|
|
14788
|
-
});
|
|
14789
|
-
}
|
|
14790
|
-
} catch (err) {
|
|
14791
|
-
onParseError?.(sourceName, err);
|
|
14792
|
-
}
|
|
14793
|
-
}
|
|
14794
|
-
const ownNotes = ownLevelNoteTexts(struct.annotations);
|
|
14795
|
-
const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
|
|
14796
|
-
const ownNotesByRoute = new Map;
|
|
14797
|
-
const attributedOwnNotesByRoute = new Map;
|
|
14798
|
-
for (const route of CANONICAL_AUTHORIZE_ROUTES) {
|
|
14799
|
-
const notesForRoute = ownAuthorizeNotes.filter((note) => authorizeAnnotationRoute(note.text) === route);
|
|
14800
|
-
ownNotesByRoute.set(route, notesForRoute);
|
|
14801
|
-
attributedOwnNotesByRoute.set(route, notesForRoute.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
|
|
15215
|
+
const columnsTag = ownTag?.tag("dashboard");
|
|
15216
|
+
if (columnsTag?.has("columns")) {
|
|
15217
|
+
if (positiveInteger(tagNumeric(columnsTag, "columns")) === undefined) {
|
|
15218
|
+
const raw = tagText(columnsTag, "columns");
|
|
15219
|
+
add(`# dashboard { columns=… } must be a positive integer, got ` + `${raw === undefined ? "a value that could not be read" : JSON.stringify(raw)}. ` + `The grid falls back to the default.`);
|
|
14802
15220
|
}
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
const
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
}
|
|
14813
|
-
routeGroupsByRoute.set(route, groupsForRoute);
|
|
15221
|
+
}
|
|
15222
|
+
for (const { query: tile } of manifest.tiles ?? []) {
|
|
15223
|
+
const parts = plainTileParts(tile);
|
|
15224
|
+
if (!parts)
|
|
15225
|
+
continue;
|
|
15226
|
+
const resolved = referencedTileGivens(tile, facts) !== undefined || "name" in parts && facts.queries.some((query) => query.name === parts.name);
|
|
15227
|
+
if (!resolved) {
|
|
15228
|
+
const detail = "source" in parts ? facts.sourceFields.has(parts.source) ? `source "${parts.source}" has no view "${parts.view}"` : `no source "${parts.source}" in this file` : `no query named "${parts.name}" in this file`;
|
|
15229
|
+
add(`tile "${tile}" does not resolve: ${detail}.`, "error");
|
|
14814
15230
|
}
|
|
14815
|
-
|
|
14816
|
-
|
|
15231
|
+
const unbindable = (referencedTileGivens(tile, facts) ?? []).filter((name) => !facts.givens.has(name));
|
|
15232
|
+
for (const name of unbindable) {
|
|
15233
|
+
add(`tile "${tile}" filters by given "${name}", which this file does ` + `not import, so no control is shown for it and it stays at its ` + `default. Add it to an import in ${facts.modelPath}.`);
|
|
14817
15234
|
}
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
const
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
annotations: annotationTexts(turtle.annotations)
|
|
14824
|
-
}));
|
|
14825
|
-
for (const field of struct.fields) {
|
|
14826
|
-
const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
|
|
14827
|
-
if (fieldAuthorizeNotes.length === 0)
|
|
14828
|
-
continue;
|
|
14829
|
-
if (fieldAuthorizeNotes.every((note) => gatedSourceOwnAuthorizeNotes.has(note))) {
|
|
14830
|
-
continue;
|
|
14831
|
-
}
|
|
14832
|
-
const fieldName = field.as || field.name;
|
|
14833
|
-
if (isJoined3(field) && isSourceDef4(field)) {
|
|
14834
|
-
const joinedStruct = field;
|
|
14835
|
-
if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
|
|
14836
|
-
continue;
|
|
14837
|
-
}
|
|
14838
|
-
misplacedAuthorize.push({
|
|
14839
|
-
kind: "field",
|
|
14840
|
-
name: sourceName,
|
|
14841
|
-
fieldName,
|
|
14842
|
-
route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14843
|
-
});
|
|
15235
|
+
}
|
|
15236
|
+
if (manifest.query !== undefined) {
|
|
15237
|
+
const query = facts.queries.find((q) => q.name === manifest.query);
|
|
15238
|
+
for (const name of query?.givens ?? []) {
|
|
15239
|
+
if (facts.givens.has(name))
|
|
14844
15240
|
continue;
|
|
14845
|
-
}
|
|
14846
|
-
misplacedAuthorize.push({
|
|
14847
|
-
kind: "field",
|
|
14848
|
-
name: sourceName,
|
|
14849
|
-
fieldName,
|
|
14850
|
-
route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14851
|
-
});
|
|
15241
|
+
add(`query "${manifest.query}" filters by given "${name}", which this ` + `file does not import, so no control is shown for it and it ` + `stays at its default. Add it to an import in ` + `${facts.modelPath}.`);
|
|
14852
15242
|
}
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
misplacedAuthorize,
|
|
14868
|
-
authorizeOwnNotes,
|
|
14869
|
-
attributedAuthorizeOwnNotes
|
|
14870
|
-
};
|
|
14871
|
-
}
|
|
14872
|
-
function extractQueriesFromModelDef(modelDef) {
|
|
14873
|
-
const isNamedQuery = (obj) => obj.type === "query";
|
|
14874
|
-
const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
|
|
14875
|
-
const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
|
|
14876
|
-
kind: "query",
|
|
14877
|
-
name: queryObj.as || queryObj.name,
|
|
14878
|
-
route: ownLevelNoteTexts(queryObj.annotations).map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14879
|
-
}));
|
|
14880
|
-
const queries = namedQueries.map((queryObj) => ({
|
|
14881
|
-
name: queryObj.as || queryObj.name,
|
|
14882
|
-
sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
|
|
14883
|
-
annotations: annotationTexts(queryObj.annotations)
|
|
14884
|
-
}));
|
|
14885
|
-
return { queries, misplacedAuthorize };
|
|
14886
|
-
}
|
|
14887
|
-
var init_source_extraction = __esm(() => {
|
|
14888
|
-
init_annotations();
|
|
14889
|
-
init_authorize();
|
|
14890
|
-
init_authorize_routes();
|
|
14891
|
-
init_filter();
|
|
14892
|
-
init_gate_registry_walk();
|
|
14893
|
-
});
|
|
14894
|
-
|
|
14895
|
-
// src/service/motly.ts
|
|
14896
|
-
import { parseAnnotation } from "@malloydata/malloy-tag";
|
|
14897
|
-
function onMotlyRoute(text) {
|
|
14898
|
-
const afterSigil = text.replace(/^##?\|?/, "");
|
|
14899
|
-
return afterSigil === "" || /^[ \t\r\n]/.test(afterSigil);
|
|
14900
|
-
}
|
|
14901
|
-
function hasEnvReference(annotation) {
|
|
14902
|
-
return annotation.includes("@env.");
|
|
14903
|
-
}
|
|
14904
|
-
function pollutionTargets() {
|
|
14905
|
-
const targets = [Object.prototype];
|
|
14906
|
-
for (const key of Object.getOwnPropertyNames(Object.prototype)) {
|
|
14907
|
-
const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, key);
|
|
14908
|
-
if (!descriptor)
|
|
14909
|
-
continue;
|
|
14910
|
-
let value;
|
|
14911
|
-
if ("value" in descriptor) {
|
|
14912
|
-
value = descriptor.value;
|
|
14913
|
-
} else {
|
|
14914
|
-
if (BUILT_IN_PROTO_GETTER === undefined || descriptor.get !== BUILT_IN_PROTO_GETTER) {
|
|
14915
|
-
return;
|
|
15243
|
+
}
|
|
15244
|
+
for (const name of Object.keys(manifest.startingGivens ?? {})) {
|
|
15245
|
+
if (facts.givens.has(name))
|
|
15246
|
+
continue;
|
|
15247
|
+
add(`'artifact { givens { ${name}=… } }' sets a starting value for given ` + `"${name}", which this file does not import, so the value is ` + `dropped at query time and the dashboard opens at the ` + `declaration's default. Add it to an import in ${facts.modelPath}.`);
|
|
15248
|
+
}
|
|
15249
|
+
for (const spec of manifest.givens) {
|
|
15250
|
+
const suggest = spec.suggest;
|
|
15251
|
+
if (!suggest) {
|
|
15252
|
+
const declared = motlyTag(facts.givens.get(spec.name)?.annotations ?? [])?.tag("suggest");
|
|
15253
|
+
if (declared) {
|
|
15254
|
+
const has = (key) => tagText(declared, key) !== undefined;
|
|
15255
|
+
const missing = has("dimension") ? `it names a dimension but no query= or source= to read it from` : has("source") ? `source= alone names no column to read options from` : `it names neither a query= nor a source=`;
|
|
15256
|
+
add(`given "${spec.name}" declares a suggest that cannot fetch ` + `options: ${missing}. Use query=, source= with dimension=, ` + `or query= with dimension=. No control options are offered ` + `for it.`, "error");
|
|
14916
15257
|
}
|
|
14917
15258
|
continue;
|
|
14918
15259
|
}
|
|
14919
|
-
if (
|
|
14920
|
-
|
|
15260
|
+
if (suggest.query !== undefined) {
|
|
15261
|
+
const named = facts.queries.find((query) => query.name === suggest.query);
|
|
15262
|
+
if (!named) {
|
|
15263
|
+
add(`given "${spec.name}" suggests options from query ` + `"${suggest.query}", which this file does not define.`, "error");
|
|
15264
|
+
} else if (named.sourceName !== undefined && !facts.sourceFields.has(named.sourceName)) {
|
|
15265
|
+
add(`given "${spec.name}" suggests options from query ` + `"${suggest.query}", which reads source ` + `"${named.sourceName}" that this file does not import. The ` + `query resolves by name, so the package loads, but the ` + `control answers "Undefined source '${named.sourceName}'" ` + `and offers no options. Import that source here too.`, "error");
|
|
15266
|
+
}
|
|
15267
|
+
}
|
|
15268
|
+
if (suggest.source !== undefined) {
|
|
15269
|
+
const fields = facts.sourceFields.get(suggest.source);
|
|
15270
|
+
if (!fields) {
|
|
15271
|
+
add(`given "${spec.name}" suggests options from source ` + `"${suggest.source}", which this file does not define.`, "error");
|
|
15272
|
+
} else if (suggest.dimension !== undefined && !fields.has(suggest.dimension)) {
|
|
15273
|
+
add(`given "${spec.name}" suggests options from ` + `"${suggest.source} -> ${suggest.dimension}", but that ` + `source has no field "${suggest.dimension}".`, "error");
|
|
15274
|
+
}
|
|
14921
15275
|
}
|
|
14922
15276
|
}
|
|
14923
|
-
return
|
|
15277
|
+
return findings;
|
|
14924
15278
|
}
|
|
14925
|
-
function
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
15279
|
+
function describeParseFailure(message) {
|
|
15280
|
+
if (message === UNSAFE_TO_PARSE) {
|
|
15281
|
+
return `was refused rather than parsed (${message}), which can be caused by something outside this file`;
|
|
14929
15282
|
}
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
try {
|
|
14933
|
-
return Object.getOwnPropertyNames(target);
|
|
14934
|
-
} catch {
|
|
14935
|
-
unreadable = true;
|
|
14936
|
-
return [];
|
|
14937
|
-
}
|
|
14938
|
-
};
|
|
14939
|
-
const before = targets.map(ownNames);
|
|
14940
|
-
if (unreadable)
|
|
14941
|
-
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
14942
|
-
const undoPollution = () => {
|
|
14943
|
-
let polluted = false;
|
|
14944
|
-
targets.forEach((target, index) => {
|
|
14945
|
-
for (const key of ownNames(target)) {
|
|
14946
|
-
if (before[index].includes(key))
|
|
14947
|
-
continue;
|
|
14948
|
-
polluted = true;
|
|
14949
|
-
try {
|
|
14950
|
-
delete target[key];
|
|
14951
|
-
} catch {}
|
|
14952
|
-
}
|
|
14953
|
-
});
|
|
14954
|
-
return polluted || unreadable;
|
|
14955
|
-
};
|
|
14956
|
-
if (texts.some((text) => text.length > MAX_ANNOTATION_CHARS)) {
|
|
14957
|
-
return { tag: undefined, messages: [ANNOTATION_TOO_LONG] };
|
|
15283
|
+
if (message === ENV_REFERENCE_DROPPED) {
|
|
15284
|
+
return `was dropped rather than parsed (${message}), so nothing on that line took effect`;
|
|
14958
15285
|
}
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
if (undoPollution())
|
|
14962
|
-
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
14963
|
-
return {
|
|
14964
|
-
tag: result.tag,
|
|
14965
|
-
messages: result.log.map((error) => error.message)
|
|
14966
|
-
};
|
|
14967
|
-
} catch {
|
|
14968
|
-
undoPollution();
|
|
14969
|
-
return { tag: undefined, messages: [UNSAFE_TO_PARSE] };
|
|
15286
|
+
if (message === ANNOTATION_TOO_LONG) {
|
|
15287
|
+
return `was refused rather than parsed (${message})`;
|
|
14970
15288
|
}
|
|
15289
|
+
return `does not parse (${message})`;
|
|
14971
15290
|
}
|
|
14972
|
-
function
|
|
14973
|
-
const
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
const
|
|
14979
|
-
if (
|
|
14980
|
-
return
|
|
14981
|
-
const
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
return {
|
|
14989
|
-
title: lines[titleLine].trim(),
|
|
14990
|
-
body: body.length > 0 ? body : undefined
|
|
14991
|
-
};
|
|
14992
|
-
}
|
|
14993
|
-
function docCommentTitleAndDescription(texts, explicitTitle) {
|
|
14994
|
-
if (explicitTitle !== undefined) {
|
|
14995
|
-
return { title: explicitTitle, description: docCommentText(texts) };
|
|
15291
|
+
function lintUndiscoveredDashboard(facts) {
|
|
15292
|
+
const subject = dashboardSlug(facts.modelPath);
|
|
15293
|
+
const messages = [
|
|
15294
|
+
...motlyParseErrors(facts.modelAnnotations),
|
|
15295
|
+
...facts.queries.flatMap((query) => motlyParseErrors(query.annotations))
|
|
15296
|
+
].map((message) => `Tag ${describeParseFailure(message)}, so the whole tag is discarded ` + `and this file is treated as a shared include rather than a dashboard.`);
|
|
15297
|
+
const findings = Array.from(new Set(messages), (message) => ({ subject, message, severity: "error" }));
|
|
15298
|
+
if (findings.length > 0)
|
|
15299
|
+
return findings;
|
|
15300
|
+
const modelArtifact = motlyTag(facts.modelAnnotations)?.tag("artifact");
|
|
15301
|
+
if (modelArtifact) {
|
|
15302
|
+
findings.push({
|
|
15303
|
+
subject,
|
|
15304
|
+
message: `${describeTilelessModelArtifact(modelArtifact)}, so this file ` + `produces no dashboard. ${NAME_THE_TILES}`,
|
|
15305
|
+
severity: "error"
|
|
15306
|
+
});
|
|
14996
15307
|
}
|
|
14997
|
-
|
|
14998
|
-
return
|
|
15308
|
+
findings.push(...lintArtifactOnView(facts));
|
|
15309
|
+
return findings;
|
|
14999
15310
|
}
|
|
15000
|
-
function
|
|
15001
|
-
const
|
|
15002
|
-
const
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
while (i < annotation.length) {
|
|
15006
|
-
const char = annotation[i];
|
|
15007
|
-
const delimited = endOfDelimited(annotation, i);
|
|
15008
|
-
if (delimited > i) {
|
|
15009
|
-
out += annotation.slice(i, delimited);
|
|
15010
|
-
i = delimited;
|
|
15311
|
+
function lintArtifactOnView(facts) {
|
|
15312
|
+
const subject = dashboardSlug(facts.modelPath);
|
|
15313
|
+
const findings = [];
|
|
15314
|
+
for (const [view, annotations] of facts.viewAnnotations) {
|
|
15315
|
+
if (!motlyTag(annotations)?.tag("artifact"))
|
|
15011
15316
|
continue;
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
15317
|
+
const viewName = view.split("->").at(-1)?.trim() ?? view;
|
|
15318
|
+
findings.push({
|
|
15319
|
+
subject,
|
|
15320
|
+
message: `'# artifact' on view '${view}' is not read: Publisher reads the ` + `tag on a 'query:' or at model level, so this file produces no ` + `dashboard. Either name the view as a tile, ` + `## artifact { tiles=["${view}"] }, or tag a query that runs it, ` + `query: ${viewName} is ${view}.`,
|
|
15321
|
+
severity: "error"
|
|
15322
|
+
});
|
|
15323
|
+
}
|
|
15324
|
+
return findings;
|
|
15325
|
+
}
|
|
15326
|
+
function describeTilelessModelArtifact(modelArtifact) {
|
|
15327
|
+
return modelArtifact.array("tiles") === undefined ? `Model-level '## artifact' declares no tiles=` : `Model-level '## artifact' declares an empty tiles=`;
|
|
15328
|
+
}
|
|
15329
|
+
function lintGivenTags(facts) {
|
|
15330
|
+
const findings = new Map;
|
|
15331
|
+
for (const file of facts) {
|
|
15332
|
+
for (const [name, declaration] of file.givens) {
|
|
15333
|
+
for (const message of motlyParseErrors(declaration.annotations)) {
|
|
15334
|
+
findings.set(`${name}|${message}`, {
|
|
15335
|
+
subject: name,
|
|
15336
|
+
message: `given "${name}" has an annotation that ` + `${describeParseFailure(message)}, so the whole line is ` + `discarded and the given ` + `loses any label, control, range or suggest it declared. ` + `It still accepts values; only its presentation is lost.`,
|
|
15337
|
+
severity: "error"
|
|
15338
|
+
});
|
|
15020
15339
|
}
|
|
15021
15340
|
}
|
|
15022
|
-
out += char;
|
|
15023
|
-
i += 1;
|
|
15024
15341
|
}
|
|
15025
|
-
return
|
|
15342
|
+
return Array.from(findings.values());
|
|
15026
15343
|
}
|
|
15027
|
-
function
|
|
15028
|
-
const
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
if (
|
|
15033
|
-
|
|
15344
|
+
function lintDrillTargets(facts, knownSlugs) {
|
|
15345
|
+
const findings = new Map;
|
|
15346
|
+
for (const file of facts) {
|
|
15347
|
+
for (const drill of file.drills) {
|
|
15348
|
+
const where = `${drill.source}.${drill.dimension}`;
|
|
15349
|
+
if (drill.to.length === 0) {
|
|
15350
|
+
findings.set(where, {
|
|
15351
|
+
subject: where,
|
|
15352
|
+
message: `# drill on ${where} names no destination (to=…).`,
|
|
15353
|
+
severity: "error"
|
|
15354
|
+
});
|
|
15034
15355
|
continue;
|
|
15035
15356
|
}
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15357
|
+
for (const destination of drill.to) {
|
|
15358
|
+
if (destination === "self" || knownSlugs.has(destination))
|
|
15359
|
+
continue;
|
|
15360
|
+
findings.set(`${where}|${destination}`, {
|
|
15361
|
+
subject: where,
|
|
15362
|
+
message: `# drill on ${where} targets "${destination}", which is not ` + `a dashboard in this package.`,
|
|
15363
|
+
severity: "error"
|
|
15364
|
+
});
|
|
15365
|
+
}
|
|
15042
15366
|
}
|
|
15043
|
-
return text.length;
|
|
15044
15367
|
}
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15368
|
+
return Array.from(findings.values());
|
|
15369
|
+
}
|
|
15370
|
+
function drillGivenName(drill) {
|
|
15371
|
+
return drill.given ?? drill.dimension;
|
|
15372
|
+
}
|
|
15373
|
+
function lintSelfDrills(facts) {
|
|
15374
|
+
const surfaced = new Set;
|
|
15375
|
+
for (const file of facts) {
|
|
15376
|
+
for (const name of file.givens.keys())
|
|
15377
|
+
surfaced.add(name.toLowerCase());
|
|
15378
|
+
}
|
|
15379
|
+
const findings = new Map;
|
|
15380
|
+
for (const file of facts) {
|
|
15381
|
+
for (const drill of file.drills) {
|
|
15382
|
+
if (!drill.to.includes("self"))
|
|
15383
|
+
continue;
|
|
15384
|
+
const given = drillGivenName(drill);
|
|
15385
|
+
if (surfaced.has(given.toLowerCase()))
|
|
15386
|
+
continue;
|
|
15387
|
+
const where = `${drill.source}.${drill.dimension}`;
|
|
15388
|
+
findings.set(where, {
|
|
15389
|
+
subject: where,
|
|
15390
|
+
message: `# drill on ${where} has to=self, but no model in this ` + `package declares a given "${given}" for the clicked value to ` + `filter on, so the drill cannot fire anywhere. Declare it, or ` + `point the tag at a given that exists with given=.`,
|
|
15391
|
+
severity: "error"
|
|
15392
|
+
});
|
|
15056
15393
|
}
|
|
15057
15394
|
}
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15395
|
+
return Array.from(findings.values());
|
|
15396
|
+
}
|
|
15397
|
+
var DASHBOARDS_DIR = "dashboards", COMPONENT_FILE_SUFFIXES, MODEL_FILE_SUFFIX2 = ".malloy", PUBLISHER_OWNED_TAGS, RENDERER_CHILD_TAGS, UNKNOWN_RENDER_TAG, COMPOSITE_ARTIFACT_PROPERTIES, QUERY_ARTIFACT_PROPERTIES, ARTIFACT_PROPERTY_REPLACEMENTS, NAME_THE_TILES = `Name the views to show: tiles=["source -> view", …].`;
|
|
15398
|
+
var init_dashboard = __esm(() => {
|
|
15399
|
+
init_annotations();
|
|
15400
|
+
init_given();
|
|
15401
|
+
init_motly();
|
|
15402
|
+
COMPONENT_FILE_SUFFIXES = [".jsx", ".tsx"];
|
|
15403
|
+
PUBLISHER_OWNED_TAGS = ["artifact", "drill"];
|
|
15404
|
+
RENDERER_CHILD_TAGS = ["colspan", "break", "subtitle", "borderless"];
|
|
15405
|
+
UNKNOWN_RENDER_TAG = /^Unknown render tag '([^']+)' on field '([^']*)'/;
|
|
15406
|
+
COMPOSITE_ARTIFACT_PROPERTIES = [
|
|
15407
|
+
"title",
|
|
15408
|
+
"tiles",
|
|
15409
|
+
"givens",
|
|
15410
|
+
"autorun"
|
|
15411
|
+
];
|
|
15412
|
+
QUERY_ARTIFACT_PROPERTIES = COMPOSITE_ARTIFACT_PROPERTIES.filter((property) => property !== "tiles");
|
|
15413
|
+
ARTIFACT_PROPERTY_REPLACEMENTS = {
|
|
15414
|
+
dashboard_columns: "Write the grid width as `# dashboard { columns=N }` beside the artifact tag.",
|
|
15415
|
+
dashboard: "The grid width is a sibling of the artifact tag, not a property of it: " + "close the artifact braces first, as `## artifact { … } dashboard { columns=N }`."
|
|
15416
|
+
};
|
|
15417
|
+
});
|
|
15418
|
+
|
|
15419
|
+
// src/service/filter.ts
|
|
15420
|
+
function parseFilterAnnotation(annotation) {
|
|
15421
|
+
const trimmed = annotation.trim();
|
|
15422
|
+
if (!trimmed.startsWith(ANNOTATION_PREFIX)) {
|
|
15423
|
+
return null;
|
|
15424
|
+
}
|
|
15425
|
+
const body = trimmed.slice(ANNOTATION_PREFIX.length).trim();
|
|
15426
|
+
const tokens = tokenize(body);
|
|
15427
|
+
let name;
|
|
15428
|
+
let dimension;
|
|
15429
|
+
let type;
|
|
15430
|
+
let implicit = false;
|
|
15431
|
+
let required = false;
|
|
15432
|
+
for (const token of tokens) {
|
|
15433
|
+
if (token.includes("=")) {
|
|
15434
|
+
const eqIndex = token.indexOf("=");
|
|
15435
|
+
const key = token.slice(0, eqIndex).toLowerCase();
|
|
15436
|
+
const value = token.slice(eqIndex + 1);
|
|
15437
|
+
switch (key) {
|
|
15438
|
+
case "name":
|
|
15439
|
+
name = value;
|
|
15440
|
+
break;
|
|
15441
|
+
case "dimension":
|
|
15442
|
+
dimension = value;
|
|
15443
|
+
break;
|
|
15444
|
+
case "type":
|
|
15445
|
+
if (!VALID_FILTER_TYPES.has(value)) {
|
|
15446
|
+
throw new Error(`Invalid filter type "${value}". Must be one of: ${[...VALID_FILTER_TYPES].join(", ")}`);
|
|
15447
|
+
}
|
|
15448
|
+
type = value;
|
|
15449
|
+
break;
|
|
15450
|
+
default:
|
|
15451
|
+
throw new Error(`Unknown filter parameter "${key}"`);
|
|
15070
15452
|
}
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
if (
|
|
15076
|
-
|
|
15077
|
-
|
|
15453
|
+
} else {
|
|
15454
|
+
const flag = token.toLowerCase();
|
|
15455
|
+
if (flag === "implicit") {
|
|
15456
|
+
implicit = true;
|
|
15457
|
+
} else if (flag === "required") {
|
|
15458
|
+
required = true;
|
|
15459
|
+
} else {
|
|
15460
|
+
throw new Error(`Unknown filter flag "${token}"`);
|
|
15078
15461
|
}
|
|
15079
|
-
if (text[i] === char)
|
|
15080
|
-
return i + 1;
|
|
15081
|
-
if (text[i] === `
|
|
15082
|
-
`)
|
|
15083
|
-
return i;
|
|
15084
|
-
i += 1;
|
|
15085
15462
|
}
|
|
15086
|
-
return text.length;
|
|
15087
15463
|
}
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
}
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
const direct = parseGuarded(motly);
|
|
15102
|
-
if (direct.messages.length === 0)
|
|
15103
|
-
return { tag: direct.tag, errors: envErrors };
|
|
15104
|
-
const rescued = motly.map((text) => {
|
|
15105
|
-
if (parseGuarded([text]).messages.length === 0)
|
|
15106
|
-
return text;
|
|
15107
|
-
const rewritten = quoteFilterLiterals(text);
|
|
15108
|
-
return parseGuarded([rewritten]).messages.length === 0 ? rewritten : text;
|
|
15109
|
-
});
|
|
15110
|
-
const after = parseGuarded(rescued);
|
|
15111
|
-
return { tag: after.tag, errors: [...envErrors, ...after.messages] };
|
|
15464
|
+
if (!dimension) {
|
|
15465
|
+
throw new Error("filter annotation missing required 'dimension' parameter");
|
|
15466
|
+
}
|
|
15467
|
+
if (!type) {
|
|
15468
|
+
throw new Error("filter annotation missing required 'type' parameter");
|
|
15469
|
+
}
|
|
15470
|
+
return {
|
|
15471
|
+
name: name ?? dimension,
|
|
15472
|
+
dimension,
|
|
15473
|
+
type,
|
|
15474
|
+
implicit,
|
|
15475
|
+
required
|
|
15476
|
+
};
|
|
15112
15477
|
}
|
|
15113
|
-
function
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15478
|
+
function parseFilters(annotations) {
|
|
15479
|
+
const byName = new Map;
|
|
15480
|
+
for (const annotation of annotations) {
|
|
15481
|
+
const parsed = parseFilterAnnotation(annotation);
|
|
15482
|
+
if (parsed) {
|
|
15483
|
+
byName.set(parsed.name, parsed);
|
|
15484
|
+
}
|
|
15118
15485
|
}
|
|
15486
|
+
return [...byName.values()];
|
|
15119
15487
|
}
|
|
15120
|
-
function
|
|
15121
|
-
|
|
15122
|
-
const raw = tagText(tag, ...path);
|
|
15123
|
-
if (raw === undefined)
|
|
15124
|
-
return;
|
|
15125
|
-
const DECIMAL = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/;
|
|
15126
|
-
const text = raw.trim();
|
|
15127
|
-
if (!DECIMAL.test(text))
|
|
15128
|
-
return;
|
|
15129
|
-
const value = Number(text);
|
|
15130
|
-
return Number.isFinite(value) ? value : undefined;
|
|
15131
|
-
} catch {
|
|
15132
|
-
return;
|
|
15133
|
-
}
|
|
15488
|
+
function escapeMalloyString(value) {
|
|
15489
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
15134
15490
|
}
|
|
15135
|
-
function
|
|
15136
|
-
|
|
15491
|
+
function isBooleanLiteral(v) {
|
|
15492
|
+
const lower = v.toLowerCase();
|
|
15493
|
+
return lower === "true" || lower === "false";
|
|
15137
15494
|
}
|
|
15138
|
-
function
|
|
15139
|
-
return
|
|
15495
|
+
function isDateLiteral(v) {
|
|
15496
|
+
return ISO_DATE_RE.test(v) || ISO_TIMESTAMP_RE.test(v);
|
|
15140
15497
|
}
|
|
15141
|
-
function
|
|
15142
|
-
|
|
15498
|
+
function malloyLiteral(v) {
|
|
15499
|
+
if (isBooleanLiteral(v)) {
|
|
15500
|
+
return v.toLowerCase();
|
|
15501
|
+
}
|
|
15502
|
+
if (isDateLiteral(v)) {
|
|
15503
|
+
return `@${v.slice(0, 10)}`;
|
|
15504
|
+
}
|
|
15505
|
+
return `'${escapeMalloyString(v)}'`;
|
|
15143
15506
|
}
|
|
15144
|
-
function
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
|
|
15507
|
+
function buildPredicate(filter, value) {
|
|
15508
|
+
const dim = `\`${filter.dimension}\``;
|
|
15509
|
+
switch (filter.type) {
|
|
15510
|
+
case "equal": {
|
|
15511
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
15512
|
+
return `${dim} = ${malloyLiteral(v)}`;
|
|
15513
|
+
}
|
|
15514
|
+
case "in": {
|
|
15515
|
+
const values = Array.isArray(value) ? value : [value];
|
|
15516
|
+
if (values.length === 1) {
|
|
15517
|
+
return `${dim} = ${malloyLiteral(values[0])}`;
|
|
15518
|
+
}
|
|
15519
|
+
const conditions = values.map((v) => `${dim} = ${malloyLiteral(v)}`);
|
|
15520
|
+
return `(${conditions.join(" or ")})`;
|
|
15521
|
+
}
|
|
15522
|
+
case "like": {
|
|
15523
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
15524
|
+
const escaped = escapeMalloyString(v.toLowerCase());
|
|
15525
|
+
const pattern = escaped.startsWith("%") || escaped.endsWith("%") ? escaped : `%${escaped}%`;
|
|
15526
|
+
return `lower(${dim}) ~ '${pattern}'`;
|
|
15527
|
+
}
|
|
15528
|
+
case "greater_than": {
|
|
15529
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
15530
|
+
return `${dim} > ${malloyLiteral(v)}`;
|
|
15531
|
+
}
|
|
15532
|
+
case "less_than": {
|
|
15533
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
15534
|
+
return `${dim} < ${malloyLiteral(v)}`;
|
|
15535
|
+
}
|
|
15149
15536
|
}
|
|
15150
15537
|
}
|
|
15151
|
-
function
|
|
15152
|
-
const value =
|
|
15153
|
-
|
|
15154
|
-
return isoText.slice(0, 10);
|
|
15155
|
-
const atUtcMidnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0;
|
|
15156
|
-
const year = atUtcMidnight ? value.getUTCFullYear() : value.getFullYear();
|
|
15157
|
-
const month = atUtcMidnight ? value.getUTCMonth() : value.getMonth();
|
|
15158
|
-
const day = atUtcMidnight ? value.getUTCDate() : value.getDate();
|
|
15159
|
-
return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
15538
|
+
function filterHasValue(filter, params) {
|
|
15539
|
+
const value = params[filter.name];
|
|
15540
|
+
return value !== undefined && value !== null && (Array.isArray(value) ? value.length > 0 : value !== "");
|
|
15160
15541
|
}
|
|
15161
|
-
function
|
|
15162
|
-
const
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
const text = tagText(tag2);
|
|
15169
|
-
if (text === undefined)
|
|
15542
|
+
function buildFilterClause(filters, params) {
|
|
15543
|
+
const predicates = [];
|
|
15544
|
+
for (const filter of filters) {
|
|
15545
|
+
if (!filterHasValue(filter, params)) {
|
|
15546
|
+
if (filter.required) {
|
|
15547
|
+
throw new FilterValidationError(`Required filter "${filter.name}" (dimension: ${filter.dimension}) was not provided`);
|
|
15548
|
+
}
|
|
15170
15549
|
continue;
|
|
15171
|
-
|
|
15172
|
-
|
|
15550
|
+
}
|
|
15551
|
+
predicates.push(buildPredicate(filter, params[filter.name]));
|
|
15173
15552
|
}
|
|
15174
|
-
|
|
15553
|
+
if (predicates.length === 0) {
|
|
15554
|
+
return "";
|
|
15555
|
+
}
|
|
15556
|
+
return predicates.join(" and ");
|
|
15175
15557
|
}
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
}
|
|
15181
|
-
|
|
15182
|
-
// src/service/given.ts
|
|
15183
|
-
import {
|
|
15184
|
-
isSourceDef as isSourceDef5
|
|
15185
|
-
} from "@malloydata/malloy";
|
|
15186
|
-
function presentText(tag, ...path) {
|
|
15187
|
-
const raw = tagText(tag, ...path);
|
|
15188
|
-
return raw !== undefined && raw.trim() !== "" ? raw : undefined;
|
|
15558
|
+
function injectFilterRefinement(query, filterClause) {
|
|
15559
|
+
if (!filterClause) {
|
|
15560
|
+
return query;
|
|
15561
|
+
}
|
|
15562
|
+
return `${query.trimEnd()}
|
|
15563
|
+
+ {where: ${filterClause}}`;
|
|
15189
15564
|
}
|
|
15190
|
-
function
|
|
15191
|
-
const
|
|
15192
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
const
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15199
|
-
|
|
15200
|
-
|
|
15201
|
-
|
|
15202
|
-
|
|
15203
|
-
|
|
15565
|
+
function tokenize(input) {
|
|
15566
|
+
const tokens = [];
|
|
15567
|
+
let current = "";
|
|
15568
|
+
let inQuote = false;
|
|
15569
|
+
let quoteChar = "";
|
|
15570
|
+
for (const ch of input) {
|
|
15571
|
+
if (inQuote) {
|
|
15572
|
+
if (ch === quoteChar) {
|
|
15573
|
+
inQuote = false;
|
|
15574
|
+
} else {
|
|
15575
|
+
current += ch;
|
|
15576
|
+
}
|
|
15577
|
+
} else if (ch === '"' || ch === "'") {
|
|
15578
|
+
inQuote = true;
|
|
15579
|
+
quoteChar = ch;
|
|
15580
|
+
} else if (ch === " " || ch === "\t") {
|
|
15581
|
+
if (current) {
|
|
15582
|
+
tokens.push(current);
|
|
15583
|
+
current = "";
|
|
15584
|
+
}
|
|
15585
|
+
} else {
|
|
15586
|
+
current += ch;
|
|
15587
|
+
}
|
|
15204
15588
|
}
|
|
15205
|
-
|
|
15206
|
-
|
|
15207
|
-
spec.rangeMin = rangeMin;
|
|
15208
|
-
const rangeMax = tagNumeric(tag, "range_max");
|
|
15209
|
-
if (rangeMax !== undefined)
|
|
15210
|
-
spec.rangeMax = rangeMax;
|
|
15211
|
-
const suggest = tag.tag("suggest");
|
|
15212
|
-
if (suggest) {
|
|
15213
|
-
const parsed = {};
|
|
15214
|
-
const query = presentText(suggest, "query");
|
|
15215
|
-
if (query !== undefined)
|
|
15216
|
-
parsed.query = query;
|
|
15217
|
-
const source = presentText(suggest, "source");
|
|
15218
|
-
if (source !== undefined)
|
|
15219
|
-
parsed.source = source;
|
|
15220
|
-
const dimension = presentText(suggest, "dimension");
|
|
15221
|
-
if (dimension !== undefined)
|
|
15222
|
-
parsed.dimension = dimension;
|
|
15223
|
-
const runnable = parsed.query !== undefined || parsed.source !== undefined && parsed.dimension !== undefined;
|
|
15224
|
-
if (runnable)
|
|
15225
|
-
spec.suggest = parsed;
|
|
15589
|
+
if (current) {
|
|
15590
|
+
tokens.push(current);
|
|
15226
15591
|
}
|
|
15227
|
-
return
|
|
15592
|
+
return tokens;
|
|
15228
15593
|
}
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15594
|
+
var VALID_FILTER_TYPES, ANNOTATION_PREFIX = "#(filter)", ISO_DATE_RE, ISO_TIMESTAMP_RE, FilterValidationError;
|
|
15595
|
+
var init_filter = __esm(() => {
|
|
15596
|
+
VALID_FILTER_TYPES = new Set([
|
|
15597
|
+
"equal",
|
|
15598
|
+
"in",
|
|
15599
|
+
"like",
|
|
15600
|
+
"greater_than",
|
|
15601
|
+
"less_than"
|
|
15602
|
+
]);
|
|
15603
|
+
ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
15604
|
+
ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
15605
|
+
FilterValidationError = class FilterValidationError extends Error {
|
|
15606
|
+
constructor(message) {
|
|
15607
|
+
super(message);
|
|
15608
|
+
this.name = "FilterValidationError";
|
|
15609
|
+
}
|
|
15239
15610
|
};
|
|
15240
|
-
}
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
const
|
|
15250
|
-
if (
|
|
15251
|
-
|
|
15611
|
+
});
|
|
15612
|
+
|
|
15613
|
+
// src/service/source_extraction.ts
|
|
15614
|
+
import {
|
|
15615
|
+
isJoined as isJoined3,
|
|
15616
|
+
isSourceDef as isSourceDef6,
|
|
15617
|
+
modelDefToModelInfo
|
|
15618
|
+
} from "@malloydata/malloy";
|
|
15619
|
+
function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
|
|
15620
|
+
const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
|
|
15621
|
+
if (ids.length === 0)
|
|
15622
|
+
return false;
|
|
15623
|
+
for (const id of ids) {
|
|
15624
|
+
const entry = modelDef.sourceRegistry?.[id]?.entry;
|
|
15625
|
+
if (!entry)
|
|
15626
|
+
continue;
|
|
15627
|
+
const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
|
|
15628
|
+
if (declared && isSourceDef6(declared))
|
|
15629
|
+
return false;
|
|
15252
15630
|
}
|
|
15253
|
-
|
|
15254
|
-
collectGivenRefs(child, into);
|
|
15631
|
+
return true;
|
|
15255
15632
|
}
|
|
15256
|
-
function
|
|
15257
|
-
|
|
15258
|
-
|
|
15633
|
+
function isEarlierPosition(a, b) {
|
|
15634
|
+
return a.line < b.line || a.line === b.line && a.character < b.character;
|
|
15635
|
+
}
|
|
15636
|
+
function considerNoteOwner(declaredBy, note, candidate) {
|
|
15637
|
+
const location = candidate.location;
|
|
15638
|
+
if (!location || location.url !== note.at.url)
|
|
15259
15639
|
return;
|
|
15260
|
-
|
|
15640
|
+
const currentLocation = declaredBy.get(note)?.location;
|
|
15641
|
+
if (!currentLocation || isEarlierPosition(location.range.start, currentLocation.range.start)) {
|
|
15642
|
+
declaredBy.set(note, candidate);
|
|
15643
|
+
}
|
|
15261
15644
|
}
|
|
15262
|
-
function
|
|
15263
|
-
const
|
|
15264
|
-
const
|
|
15265
|
-
|
|
15645
|
+
function collectSourceInfos(modelDef) {
|
|
15646
|
+
const names = Object.keys(modelDef.contents).filter((name) => isSourceDef6(modelDef.contents[name]));
|
|
15647
|
+
const exports = [...new Set([...modelDef.exports, ...names])];
|
|
15648
|
+
return modelDefToModelInfo({ ...modelDef, exports }).entries.filter((entry) => entry.kind === "source");
|
|
15649
|
+
}
|
|
15650
|
+
function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
15651
|
+
const filterMap = new Map;
|
|
15652
|
+
const authorizeMap = new Map;
|
|
15653
|
+
const misplacedAuthorize = [];
|
|
15654
|
+
const gatedSourceOwnAuthorizeNotes = new Set;
|
|
15655
|
+
const authorizeNoteDeclaredBy = new Map;
|
|
15656
|
+
const nearMissAuthorize = [];
|
|
15657
|
+
const sweptStructs = [];
|
|
15266
15658
|
for (const obj of Object.values(modelDef.contents)) {
|
|
15267
|
-
if (
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
|
|
15659
|
+
if (!isSourceDef6(obj))
|
|
15660
|
+
continue;
|
|
15661
|
+
const struct = obj;
|
|
15662
|
+
sweptStructs.push(obj);
|
|
15663
|
+
for (const note of ownLevelNotes(struct.annotations)) {
|
|
15664
|
+
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
15665
|
+
gatedSourceOwnAuthorizeNotes.add(note);
|
|
15666
|
+
considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
|
|
15274
15667
|
}
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15668
|
+
}
|
|
15669
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
|
|
15670
|
+
...ownLevelNotes(struct.annotations),
|
|
15671
|
+
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
15672
|
+
].map((note) => note.text)));
|
|
15673
|
+
}
|
|
15674
|
+
for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
|
|
15675
|
+
const entry = value.entry;
|
|
15676
|
+
if (entry.type === "source_registry_reference")
|
|
15677
|
+
continue;
|
|
15678
|
+
if (!isSourceDef6(entry))
|
|
15679
|
+
continue;
|
|
15680
|
+
sweptStructs.push(entry);
|
|
15681
|
+
for (const note of ownLevelNotes(entry.annotations)) {
|
|
15682
|
+
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
15683
|
+
gatedSourceOwnAuthorizeNotes.add(note);
|
|
15684
|
+
considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
|
|
15685
|
+
}
|
|
15686
|
+
}
|
|
15687
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes(ownLevelNotes(entry.annotations).map((note) => note.text)));
|
|
15688
|
+
}
|
|
15689
|
+
for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
|
|
15690
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
|
|
15691
|
+
...ownLevelNotes(struct.annotations),
|
|
15692
|
+
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
15693
|
+
].map((note) => note.text)));
|
|
15694
|
+
}
|
|
15695
|
+
{
|
|
15696
|
+
const folded = modelAnnotations(modelDef);
|
|
15697
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
|
|
15698
|
+
}
|
|
15699
|
+
assertNoAuthorizeNearMisses(nearMissAuthorize);
|
|
15700
|
+
const authorizeOwnNotes = new Map;
|
|
15701
|
+
const attributedAuthorizeOwnNotes = new Map;
|
|
15702
|
+
{
|
|
15703
|
+
const fileNoteTexts = ownLevelNoteTexts(modelAnnotations(modelDef));
|
|
15704
|
+
if (containsAuthorizeAnnotationTag(fileNoteTexts)) {
|
|
15705
|
+
misplacedAuthorize.push({
|
|
15706
|
+
kind: "file",
|
|
15707
|
+
route: fileNoteTexts.map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
15281
15708
|
});
|
|
15282
15709
|
}
|
|
15283
15710
|
}
|
|
15284
|
-
const
|
|
15285
|
-
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15711
|
+
const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef6(obj)).map((sourceObj) => {
|
|
15712
|
+
const struct = sourceObj;
|
|
15713
|
+
const sourceName = struct.as || struct.name;
|
|
15714
|
+
const annotations = annotationTexts(struct.annotations);
|
|
15715
|
+
const collected = [];
|
|
15716
|
+
let cur = struct.annotations;
|
|
15717
|
+
while (cur) {
|
|
15718
|
+
if (cur.blockNotes) {
|
|
15719
|
+
collected.push(cur.blockNotes.map((note) => note.text));
|
|
15720
|
+
}
|
|
15721
|
+
cur = cur.inherits;
|
|
15722
|
+
}
|
|
15723
|
+
const allAnnotations = collected.reverse().flat();
|
|
15724
|
+
let filters;
|
|
15725
|
+
if (allAnnotations.length > 0) {
|
|
15726
|
+
try {
|
|
15727
|
+
const parsed = parseFilters(allAnnotations);
|
|
15728
|
+
if (parsed.length > 0) {
|
|
15729
|
+
filterMap.set(sourceName, parsed);
|
|
15730
|
+
const fields = struct.fields;
|
|
15731
|
+
filters = parsed.map((f) => {
|
|
15732
|
+
const field = fields.find((fd) => (fd.as || fd.name) === f.dimension);
|
|
15733
|
+
return {
|
|
15734
|
+
name: f.name,
|
|
15735
|
+
dimension: f.dimension,
|
|
15736
|
+
type: f.type,
|
|
15737
|
+
implicit: f.implicit,
|
|
15738
|
+
required: f.required,
|
|
15739
|
+
dimensionType: field?.type
|
|
15740
|
+
};
|
|
15741
|
+
});
|
|
15742
|
+
}
|
|
15743
|
+
} catch (err) {
|
|
15744
|
+
onParseError?.(sourceName, err);
|
|
15745
|
+
}
|
|
15746
|
+
}
|
|
15747
|
+
const ownNotes = ownLevelNoteTexts(struct.annotations);
|
|
15748
|
+
const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
|
|
15749
|
+
const ownNotesByRoute = new Map;
|
|
15750
|
+
const attributedOwnNotesByRoute = new Map;
|
|
15751
|
+
for (const route of CANONICAL_AUTHORIZE_ROUTES) {
|
|
15752
|
+
const notesForRoute = ownAuthorizeNotes.filter((note) => authorizeAnnotationRoute(note.text) === route);
|
|
15753
|
+
ownNotesByRoute.set(route, notesForRoute);
|
|
15754
|
+
attributedOwnNotesByRoute.set(route, notesForRoute.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
|
|
15755
|
+
}
|
|
15756
|
+
authorizeOwnNotes.set(sourceName, ownNotesByRoute);
|
|
15757
|
+
attributedAuthorizeOwnNotes.set(sourceName, attributedOwnNotesByRoute);
|
|
15758
|
+
const groupsForSource = [];
|
|
15759
|
+
const routeGroupsByRoute = new Map;
|
|
15760
|
+
for (const route of CANONICAL_AUTHORIZE_ROUTES) {
|
|
15761
|
+
const ownExprsForRoute = collectAuthorizeExprsForRoute(ownNotes, route);
|
|
15762
|
+
const groupsForRoute = ownExprsForRoute.length > 0 ? [ownExprsForRoute] : effectiveAncestorGateExprs(struct, modelDef, route);
|
|
15763
|
+
for (const exprs of groupsForRoute) {
|
|
15764
|
+
groupsForSource.push({ route, exprs });
|
|
15765
|
+
}
|
|
15766
|
+
routeGroupsByRoute.set(route, groupsForRoute);
|
|
15767
|
+
}
|
|
15768
|
+
if (groupsForSource.length > 0) {
|
|
15769
|
+
authorizeMap.set(sourceName, groupsForSource);
|
|
15770
|
+
}
|
|
15771
|
+
const routeGroupsToFlatWire = (groups) => groups && groups.length > 0 ? groups.flat() : undefined;
|
|
15772
|
+
const authorize = routeGroupsToFlatWire(routeGroupsByRoute.get(AUTHORIZE_ROUTE));
|
|
15773
|
+
const accessFilter = routeGroupsToFlatWire(routeGroupsByRoute.get(ACCESS_FILTER_ROUTE));
|
|
15774
|
+
const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
|
|
15775
|
+
name: turtle.as || turtle.name,
|
|
15776
|
+
annotations: annotationTexts(turtle.annotations)
|
|
15777
|
+
}));
|
|
15778
|
+
for (const field of struct.fields) {
|
|
15779
|
+
const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
|
|
15780
|
+
if (fieldAuthorizeNotes.length === 0)
|
|
15781
|
+
continue;
|
|
15782
|
+
if (fieldAuthorizeNotes.every((note) => gatedSourceOwnAuthorizeNotes.has(note))) {
|
|
15783
|
+
continue;
|
|
15784
|
+
}
|
|
15785
|
+
const fieldName = field.as || field.name;
|
|
15786
|
+
if (isJoined3(field) && isSourceDef6(field)) {
|
|
15787
|
+
const joinedStruct = field;
|
|
15788
|
+
if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
|
|
15789
|
+
continue;
|
|
15790
|
+
}
|
|
15791
|
+
misplacedAuthorize.push({
|
|
15792
|
+
kind: "field",
|
|
15793
|
+
name: sourceName,
|
|
15794
|
+
fieldName,
|
|
15795
|
+
route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
15796
|
+
});
|
|
15797
|
+
continue;
|
|
15798
|
+
}
|
|
15799
|
+
misplacedAuthorize.push({
|
|
15800
|
+
kind: "field",
|
|
15801
|
+
name: sourceName,
|
|
15802
|
+
fieldName,
|
|
15803
|
+
route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
15804
|
+
});
|
|
15298
15805
|
}
|
|
15806
|
+
return {
|
|
15807
|
+
name: sourceName,
|
|
15808
|
+
annotations,
|
|
15809
|
+
views,
|
|
15810
|
+
filters,
|
|
15811
|
+
givens,
|
|
15812
|
+
authorize,
|
|
15813
|
+
accessFilter
|
|
15814
|
+
};
|
|
15815
|
+
});
|
|
15816
|
+
return {
|
|
15817
|
+
sources,
|
|
15818
|
+
filterMap,
|
|
15819
|
+
authorizeMap,
|
|
15820
|
+
misplacedAuthorize,
|
|
15821
|
+
authorizeOwnNotes,
|
|
15822
|
+
attributedAuthorizeOwnNotes
|
|
15299
15823
|
};
|
|
15300
15824
|
}
|
|
15301
|
-
function
|
|
15302
|
-
const
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
}
|
|
15825
|
+
function extractQueriesFromModelDef(modelDef) {
|
|
15826
|
+
const isNamedQuery = (obj) => obj.type === "query";
|
|
15827
|
+
const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
|
|
15828
|
+
const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
|
|
15829
|
+
kind: "query",
|
|
15830
|
+
name: queryObj.as || queryObj.name,
|
|
15831
|
+
route: ownLevelNoteTexts(queryObj.annotations).map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
15832
|
+
}));
|
|
15833
|
+
const queries = namedQueries.map((queryObj) => ({
|
|
15834
|
+
name: queryObj.as || queryObj.name,
|
|
15835
|
+
sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
|
|
15836
|
+
annotations: annotationTexts(queryObj.annotations)
|
|
15837
|
+
}));
|
|
15838
|
+
return { queries, misplacedAuthorize };
|
|
15315
15839
|
}
|
|
15316
|
-
var
|
|
15840
|
+
var init_source_extraction = __esm(() => {
|
|
15317
15841
|
init_annotations();
|
|
15318
15842
|
init_authorize();
|
|
15319
|
-
|
|
15843
|
+
init_authorize_routes();
|
|
15844
|
+
init_filter();
|
|
15845
|
+
init_gate_registry_walk();
|
|
15320
15846
|
});
|
|
15321
15847
|
|
|
15322
15848
|
// src/utils.ts
|
|
@@ -15353,7 +15879,7 @@ init_gate_dimension();
|
|
|
15353
15879
|
var import_recursive_readdir = __toESM(require_recursive_readdir(), 1);
|
|
15354
15880
|
import {
|
|
15355
15881
|
contextOverlay,
|
|
15356
|
-
isSourceDef as
|
|
15882
|
+
isSourceDef as isSourceDef7,
|
|
15357
15883
|
MalloyConfig,
|
|
15358
15884
|
MalloyError as MalloyError2,
|
|
15359
15885
|
modelDefToModelInfo as modelDefToModelInfo2,
|
|
@@ -15370,6 +15896,8 @@ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
|
15370
15896
|
|
|
15371
15897
|
// src/service/package_manifest.ts
|
|
15372
15898
|
init_constants();
|
|
15899
|
+
init_errors();
|
|
15900
|
+
init_dashboard();
|
|
15373
15901
|
var FRESHNESS_FALLBACKS = ["live", "stale_ok", "fail"];
|
|
15374
15902
|
var PACKAGE_SCOPES = ["version", "package"];
|
|
15375
15903
|
function parsePackageScope(raw) {
|
|
@@ -15379,7 +15907,7 @@ function parsePackageScope(raw) {
|
|
|
15379
15907
|
if (PACKAGE_SCOPES.includes(raw)) {
|
|
15380
15908
|
return raw;
|
|
15381
15909
|
}
|
|
15382
|
-
throw new
|
|
15910
|
+
throw new PackageManifestError(`Invalid "scope" in the package manifest: ${JSON.stringify(raw)}. ` + `Expected "version" or "package" (default "package").`);
|
|
15383
15911
|
}
|
|
15384
15912
|
function resolvePackageScope(rootRaw, materializationRaw) {
|
|
15385
15913
|
const envelopeRaw = materializationRaw && typeof materializationRaw === "object" ? materializationRaw.scope : undefined;
|
|
@@ -15389,7 +15917,7 @@ function resolvePackageScope(rootRaw, materializationRaw) {
|
|
|
15389
15917
|
const envelopeScope = parsePackageScope(envelopeRaw);
|
|
15390
15918
|
if (rootDeclared && envelopeDeclared) {
|
|
15391
15919
|
if (rootScope !== envelopeScope) {
|
|
15392
|
-
throw new
|
|
15920
|
+
throw new PackageManifestError(`Conflicting "scope" in publisher.json: root "${rootScope}" vs ` + `"materialization.scope" "${envelopeScope}". The package cannot ` + `load until they agree. Edit "materialization": { "scope": ... } ` + `to the value you want and delete the root-level "scope" (the ` + `server rewrites both homes on its next manifest write).`);
|
|
15393
15921
|
}
|
|
15394
15922
|
return { scope: envelopeScope, warnings: [] };
|
|
15395
15923
|
}
|
|
@@ -15502,43 +16030,51 @@ function resolveExplores(input) {
|
|
|
15502
16030
|
const hasIndexModel = modelPaths.includes(INDEX_MODEL_NAME);
|
|
15503
16031
|
const warnings = [];
|
|
15504
16032
|
if (declaredExplores !== undefined && !(Array.isArray(declaredExplores) && declaredExplores.every((entry) => typeof entry === "string"))) {
|
|
15505
|
-
throw new
|
|
16033
|
+
throw new PackageManifestError(`Invalid "explores" in publisher.json: it must be a list of file ` + `names, but is ${JSON.stringify(declaredExplores)}. The package ` + `was not loaded. Fix: delete "explores" and add an ` + `${INDEX_MODEL_NAME}.`);
|
|
15506
16034
|
}
|
|
15507
16035
|
const declared = Array.isArray(declaredExplores) ? declaredExplores.map((entry) => normalizeModelPath(entry)) : undefined;
|
|
15508
16036
|
if (declaredQueryableSources !== undefined && declaredQueryableSources !== "all") {
|
|
15509
16037
|
warnings.push(QUERYABLE_SOURCES_DEPRECATION);
|
|
15510
16038
|
}
|
|
15511
16039
|
if (declared !== undefined) {
|
|
15512
|
-
if (declared.length === 0
|
|
15513
|
-
warnings.push(EXPLORES_EMPTY_IS_UNCURATED);
|
|
15514
|
-
} else
|
|
15515
|
-
warnings.push(
|
|
15516
|
-
} else if (declared.length === 1) {
|
|
15517
|
-
warnings.push(EXPLORES_DEPRECATION);
|
|
16040
|
+
if (declared.length === 0) {
|
|
16041
|
+
warnings.push(hasIndexModel ? EXPLORES_EMPTY_SUPPRESSES_CONVENTION : EXPLORES_EMPTY_IS_UNCURATED);
|
|
16042
|
+
} else {
|
|
16043
|
+
warnings.push(exploresDeprecation(declared, hasIndexModel));
|
|
15518
16044
|
}
|
|
15519
|
-
if (hasIndexModel && declared.length > 0) {
|
|
15520
|
-
|
|
15521
|
-
warnings.push(exploresOmitsIndexModel(declared));
|
|
15522
|
-
}
|
|
16045
|
+
if (hasIndexModel && declared.length > 0 && !declared.includes(INDEX_MODEL_NAME)) {
|
|
16046
|
+
warnings.push(EXPLORES_OMITS_INDEX_MODEL);
|
|
15523
16047
|
}
|
|
15524
16048
|
return { explores: declared, warnings };
|
|
15525
16049
|
}
|
|
15526
16050
|
if (!hasIndexModel) {
|
|
16051
|
+
for (const modelPath of modelPaths) {
|
|
16052
|
+
if (!modelPath.includes("/") && modelPath.toLowerCase() === INDEX_MODEL_NAME) {
|
|
16053
|
+
warnings.push(`${modelPath} is ignored: only a root file named exactly ` + `${INDEX_MODEL_NAME} decides what is published. Fix: rename ` + `it to ${INDEX_MODEL_NAME}.`);
|
|
16054
|
+
}
|
|
16055
|
+
}
|
|
15527
16056
|
return { explores: undefined, warnings };
|
|
15528
16057
|
}
|
|
15529
|
-
if (modelPaths.some((modelPath) => modelPath.endsWith(MODEL_FILE_SUFFIX) && modelPath !== INDEX_MODEL_NAME)) {
|
|
15530
|
-
warnings.push(INDEX_MODEL_IS_THE_SURFACE);
|
|
15531
|
-
}
|
|
15532
16058
|
return { explores: [INDEX_MODEL_NAME], warnings };
|
|
15533
16059
|
}
|
|
15534
|
-
|
|
15535
|
-
|
|
15536
|
-
|
|
16060
|
+
function listFiles(files) {
|
|
16061
|
+
return files.length <= 1 ? files[0] ?? "" : `${files.slice(0, -1).join(", ")} and ${files[files.length - 1]}`;
|
|
16062
|
+
}
|
|
16063
|
+
function exploresDeprecation(declared, hasIndexModel) {
|
|
16064
|
+
const lead = `"explores" in publisher.json is deprecated.`;
|
|
16065
|
+
const files = declared.filter((entry) => entry !== INDEX_MODEL_NAME && !isDashboardModelPath(entry));
|
|
16066
|
+
if (files.length > 0) {
|
|
16067
|
+
return hasIndexModel ? `${lead} Fix: import ${listFiles(files)} into ${INDEX_MODEL_NAME}, ` + `export the sources you publish from it, then delete "explores".` : `${lead} Fix: add an ${INDEX_MODEL_NAME} at the package root that ` + `imports ${listFiles(files)} and exports the sources you want to ` + `publish, then delete "explores".`;
|
|
16068
|
+
}
|
|
16069
|
+
if (declared.includes(INDEX_MODEL_NAME)) {
|
|
16070
|
+
return `${lead} ${INDEX_MODEL_NAME} already publishes the same thing. ` + `Fix: delete "explores".`;
|
|
16071
|
+
}
|
|
16072
|
+
return hasIndexModel ? `${lead} Fix: delete "explores", so ${INDEX_MODEL_NAME} decides what is published.` : `${lead} Fix: add an ${INDEX_MODEL_NAME} at the package root that ` + `exports the sources you want to publish, then delete "explores".`;
|
|
15537
16073
|
}
|
|
15538
|
-
var
|
|
15539
|
-
var EXPLORES_EMPTY_SUPPRESSES_CONVENTION = `"explores"
|
|
15540
|
-
var EXPLORES_EMPTY_IS_UNCURATED = `"explores": [] in publisher.json
|
|
15541
|
-
var QUERYABLE_SOURCES_DEPRECATION = `"queryableSources" in publisher.json
|
|
16074
|
+
var EXPLORES_OMITS_INDEX_MODEL = `${INDEX_MODEL_NAME} is ignored because "explores" in publisher.json ` + `doesn't list it. Fix: delete "explores" to publish what ` + `${INDEX_MODEL_NAME} exports, or rename ${INDEX_MODEL_NAME} (and any import ` + `of it) if it isn't meant to decide what is published.`;
|
|
16075
|
+
var EXPLORES_EMPTY_SUPPRESSES_CONVENTION = `"explores" in publisher.json is deprecated. Here it stops ` + `${INDEX_MODEL_NAME} from limiting what this package publishes. Fix: to ` + `publish everything, rename ${INDEX_MODEL_NAME}, point any import of it at ` + `the new name, then delete "explores".`;
|
|
16076
|
+
var EXPLORES_EMPTY_IS_UNCURATED = `"explores": [] in publisher.json does nothing. Fix: delete it.`;
|
|
16077
|
+
var QUERYABLE_SOURCES_DEPRECATION = `"queryableSources" in publisher.json does nothing. Fix: delete it.`;
|
|
15542
16078
|
|
|
15543
16079
|
// src/package_load/package_load_worker.ts
|
|
15544
16080
|
init_source_extraction();
|
|
@@ -15776,7 +16312,15 @@ function buildWorkerMalloyConfig(job) {
|
|
|
15776
16312
|
async function readPackageMetadata(packagePath, modelPaths) {
|
|
15777
16313
|
const manifestPath = path2.join(packagePath, PACKAGE_MANIFEST_NAME);
|
|
15778
16314
|
const contents = await fs2.promises.readFile(manifestPath, "utf8");
|
|
15779
|
-
|
|
16315
|
+
let parsed;
|
|
16316
|
+
try {
|
|
16317
|
+
parsed = JSON.parse(contents);
|
|
16318
|
+
} catch (error) {
|
|
16319
|
+
throw new PackageManifestError(`Invalid ${PACKAGE_MANIFEST_NAME}: it is not valid JSON ` + `(${error instanceof Error ? error.message : String(error)}). ` + `The package is not served until the file parses. Fix: make it ` + `valid JSON; a trailing comma or an unquoted key is the usual cause.`);
|
|
16320
|
+
}
|
|
16321
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16322
|
+
throw new PackageManifestError(`Invalid ${PACKAGE_MANIFEST_NAME}: expected a JSON object, got ` + `${JSON.stringify(parsed)}. Fix: { "name": "my-package" }.`);
|
|
16323
|
+
}
|
|
15780
16324
|
const scope = resolvePackageScope(parsed.scope, parsed.materialization);
|
|
15781
16325
|
const queryMetadata = resolvePackageQueryMetadata(parsed.queryMetadata, parsed.materialization);
|
|
15782
16326
|
const explores = resolveExplores({
|
|
@@ -15910,7 +16454,7 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
|
|
|
15910
16454
|
onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
|
|
15911
16455
|
onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
|
|
15912
16456
|
const struct = modelDef.contents[sourceName];
|
|
15913
|
-
if (!struct || !
|
|
16457
|
+
if (!struct || !isSourceDef7(struct))
|
|
15914
16458
|
return;
|
|
15915
16459
|
validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
|
|
15916
16460
|
recordRowLevelGateRejected(cause);
|
|
@@ -16035,7 +16579,7 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
|
|
|
16035
16579
|
onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
|
|
16036
16580
|
onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
|
|
16037
16581
|
const struct = finalCompiledModelDef.contents[sourceName];
|
|
16038
|
-
if (!struct || !
|
|
16582
|
+
if (!struct || !isSourceDef7(struct))
|
|
16039
16583
|
return;
|
|
16040
16584
|
validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
|
|
16041
16585
|
recordRowLevelGateRejected(cause);
|
|
@@ -16133,6 +16677,14 @@ function serializeError(error) {
|
|
|
16133
16677
|
isCompilationError: true
|
|
16134
16678
|
};
|
|
16135
16679
|
}
|
|
16680
|
+
if (error instanceof PackageManifestError) {
|
|
16681
|
+
return {
|
|
16682
|
+
name: error.name,
|
|
16683
|
+
message: error.message,
|
|
16684
|
+
stack: error.stack,
|
|
16685
|
+
isManifestError: true
|
|
16686
|
+
};
|
|
16687
|
+
}
|
|
16136
16688
|
if (error instanceof Error) {
|
|
16137
16689
|
return {
|
|
16138
16690
|
name: error.name,
|