@datarails-ai/dr-appspace-cli 0.12.36 → 0.12.38
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/README.md +5 -2
- package/dist/cli.js +731 -112
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
|
|
|
13
13
|
// package.json
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "@datarails-ai/dr-appspace-cli",
|
|
16
|
-
version: "0.12.
|
|
16
|
+
version: "0.12.38",
|
|
17
17
|
description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
|
|
18
18
|
type: "module",
|
|
19
19
|
engines: {
|
|
@@ -57,7 +57,7 @@ var package_default = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// src/buildInfo.ts
|
|
60
|
-
var BUILD_TIME = "2026-10-
|
|
60
|
+
var BUILD_TIME = "2026-10-26T15:07:50.401Z";
|
|
61
61
|
function formatBuildTime(iso) {
|
|
62
62
|
if (iso === "") return "";
|
|
63
63
|
const d = new Date(iso);
|
|
@@ -6140,6 +6140,15 @@ function rendererOf(pages) {
|
|
|
6140
6140
|
}
|
|
6141
6141
|
return {};
|
|
6142
6142
|
}
|
|
6143
|
+
function metricsOf(pages) {
|
|
6144
|
+
for (const page of pages ?? []) {
|
|
6145
|
+
const embedded = embeddedContractOf(page);
|
|
6146
|
+
if (embedded === null) continue;
|
|
6147
|
+
const metrics = embedded.data?.metrics;
|
|
6148
|
+
return isPlainObject(metrics) ? { metrics } : {};
|
|
6149
|
+
}
|
|
6150
|
+
return {};
|
|
6151
|
+
}
|
|
6143
6152
|
function cardSpecOf(element) {
|
|
6144
6153
|
const card = element?.props?.card;
|
|
6145
6154
|
return element?.type === "widget" && typeof card === "string" && card !== "" ? card : null;
|
|
@@ -6729,7 +6738,9 @@ function dataSection(app, manifest, spec, elements, describe7, problems) {
|
|
|
6729
6738
|
...application === null ? {} : { app: application },
|
|
6730
6739
|
// Only when there are any: a contract written before sources existed
|
|
6731
6740
|
// reads back byte for byte as it always did.
|
|
6732
|
-
...Object.keys(sources).length > 0 ? { sources } : {}
|
|
6741
|
+
...Object.keys(sources).length > 0 ? { sources } : {},
|
|
6742
|
+
// The same for declared metrics, which only an app's pages carry.
|
|
6743
|
+
...isApp ? metricsOf(app.pages) : {}
|
|
6733
6744
|
};
|
|
6734
6745
|
if (describe7) {
|
|
6735
6746
|
const widgets2 = isPlainObject(describe7.widgets) ? describe7.widgets : {};
|
|
@@ -7780,8 +7791,11 @@ var contract_schema_default = {
|
|
|
7780
7791
|
required: ["from"],
|
|
7781
7792
|
properties: {
|
|
7782
7793
|
from: {
|
|
7783
|
-
description: 'one of four spellings: {"financeos": {...}} (the dashboard SPEC source vocabulary), {"table": "<app table>"}, {"call": "<dotted method>", "args": {...}}, or ["<source>", ...] to compose others (with a transform, or with an expr the platform evaluates)',
|
|
7784
|
-
oneOf: [
|
|
7794
|
+
description: 'one of four spellings: {"financeos": {...}} (the dashboard SPEC source vocabulary; a metric is {"metric": "@<key>"}, a key data.metrics declares, or {"metric_id": <id>}, which ties the app to one company), {"table": "<app table>"}, {"call": "<dotted method>", "args": {...}}, or ["<source>", ...] to compose others (with a transform, or with an expr the platform evaluates)',
|
|
7795
|
+
oneOf: [
|
|
7796
|
+
{ type: "object", properties: { financeos: { type: "object", properties: { metric: { type: "string", description: 'OPTIONAL. The metric this source reads, as "@<key>": a key data.metrics declares ("@revenue_mom"). `app-space ship` binds the key to the company\'s own metric, so the contract names no metric id; the read parameters beside it (scenario, aggregation_type, period, honors, drill_rows, filterable) are what they are for metric_id, and the two never stand on one source. The compiler refuses the rest (source_metric).' } } } } },
|
|
7797
|
+
{ type: "array", items: { type: "string" } }
|
|
7798
|
+
]
|
|
7785
7799
|
},
|
|
7786
7800
|
expr: { type: "string", description: "OPTIONAL, beside from as a list and no transform (lineage tier 3, docs/appspace/design-computed-card-lineage.md): the source's figure as a formula over its from sources' figures, which the PLATFORM evaluates, so the figure is made of Datarails reads and never computed by the app. It may use numbers, the operands' names, + - * / and parentheses (/ is a safe divide: a zero or missing divisor is no figure, and any missing figure makes the result none), and abs(x), min(a, b, ...), max(a, b, ...), round(x) or round(x, decimals 0-6, half away from zero), and if(comparison, then, else), the one place a comparison (< <= > >= == !=) may stand. Shape kpi (the value, and the compare where every operand carries one), series (point by point: each series operand draws one line, and a kpi operand is one figure at every point) or breakdown (member by member: every breakdown operand states top: null, and a kpi operand is one figure for every member). Its figure comes from an operand on every path: each branch of if() and each argument of min()/max() reads an operand, so a number alone (870000, or a fallback 0) is never the figure; write actual / budget, not if(budget > 0, actual / budget, 0). Every operand is a FinanceOS table read or metric (from.financeos) or another expr source over them - never a transform's output, a composed source, a call or an app table (a KPI belongs in the Datarails semantic layer, not in app data) - and every operand is used; compare is not stated here (it is the formula over the operands' compares). The compiler refuses the rest (source_expr)." },
|
|
7787
7801
|
shape: { enum: ["rows", "series", "kpi", "breakdown", "table", "ohlc", "scatter", "narrative"] },
|
|
@@ -7798,6 +7812,53 @@ var contract_schema_default = {
|
|
|
7798
7812
|
}
|
|
7799
7813
|
}
|
|
7800
7814
|
},
|
|
7815
|
+
metrics: {
|
|
7816
|
+
$comment: "Present only for kind: app, and only when the contract declares any (docs/appspace/research/2026-10-25-datarails-calc-layer/06-design-contract-declared-metrics.md \xA72). The package carries it in the contract its pages embed. The key pattern and the per-kind rules are the compiler's (this schema has no propertyNames and no conditional keyword): metric_decl, metric_kind_later, metric_formula, metric_base, metric_format.",
|
|
7817
|
+
type: "object",
|
|
7818
|
+
description: `the Datarails metrics this app reads, declared rather than addressed by id, keyed by a local name: a lowercase letter, then lowercase letters, digits or _ (revenue_mom), never sent to Datarails. A source reads one with from.financeos.metric "@<key>" and a CALC formula with [@<key>]. \`app-space ship\` binds each key in the company it ships to: it reuses the company's metric with the same definition, or the one with the same name when reuse is "name", and creates it otherwise (create), as the person shipping; a metric with the declared name and another definition is refused, never suffixed. So the contract names no company's metric, and every figure stays a Datarails metric read, the same number MCP and DR.METRIC give. A declaration with no definition binds the company's own metric by name: {"name": "Revenue", "reuse": "name", "create": false}.`,
|
|
7819
|
+
additionalProperties: {
|
|
7820
|
+
type: "object",
|
|
7821
|
+
required: ["name"],
|
|
7822
|
+
additionalProperties: false,
|
|
7823
|
+
properties: {
|
|
7824
|
+
name: { type: "string", minLength: 1, maxLength: 128, description: `the metric's name in Datarails, which finance users, MCP and DR.METRIC see, so a name a finance team would use ("Revenue MoM growth"), never an app id: at most 128 characters, no ' [ or ] (a CALC formula's reference delimiters), not starting with @, and not DAYS_IN_PERIOD or PERIODS_IN_YEAR (Datarails reserves both)` },
|
|
7825
|
+
kind: { enum: ["base", "calc", "driver"], description: "base (a table's value field aggregated by its date field) or calc (a formula over other declared metrics). driver is reserved: the compiler refuses it (metric_kind_later) until driver declarations ship. Absent only on a declaration with no definition (create: false, reuse: name)" },
|
|
7826
|
+
formula: { type: "string", description: "calc only: the metric in Datarails' CALC syntax: numbers, + - * / and parentheses over operands, each [@<key>] (another metric this block declares) or a system variable written bare, PERIODS_IN_YEAR or DAYS_IN_PERIOD. An operand may carry one modifier: .SHIFT(unit, offset), unit year, quarter, month, day or consumption_period and offset 0 to -1200, or .AS_OF(anchor), anchor year_end, quarter_end or month_end. A window function avg|min|max([@<key>], unit, periods), unit year, quarter or month and periods 1 to 120, compiles with a warning (metric_window): Datarails evaluates it only where the company has scenario_bound_calculations on. Never a metric's name, quoted or bracketed ('Revenue', [Revenue]): the contract names no company's metric. No scenario pin (| Budget), at least one [@<key>], at most 5000 characters, and no declaration that reads itself, however far round. ([@revenue] - [@revenue].SHIFT(month, -1)) / [@revenue].SHIFT(month, -1)" },
|
|
7827
|
+
table: {
|
|
7828
|
+
type: "object",
|
|
7829
|
+
additionalProperties: false,
|
|
7830
|
+
description: 'base only: the FinanceOS table, exactly one of {"alias": "<table alias>"} or {"name": "<table name>"}, resolved in the company at ship, or {"id": <id>}, which ties the app to one company as metric_id does',
|
|
7831
|
+
properties: {
|
|
7832
|
+
alias: { type: "string", minLength: 1 },
|
|
7833
|
+
name: { type: "string", minLength: 1 },
|
|
7834
|
+
id: { type: "integer", minimum: 1 }
|
|
7835
|
+
}
|
|
7836
|
+
},
|
|
7837
|
+
value: { type: "string", minLength: 1, description: "base only: the field aggregated, by its alias, falling back to its name; resolved to the field's id in the company at ship" },
|
|
7838
|
+
date: { type: "string", minLength: 1, description: "base only: the field the metric's periods come from, by its alias, falling back to its name" },
|
|
7839
|
+
aggregate: { enum: ["sum", "count", "cumulative_sum", "cumulative_count"], description: "base only: how the value field aggregates in a period; the cumulative ones carry the total forward" },
|
|
7840
|
+
time_aggregation: { enum: ["sum", "eop", "avg"], description: "OPTIONAL, base only: how periods roll up into a longer one (a quarter of months). Absent: sum, or eop for a cumulative aggregate, which may not roll up with sum" },
|
|
7841
|
+
filters: {
|
|
7842
|
+
type: "array",
|
|
7843
|
+
description: 'OPTIONAL, base only: the rows the metric reads, each {"field": "<alias or name>", "values": [...], "exclude": false}; exclude: true reads every row but those values',
|
|
7844
|
+
items: {
|
|
7845
|
+
type: "object",
|
|
7846
|
+
required: ["field", "values"],
|
|
7847
|
+
additionalProperties: false,
|
|
7848
|
+
properties: {
|
|
7849
|
+
field: { type: "string", minLength: 1 },
|
|
7850
|
+
values: { type: "array", minItems: 1, items: { type: ["string", "number"] } },
|
|
7851
|
+
exclude: { type: "boolean" }
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
},
|
|
7855
|
+
format: { type: "string", description: `OPTIONAL: the metric's display in Datarails (its Metric page, MCP, Excel), one of the 180 strings the metric editor writes: an optional currency prefix ("$" quoted, or \u20AC \xA3 \u20AA), #,### or ####, optional decimals .# to .####, an optional scale [kilo]k or [mega]M, and an optional % suffix (never with a currency): #,### "$"#,###.## ####.#%. A card keeps its own format` },
|
|
7856
|
+
description: { type: "string", description: "OPTIONAL: what the metric is, in words, for its page in Datarails" },
|
|
7857
|
+
reuse: { enum: ["definition", "name"], description: "OPTIONAL. definition (the default): bind only a company metric whose definition matches this one. name: bind the company's metric of this name whatever its definition, so a finance team's own Revenue wins; created from this definition when the company has none" },
|
|
7858
|
+
create: { type: "boolean", description: "OPTIONAL. true (the default): create the metric when the company has none to bind. false: bind only, never create; a declaration with no definition says false" }
|
|
7859
|
+
}
|
|
7860
|
+
}
|
|
7861
|
+
},
|
|
7801
7862
|
library: {
|
|
7802
7863
|
oneOf: [
|
|
7803
7864
|
{ type: "null" },
|
|
@@ -9557,7 +9618,7 @@ function rolesLandingNowhere(contract) {
|
|
|
9557
9618
|
}
|
|
9558
9619
|
return out;
|
|
9559
9620
|
}
|
|
9560
|
-
function sourceRoles(contract, name) {
|
|
9621
|
+
function sourceRoles(contract, name, known = /* @__PURE__ */ new Set()) {
|
|
9561
9622
|
const source = contract?.data?.sources?.[name];
|
|
9562
9623
|
const stated2 = gateRoles(source?.requiresRole);
|
|
9563
9624
|
const elements = isPlainObject3(contract?.elements) ? contract.elements : {};
|
|
@@ -9575,7 +9636,7 @@ function sourceRoles(contract, name) {
|
|
|
9575
9636
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
9576
9637
|
}
|
|
9577
9638
|
for (const page of pagesOf(contract)) {
|
|
9578
|
-
if (page.owned !== true || readers === 0) continue;
|
|
9639
|
+
if (page.owned !== true || readers === 0 || known.has(page.path)) continue;
|
|
9579
9640
|
const roles = pageRoles(page);
|
|
9580
9641
|
if (roles.length === 0) open2 = true;
|
|
9581
9642
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
@@ -10469,14 +10530,14 @@ var PROP_ORDER = ["kind", "params", "title", "subtitle", "description", "source"
|
|
|
10469
10530
|
function compile(contract, { kit, extra = {} } = {}) {
|
|
10470
10531
|
if (!isPlainObject6(kit)) throw new TypeError("compile(contract, {kit}): the kit manifest (kit.json) says which shelf modules a page loads");
|
|
10471
10532
|
const problems = [];
|
|
10472
|
-
const
|
|
10533
|
+
const refuse2 = (code, where2, message) => {
|
|
10473
10534
|
problems.push({ code, where: where2, lossy: false, message });
|
|
10474
10535
|
return { files: {}, problems };
|
|
10475
10536
|
};
|
|
10476
10537
|
const source = isPlainObject6(contract) ? contract : {};
|
|
10477
10538
|
const kind = source.app?.kind ?? null;
|
|
10478
10539
|
if (kind !== "dashboard") {
|
|
10479
|
-
return
|
|
10540
|
+
return refuse2(
|
|
10480
10541
|
"kind_not_compilable",
|
|
10481
10542
|
MANIFEST_FILE,
|
|
10482
10543
|
`this compiler writes dashboards; app.kind is ${JSON.stringify(kind)}, and a kind: app contract is compiled by compileApp`
|
|
@@ -10484,23 +10545,23 @@ function compile(contract, { kit, extra = {} } = {}) {
|
|
|
10484
10545
|
}
|
|
10485
10546
|
if (source.app?.renderer === RUNTIME_RENDERER2) return compileApp(source, { kit, extra });
|
|
10486
10547
|
if (source.app?.renderer !== void 0) {
|
|
10487
|
-
return
|
|
10548
|
+
return refuse2(
|
|
10488
10549
|
"renderer_not_compilable",
|
|
10489
10550
|
MANIFEST_FILE,
|
|
10490
10551
|
`app.renderer is ${JSON.stringify(source.app.renderer)}; the one renderer a dashboard may name is ${JSON.stringify(RUNTIME_RENDERER2)} (the app runtime, whose page compileApp writes), and absent means the dr-widgets page this compiler writes. Remove it, or set it to ${JSON.stringify(RUNTIME_RENDERER2)}`
|
|
10491
10552
|
);
|
|
10492
10553
|
}
|
|
10493
10554
|
const described = describeTier(source.data);
|
|
10494
|
-
if (described !== null) return
|
|
10555
|
+
if (described !== null) return refuse2("describe_tier_not_compilable", described.where, described.message);
|
|
10495
10556
|
const { contract: composed, applied } = composeLayout(source);
|
|
10496
10557
|
for (const fill of applied) {
|
|
10497
10558
|
problems.push({ code: "layout_composed", where: fill.where, lossy: false, message: `${fill.message} (${fill.what})` });
|
|
10498
10559
|
}
|
|
10499
10560
|
const pages = [...Array.isArray(composed.pages) ? composed.pages : []].sort((a, b) => natural3(a.path ?? "", b.path ?? ""));
|
|
10500
|
-
if (pages.length === 0) return
|
|
10561
|
+
if (pages.length === 0) return refuse2("no_pages", MANIFEST_FILE, "the contract declares no pages; a dashboard is one page of widgets or more");
|
|
10501
10562
|
const handlers = [...new Set(pages.map((page) => page.mount?.handler).filter((name) => typeof name === "string" && name !== ""))];
|
|
10502
10563
|
if (handlers.length !== 1) {
|
|
10503
|
-
return
|
|
10564
|
+
return refuse2(
|
|
10504
10565
|
"one_handler_per_dashboard",
|
|
10505
10566
|
MANIFEST_FILE,
|
|
10506
10567
|
handlers.length === 0 ? "no page names a mount handler; every widget on a dashboard is answered by one handler" : `the pages name ${handlers.length} mount handlers (${handlers.join(", ")}); a dashboard has one, and a second source is a second ROW in its SPEC`
|
|
@@ -10508,7 +10569,7 @@ function compile(contract, { kit, extra = {} } = {}) {
|
|
|
10508
10569
|
}
|
|
10509
10570
|
const handlerName = handlers[0];
|
|
10510
10571
|
const handler = dashboardHandler(composed, handlerName, extra);
|
|
10511
|
-
if (handler.unwritable !== null) return
|
|
10572
|
+
if (handler.unwritable !== null) return refuse2(handler.unwritable.code, handler.unwritable.where, handler.unwritable.message);
|
|
10512
10573
|
for (const note of handler.notes) problems.push(note);
|
|
10513
10574
|
if (handler.refusals.length > 0) {
|
|
10514
10575
|
for (const problem5 of handler.refusals) problems.push(problem5);
|
|
@@ -11045,7 +11106,7 @@ var DROPPED_REGIONS = /* @__PURE__ */ new Set(["controls", "band"]);
|
|
|
11045
11106
|
var KEPT_CARD_PROPS = ["label", "format", "negative", "props"];
|
|
11046
11107
|
function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt, appComponents, appControls } = {}) {
|
|
11047
11108
|
const problems = [];
|
|
11048
|
-
const
|
|
11109
|
+
const refuse2 = (code, pointer2, message) => problems.push({ code, pointer: pointer2, message });
|
|
11049
11110
|
const note = (code, where2, message) => problems.push({ code, where: where2, lossy: false, message });
|
|
11050
11111
|
const refused = (restated2 = contract) => ({ files: {}, problems, contract: restated2, warnings: [] });
|
|
11051
11112
|
const source = isPlainObject7(contract) ? contract : {};
|
|
@@ -11053,19 +11114,19 @@ function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt
|
|
|
11053
11114
|
const markdown = named5(kit?.markdown) ? kit.markdown : MARKDOWN;
|
|
11054
11115
|
const described = describeTier(source.data);
|
|
11055
11116
|
if (described !== null) {
|
|
11056
|
-
|
|
11117
|
+
refuse2("describe_tier_not_compilable", described.where, described.message);
|
|
11057
11118
|
return refused();
|
|
11058
11119
|
}
|
|
11059
11120
|
const { contract: composed, applied } = composeLayout(source);
|
|
11060
11121
|
for (const fill of applied) note("layout_composed", fill.where, `${fill.message} (${fill.what})`);
|
|
11061
11122
|
const pages = [...Array.isArray(composed.pages) ? composed.pages : []].sort((a, b) => natural3(a.path ?? "", b.path ?? ""));
|
|
11062
11123
|
if (pages.length === 0) {
|
|
11063
|
-
|
|
11124
|
+
refuse2("no_pages", "/pages", "the contract declares no pages; a dashboard is one page of widgets or more");
|
|
11064
11125
|
return refused();
|
|
11065
11126
|
}
|
|
11066
11127
|
const handlers = [...new Set(pages.map((page) => page.mount?.handler).filter((name) => typeof name === "string" && name !== ""))];
|
|
11067
11128
|
if (handlers.length !== 1) {
|
|
11068
|
-
|
|
11129
|
+
refuse2(
|
|
11069
11130
|
"one_handler_per_dashboard",
|
|
11070
11131
|
"/pages",
|
|
11071
11132
|
handlers.length === 0 ? "no page names a mount handler; every widget on a dashboard is answered by one handler" : `the pages name ${handlers.length} mount handlers (${handlers.join(", ")}); a dashboard has one, and a second source is a second ROW in its SPEC`
|
|
@@ -11075,15 +11136,15 @@ function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt
|
|
|
11075
11136
|
const handlerName = handlers[0];
|
|
11076
11137
|
const handler = dashboardHandler(composed, handlerName, extra);
|
|
11077
11138
|
if (handler.unwritable !== null) {
|
|
11078
|
-
|
|
11139
|
+
refuse2(handler.unwritable.code, handler.unwritable.where, handler.unwritable.message);
|
|
11079
11140
|
return refused();
|
|
11080
11141
|
}
|
|
11081
11142
|
problems.push(...handler.notes);
|
|
11082
11143
|
if (handler.refusals.length > 0) {
|
|
11083
|
-
for (const problem5 of handler.refusals)
|
|
11144
|
+
for (const problem5 of handler.refusals) refuse2(problem5.code, problem5.where, problem5.message);
|
|
11084
11145
|
return refused();
|
|
11085
11146
|
}
|
|
11086
|
-
const restated = restateDashboard(composed, pages, handlerName, handler, { refuse, note, stated: Array.isArray(source.pages) ? source.pages : [] });
|
|
11147
|
+
const restated = restateDashboard(composed, pages, handlerName, handler, { refuse: refuse2, note, stated: Array.isArray(source.pages) ? source.pages : [] });
|
|
11087
11148
|
const whole = canonical({
|
|
11088
11149
|
...composed.v !== void 0 ? { v: composed.v } : {},
|
|
11089
11150
|
...composed.generated_by !== void 0 ? { generated_by: composed.generated_by } : {},
|
|
@@ -11120,14 +11181,14 @@ function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt
|
|
|
11120
11181
|
function isRefusal(problem5) {
|
|
11121
11182
|
return problem5.lossy !== false;
|
|
11122
11183
|
}
|
|
11123
|
-
function restateDashboard(composed, pages, handlerName, handler, { refuse, note, stated: stated2 }) {
|
|
11184
|
+
function restateDashboard(composed, pages, handlerName, handler, { refuse: refuse2, note, stated: stated2 }) {
|
|
11124
11185
|
const app = isPlainObject7(composed.app) ? composed.app : {};
|
|
11125
11186
|
const typed = isPlainObject7(composed.elements) ? composed.elements : {};
|
|
11126
11187
|
const declared = treeOf(typed);
|
|
11127
11188
|
const conflicts = new Map(declared.problems.map((problem5) => [uidOfPointer(problem5.pointer), problem5]));
|
|
11128
11189
|
const paths = new Set(pages.map((page) => page.path));
|
|
11129
11190
|
const unplaced = Object.fromEntries(Object.keys(typed).filter((uid) => isPlainObject7(typed[uid]) && !paths.has(typed[uid].page)).map((uid) => [uid, typed[uid]]));
|
|
11130
|
-
for (const problem5 of placementProblems(unplaced, pages, declared, []))
|
|
11191
|
+
for (const problem5 of placementProblems(unplaced, pages, declared, [])) refuse2(problem5.code, problem5.pointer, problem5.message);
|
|
11131
11192
|
const elements = {};
|
|
11132
11193
|
const outPages = [];
|
|
11133
11194
|
for (const page of pages) {
|
|
@@ -11163,7 +11224,7 @@ function restateDashboard(composed, pages, handlerName, handler, { refuse, note,
|
|
|
11163
11224
|
const onlyOne = (uids, what) => {
|
|
11164
11225
|
for (const uid of uids.slice(1)) {
|
|
11165
11226
|
duplicates.add(uid);
|
|
11166
|
-
|
|
11227
|
+
refuse2(
|
|
11167
11228
|
"chrome_duplicate",
|
|
11168
11229
|
`/elements/${escapePointer3(uid)}`,
|
|
11169
11230
|
`"${uid}" is a second ${what} on ${path3}, after "${uids[0]}"; a dashboard page has one of each piece of chrome \u2014 remove it, or merge it into "${uids[0]}"`
|
|
@@ -11237,12 +11298,12 @@ function restateDashboard(composed, pages, handlerName, handler, { refuse, note,
|
|
|
11237
11298
|
const params = (Array.isArray(page.params) ? page.params : []).filter(isPlainObject7).map(canonical);
|
|
11238
11299
|
const parts = [];
|
|
11239
11300
|
for (const uid of own2.filter((uid2) => typed[uid2].type === "control")) {
|
|
11240
|
-
const made = controlToPart(typed[uid], uid, path3,
|
|
11301
|
+
const made = controlToPart(typed[uid], uid, path3, refuse2);
|
|
11241
11302
|
if (made === null) continue;
|
|
11242
11303
|
const param = canonical(made.param);
|
|
11243
11304
|
const taken = params.find((one) => one?.name === param.name);
|
|
11244
11305
|
if (taken !== void 0 && JSON.stringify(taken) !== JSON.stringify(param)) {
|
|
11245
|
-
|
|
11306
|
+
refuse2(
|
|
11246
11307
|
"control_conflict",
|
|
11247
11308
|
`/elements/${escapePointer3(uid)}`,
|
|
11248
11309
|
`control "${made.element.props.id}" declares page param ${JSON.stringify(param)}, and ${path3} already declares "${param.name}" as ${JSON.stringify(taken)}; one name is one declaration \u2014 make the two the same, or give the control its own param`
|
|
@@ -11259,14 +11320,14 @@ function restateDashboard(composed, pages, handlerName, handler, { refuse, note,
|
|
|
11259
11320
|
const handled = /* @__PURE__ */ new Set([header, ...chrome, filters, ...nav === void 0 ? [] : [nav, ...elements[nav].children], ...parts, ...cards]);
|
|
11260
11321
|
const kept = own2.filter((uid) => !handled.has(uid) && !dropped.has(uid) && !["region", "nav", "tab", "control", "widget"].includes(typed[uid].type));
|
|
11261
11322
|
for (const uid of kept) {
|
|
11262
|
-
if (conflicts.has(uid))
|
|
11323
|
+
if (conflicts.has(uid)) refuse2(conflicts.get(uid).code, conflicts.get(uid).pointer, conflicts.get(uid).message);
|
|
11263
11324
|
elements[uid] = keptOf(typed[uid], uid, path3, typed, declared, dropped, kept, cards, sections);
|
|
11264
11325
|
}
|
|
11265
11326
|
for (const uid of cards) {
|
|
11266
11327
|
const held = declared.parent.get(uid) ?? null;
|
|
11267
11328
|
const parent = held !== null && kept.includes(held) ? held : null;
|
|
11268
|
-
if (conflicts.has(uid) && (parent !== null || kept.includes(typed[uid].parent)))
|
|
11269
|
-
elements[uid] = cardOf(typed[uid], uid, path3, handlerName, sections,
|
|
11329
|
+
if (conflicts.has(uid) && (parent !== null || kept.includes(typed[uid].parent))) refuse2(conflicts.get(uid).code, conflicts.get(uid).pointer, conflicts.get(uid).message);
|
|
11330
|
+
elements[uid] = cardOf(typed[uid], uid, path3, handlerName, sections, refuse2, parent);
|
|
11270
11331
|
}
|
|
11271
11332
|
const otherRegions = own2.filter((uid) => typed[uid].type === "region" && !handled.has(uid) && !dropped.has(uid) && !duplicates.has(uid));
|
|
11272
11333
|
for (const uid of otherRegions) elements[uid] = region(uid, typed[uid].role ?? "header", "section", []);
|
|
@@ -11279,7 +11340,7 @@ function restateDashboard(composed, pages, handlerName, handler, { refuse, note,
|
|
|
11279
11340
|
const authored = stated2.findIndex((one) => isPlainObject7(one) && one.path === path3);
|
|
11280
11341
|
const statedRoute = authored === -1 ? null : stated2[authored].route ?? null;
|
|
11281
11342
|
if (statedRoute !== null && statedRoute !== route) {
|
|
11282
|
-
|
|
11343
|
+
refuse2(
|
|
11283
11344
|
"route_mismatch",
|
|
11284
11345
|
`/pages/${authored}/route`,
|
|
11285
11346
|
`${path3} states route ${JSON.stringify(statedRoute)}, and app.routes (${JSON.stringify(Array.isArray(app.routes) ? app.routes : [])}) ${route === null ? "serves it at no route" : `serves it at ${JSON.stringify(route)}`}; the manifest serves what app.routes lists \u2014 list the route there, or drop it from the page`
|
|
@@ -11295,11 +11356,11 @@ function restateDashboard(composed, pages, handlerName, handler, { refuse, note,
|
|
|
11295
11356
|
data: dataOf(composed, handler)
|
|
11296
11357
|
};
|
|
11297
11358
|
}
|
|
11298
|
-
function cardOf(element, uid, path3, handlerName, sections,
|
|
11359
|
+
function cardOf(element, uid, path3, handlerName, sections, refuse2, parent = null) {
|
|
11299
11360
|
const props = isPlainObject7(element.props) ? element.props : {};
|
|
11300
11361
|
const spec = named5(element.id) ? element.id : named5(props.card) ? props.card : named5(props.id) ? props.id : null;
|
|
11301
11362
|
if (spec === null) {
|
|
11302
|
-
|
|
11363
|
+
refuse2(
|
|
11303
11364
|
"card_unnamed",
|
|
11304
11365
|
`/elements/${escapePointer3(uid)}`,
|
|
11305
11366
|
`widget "${uid}" names no SPEC card; a dashboard card is bound to a row of its handler's SPEC by its id \u2014 the element's id, or props.card`
|
|
@@ -11334,12 +11395,12 @@ function cardOf(element, uid, path3, handlerName, sections, refuse, parent = nul
|
|
|
11334
11395
|
...section === void 0 ? {} : { section }
|
|
11335
11396
|
};
|
|
11336
11397
|
}
|
|
11337
|
-
function controlToPart(control, uid, path3,
|
|
11398
|
+
function controlToPart(control, uid, path3, refuse2) {
|
|
11338
11399
|
const props = isPlainObject7(control.props) ? control.props : {};
|
|
11339
11400
|
const name = named5(props.param) ? props.param : named5(control.binding) ? control.binding : null;
|
|
11340
11401
|
const id = named5(control.id) ? control.id : named5(props.id) ? props.id : name;
|
|
11341
11402
|
if (name === null) {
|
|
11342
|
-
|
|
11403
|
+
refuse2("control_unsupported", `/elements/${escapePointer3(uid)}`, `control "${id ?? uid}" names no param; a dashboard control parameterises its widgets by the param it writes (props.param)`);
|
|
11343
11404
|
return null;
|
|
11344
11405
|
}
|
|
11345
11406
|
const kind = named5(props.control_kind) ? props.control_kind : props.type === "range" ? "slider" : props.type === "number" ? "number" : props.type === "checkbox" ? "toggle" : null;
|
|
@@ -11374,7 +11435,7 @@ function controlToPart(control, uid, path3, refuse) {
|
|
|
11374
11435
|
if (value === void 0 || value === null || value === "") continue;
|
|
11375
11436
|
const n = typeof value === "number" ? value : Number(value);
|
|
11376
11437
|
if (!Number.isFinite(n)) {
|
|
11377
|
-
|
|
11438
|
+
refuse2("control_unsupported", `/elements/${escapePointer3(uid)}/props/${key}`, `control "${id}" states ${key} ${JSON.stringify(value)}, which is not a number; a ${kind} page param takes numbers`);
|
|
11378
11439
|
return null;
|
|
11379
11440
|
}
|
|
11380
11441
|
numbers[as] = n;
|
|
@@ -11386,7 +11447,7 @@ function controlToPart(control, uid, path3, refuse) {
|
|
|
11386
11447
|
if (kind === "toggle") {
|
|
11387
11448
|
return part("toggle", { name, kind: "toggle", ...label, default: props.checked !== void 0 && props.checked !== false && props.checked !== "false" });
|
|
11388
11449
|
}
|
|
11389
|
-
|
|
11450
|
+
refuse2(
|
|
11390
11451
|
"control_unsupported",
|
|
11391
11452
|
`/elements/${escapePointer3(uid)}`,
|
|
11392
11453
|
`control "${id}" is ${kind === null ? "of no kind this compiler knows" : `a ${kind}`}; on a dashboard the app runtime renders a control is a part over a page param, and the parts here are a slider (a range), a number input and a toggle (a checkbox)`
|
|
@@ -11889,6 +11950,234 @@ function formulaProblems(text3, operands) {
|
|
|
11889
11950
|
return problems.length > 0 ? { tree: null, problems } : { tree, problems: [] };
|
|
11890
11951
|
}
|
|
11891
11952
|
|
|
11953
|
+
// ../flows/insights-builder/kit/contract/metricFormula.mjs
|
|
11954
|
+
var SHIFT_UNITS = Object.freeze(["year", "quarter", "month", "day", "consumption_period"]);
|
|
11955
|
+
var AS_OF_ANCHORS = Object.freeze(["year_end", "quarter_end", "month_end"]);
|
|
11956
|
+
var WINDOW_FUNCTIONS = Object.freeze(["avg", "min", "max"]);
|
|
11957
|
+
var WINDOW_UNITS = Object.freeze(["year", "quarter", "month"]);
|
|
11958
|
+
var MAX_WINDOW_PERIODS = 120;
|
|
11959
|
+
var SYSTEM_VARIABLES = Object.freeze(["DAYS_IN_PERIOD", "PERIODS_IN_YEAR"]);
|
|
11960
|
+
var MAX_FORMULA_LENGTH = 5e3;
|
|
11961
|
+
var MAX_SHIFT_OFFSET_MAGNITUDE = 1200;
|
|
11962
|
+
var METRIC_KEY_RE = /^[a-z][a-z0-9_]*$/;
|
|
11963
|
+
var OPERATOR_RE = /\s*([+\-*/])\s*/g;
|
|
11964
|
+
var SENTINEL = "\uE000";
|
|
11965
|
+
var STASH_RE = /(\d+)/g;
|
|
11966
|
+
var REF_WITH_MOD_RE = /\[([^[\]]+)\](\.(?:SHIFT|AS_OF)\([^)]*\))?/g;
|
|
11967
|
+
var WINDOW_RE = new RegExp(String.raw`\b(${WINDOW_FUNCTIONS.join("|")})\(\s*\[([^[\]]+)\]\s*,\s*(\w+)\s*,\s*([+-]?\d+)\s*\)`, "g");
|
|
11968
|
+
var SYSTEM_VARIABLE_RE = new RegExp(`\\b(?:${SYSTEM_VARIABLES.join("|")})\\b`, "g");
|
|
11969
|
+
var SHIFT_RE = /^\.SHIFT\(\s*(\w+)\s*,\s*([+-]?\d+)\s*\)$/;
|
|
11970
|
+
var AS_OF_RE = /^\.AS_OF\(\s*(\w+)\s*\)$/;
|
|
11971
|
+
var MetricFormulaError = class extends Error {
|
|
11972
|
+
};
|
|
11973
|
+
function refuse(sentence2) {
|
|
11974
|
+
throw new MetricFormulaError(sentence2);
|
|
11975
|
+
}
|
|
11976
|
+
function modifierOnSystemVariable(token) {
|
|
11977
|
+
return `cannot attach a time modifier to the reserved token "${token}" (resolved at runtime, not a metric); drop the modifier or move it to a [@key] operand`;
|
|
11978
|
+
}
|
|
11979
|
+
function timeModifierProblem(mod) {
|
|
11980
|
+
const shift = SHIFT_RE.exec(mod);
|
|
11981
|
+
if (shift) {
|
|
11982
|
+
const unit = shift[1].toLowerCase();
|
|
11983
|
+
const offset = Number(shift[2]);
|
|
11984
|
+
if (!SHIFT_UNITS.includes(unit)) return `has an invalid SHIFT unit "${shift[1]}" (valid: ${SHIFT_UNITS.join(", ")})`;
|
|
11985
|
+
if (offset > 0) return `has a future SHIFT offset ${shift[2]}; the offset must be negative or zero (e.g. SHIFT(${unit}, -1))`;
|
|
11986
|
+
if (Math.abs(offset) > MAX_SHIFT_OFFSET_MAGNITUDE) {
|
|
11987
|
+
return `has a SHIFT offset ${shift[2]} whose magnitude exceeds the server maximum of ${MAX_SHIFT_OFFSET_MAGNITUDE}; use a smaller offset`;
|
|
11988
|
+
}
|
|
11989
|
+
return null;
|
|
11990
|
+
}
|
|
11991
|
+
const asOf = AS_OF_RE.exec(mod);
|
|
11992
|
+
if (asOf) {
|
|
11993
|
+
if (!AS_OF_ANCHORS.includes(asOf[1].toLowerCase())) return `has an invalid AS_OF anchor "${asOf[1]}" (valid: ${AS_OF_ANCHORS.join(", ")})`;
|
|
11994
|
+
return null;
|
|
11995
|
+
}
|
|
11996
|
+
return `has a malformed time modifier "${mod}" (expected .SHIFT(<unit>, <offset>) or .AS_OF(<anchor>))`;
|
|
11997
|
+
}
|
|
11998
|
+
function checkModifier(mod) {
|
|
11999
|
+
const problem5 = timeModifierProblem(mod);
|
|
12000
|
+
if (problem5 !== null) refuse(problem5);
|
|
12001
|
+
}
|
|
12002
|
+
function namedItself(spelled) {
|
|
12003
|
+
return `names the metric ${spelled} itself; an operand is [@key], a key data.metrics declares, so the contract names no company's metric \u2014 declare it there (with "reuse": "name" and "create": false to bind the company's own) and write [@<its key>]`;
|
|
12004
|
+
}
|
|
12005
|
+
function declaredKey(inner, declared) {
|
|
12006
|
+
if (!inner.startsWith("@")) refuse(namedItself(`[${inner}]`));
|
|
12007
|
+
const key = inner.slice(1);
|
|
12008
|
+
if (!METRIC_KEY_RE.test(key)) refuse(`writes [${inner}], and ${JSON.stringify(key)} is not a key: a key is a lowercase letter, then lowercase letters, digits or _`);
|
|
12009
|
+
if (!declared.includes(key)) {
|
|
12010
|
+
refuse(`reads [@${key}], which data.metrics does not declare (it declares ${declared.length > 0 ? declared.map((one) => `[@${one}]`).join(", ") : "none"})`);
|
|
12011
|
+
}
|
|
12012
|
+
return key;
|
|
12013
|
+
}
|
|
12014
|
+
function spelledEntry(entry) {
|
|
12015
|
+
if (entry.kind === "token") return entry.token;
|
|
12016
|
+
if (entry.kind === "window") return `${entry.fn}([@${entry.key}], ${entry.unit}, ${entry.periods})`;
|
|
12017
|
+
return `[@${entry.key}]${entry.mod}`;
|
|
12018
|
+
}
|
|
12019
|
+
function arithmeticProblem(skeleton, stash) {
|
|
12020
|
+
const tokens = [];
|
|
12021
|
+
for (let at = 0; at < skeleton.length; ) {
|
|
12022
|
+
const c = skeleton[at];
|
|
12023
|
+
if (/\s/.test(c)) {
|
|
12024
|
+
at += 1;
|
|
12025
|
+
} else if (c === SENTINEL) {
|
|
12026
|
+
const close = skeleton.indexOf(SENTINEL, at + 1);
|
|
12027
|
+
tokens.push({ kind: "value", text: spelledEntry(stash[Number(skeleton.slice(at + 1, close))]) });
|
|
12028
|
+
at = close + 1;
|
|
12029
|
+
} else if ("+-*/()".includes(c)) {
|
|
12030
|
+
tokens.push({ kind: c === "(" || c === ")" ? c : "op", text: c });
|
|
12031
|
+
at += 1;
|
|
12032
|
+
} else {
|
|
12033
|
+
const start = at;
|
|
12034
|
+
while (at < skeleton.length && /[0-9.]/.test(skeleton[at])) at += 1;
|
|
12035
|
+
const spelled = skeleton.slice(start, at);
|
|
12036
|
+
if (!/^(?:\d+\.?\d*|\.\d+)$/.test(spelled)) return `"${spelled || c}" is not a number`;
|
|
12037
|
+
tokens.push({ kind: "value", text: spelled });
|
|
12038
|
+
}
|
|
12039
|
+
}
|
|
12040
|
+
let expecting = "operand";
|
|
12041
|
+
let depth = 0;
|
|
12042
|
+
for (let at = 0; at <= tokens.length; at += 1) {
|
|
12043
|
+
const found = tokens[at];
|
|
12044
|
+
const before = tokens[at - 1];
|
|
12045
|
+
if (expecting === "operand") {
|
|
12046
|
+
if (found?.kind === "value") expecting = "operator";
|
|
12047
|
+
else if (found?.kind === "(") depth += 1;
|
|
12048
|
+
else if (found?.kind === "op" && found.text === "-") continue;
|
|
12049
|
+
else if (found?.kind === "op") return before?.kind === "op" ? `two operators in a row ("${before.text} ${found.text}")` : `an operand is missing before "${found.text}"`;
|
|
12050
|
+
else if (before?.kind === "(") return 'it has empty parentheses "()"';
|
|
12051
|
+
else if (before !== void 0) return `an operand is missing after "${before.text}"`;
|
|
12052
|
+
else return "it has no operand";
|
|
12053
|
+
} else if (found === void 0) {
|
|
12054
|
+
return depth > 0 ? 'a "(" is never closed' : null;
|
|
12055
|
+
} else if (found.kind === "op") {
|
|
12056
|
+
expecting = "operand";
|
|
12057
|
+
} else if (found.kind === ")") {
|
|
12058
|
+
if (depth === 0) return 'a ")" has no "(" before it';
|
|
12059
|
+
depth -= 1;
|
|
12060
|
+
} else {
|
|
12061
|
+
return `no operator stands between "${before.text}" and "${found.text}"`;
|
|
12062
|
+
}
|
|
12063
|
+
}
|
|
12064
|
+
return null;
|
|
12065
|
+
}
|
|
12066
|
+
function scan(formula, declared) {
|
|
12067
|
+
if (typeof formula !== "string") refuse(`is ${JSON.stringify(formula ?? null)}, not text`);
|
|
12068
|
+
if (formula.trim() === "") refuse("must be non-empty");
|
|
12069
|
+
if (formula.length > MAX_FORMULA_LENGTH) {
|
|
12070
|
+
refuse(`is ${formula.length} characters, over the server maximum of ${MAX_FORMULA_LENGTH}; shorten the formula`);
|
|
12071
|
+
}
|
|
12072
|
+
if (formula.includes(SENTINEL)) refuse("contains an unsupported character (U+E000)");
|
|
12073
|
+
if (formula.includes("|")) refuse("pins a scenario (|); a scenario pin is not accepted in a declared metric's formula \u2014 read the scenario on the source instead (from.financeos.scenario)");
|
|
12074
|
+
if (formula.includes("'")) {
|
|
12075
|
+
const quoted3 = /'[^']*'/.exec(formula);
|
|
12076
|
+
if (quoted3) refuse(namedItself(quoted3[0]));
|
|
12077
|
+
refuse("holds an apostrophe ('), which Datarails reads as the edge of a quoted metric name; an operand is [@key], and a metric name has no apostrophe");
|
|
12078
|
+
}
|
|
12079
|
+
const stash = [];
|
|
12080
|
+
const put = (entry) => {
|
|
12081
|
+
stash.push(entry);
|
|
12082
|
+
return `${SENTINEL}${stash.length - 1}${SENTINEL}`;
|
|
12083
|
+
};
|
|
12084
|
+
let working = formula.replace(WINDOW_RE, (_m, fn, inner, unit, periods) => {
|
|
12085
|
+
if (SYSTEM_VARIABLES.includes(inner)) refuse(`has a window over the reserved token "${inner}"; a window function reads a [@key] operand`);
|
|
12086
|
+
const key = declaredKey(inner, declared);
|
|
12087
|
+
if (!WINDOW_UNITS.includes(unit.toLowerCase())) refuse(`has an invalid window unit "${unit}" in ${fn}() (valid: ${WINDOW_UNITS.join(", ")})`);
|
|
12088
|
+
const count = Number(periods);
|
|
12089
|
+
if (!Number.isInteger(count) || count < 1 || count > MAX_WINDOW_PERIODS) {
|
|
12090
|
+
refuse(`has a window of ${periods} periods in ${fn}(); a window spans 1 to ${MAX_WINDOW_PERIODS} periods`);
|
|
12091
|
+
}
|
|
12092
|
+
return put({ kind: "window", fn, key, unit: unit.toLowerCase(), periods: String(count) });
|
|
12093
|
+
});
|
|
12094
|
+
working = working.replace(REF_WITH_MOD_RE, (_m, inner, mod) => {
|
|
12095
|
+
if (SYSTEM_VARIABLES.includes(inner)) {
|
|
12096
|
+
if (mod) refuse(modifierOnSystemVariable(inner));
|
|
12097
|
+
return put({ kind: "token", token: inner });
|
|
12098
|
+
}
|
|
12099
|
+
const key = declaredKey(inner, declared);
|
|
12100
|
+
if (mod) checkModifier(mod);
|
|
12101
|
+
return put({ kind: "ref", key, mod: mod ?? "" });
|
|
12102
|
+
});
|
|
12103
|
+
working = working.replace(SYSTEM_VARIABLE_RE, (token, offset, whole) => {
|
|
12104
|
+
if (/^\s*\.(?:SHIFT|AS_OF)\(/i.test(whole.slice(offset + token.length))) refuse(modifierOnSystemVariable(token));
|
|
12105
|
+
return put({ kind: "token", token });
|
|
12106
|
+
});
|
|
12107
|
+
if (working.includes("[") || working.includes("]")) refuse("has unbalanced bracket(s); each operand must be wrapped as [@key]");
|
|
12108
|
+
const unclosed = /\.(?:SHIFT|AS_OF)\([^)]*$/i.exec(working);
|
|
12109
|
+
if (unclosed) checkModifier(unclosed[0]);
|
|
12110
|
+
if (/\.(?:SHIFT|AS_OF)\(/i.test(working)) {
|
|
12111
|
+
refuse("time modifiers must use uppercase .SHIFT/.AS_OF, must be attached to a [@key] operand (e.g. [@revenue].SHIFT(year, -1)), and cannot be chained on the same operand");
|
|
12112
|
+
}
|
|
12113
|
+
const window = new RegExp(String.raw`\b(${WINDOW_FUNCTIONS.join("|")})\s*\(`, "i").exec(working);
|
|
12114
|
+
if (window) {
|
|
12115
|
+
refuse(
|
|
12116
|
+
`has a window function it cannot read (${window[1]}(...)); write it as ${window[1].toLowerCase()}([@key], unit, periods) \u2014 ${WINDOW_FUNCTIONS.join(", ")}, in lowercase, over one [@key] with no modifier \u2014 with unit ${WINDOW_UNITS.join(", ")} and periods from 1 to ${MAX_WINDOW_PERIODS}`
|
|
12117
|
+
);
|
|
12118
|
+
}
|
|
12119
|
+
let depth = 0;
|
|
12120
|
+
for (const ch of working) {
|
|
12121
|
+
if (ch === "(") depth += 1;
|
|
12122
|
+
else if (ch === ")" && --depth < 0) break;
|
|
12123
|
+
}
|
|
12124
|
+
if (depth !== 0) refuse('grouping is not balanced; every "(" must have a matching ")"');
|
|
12125
|
+
const padded = working.replace(OPERATOR_RE, " $1 ").trim();
|
|
12126
|
+
const residue = padded.replace(STASH_RE, "").replace(/[\s+\-*/()0-9.]/g, "");
|
|
12127
|
+
if (residue.includes("@")) refuse(`writes @ outside brackets (offending characters: "${residue}"); an operand is [@key]`);
|
|
12128
|
+
if (residue.length > 0) {
|
|
12129
|
+
refuse(`has unbracketed identifier(s) (offending characters: "${residue}"); an operand is [@key], or ${SYSTEM_VARIABLES.join(" or ")} written bare`);
|
|
12130
|
+
}
|
|
12131
|
+
const malformed = arithmeticProblem(padded, stash);
|
|
12132
|
+
if (malformed !== null) refuse(`is not a formula Datarails can evaluate: ${malformed}`);
|
|
12133
|
+
if (!stash.some((entry) => entry.kind !== "token")) refuse("must read at least one declared metric, e.g. [@revenue]");
|
|
12134
|
+
return { padded, stash };
|
|
12135
|
+
}
|
|
12136
|
+
function metricFormulaProblems(formula, declared) {
|
|
12137
|
+
try {
|
|
12138
|
+
const { stash } = scan(formula, Array.isArray(declared) ? declared.filter((one) => typeof one === "string") : []);
|
|
12139
|
+
const operands = [...new Set(stash.filter((entry) => entry.kind !== "token").map((entry) => entry.key))].sort();
|
|
12140
|
+
const windows = stash.filter((entry) => entry.kind === "window").map((entry) => `${entry.fn}([@${entry.key}], ${entry.unit}, ${entry.periods})`);
|
|
12141
|
+
return { problems: [], operands, windows };
|
|
12142
|
+
} catch (error) {
|
|
12143
|
+
if (!(error instanceof MetricFormulaError)) throw error;
|
|
12144
|
+
return { problems: [error.message], operands: [], windows: [] };
|
|
12145
|
+
}
|
|
12146
|
+
}
|
|
12147
|
+
|
|
12148
|
+
// ../flows/insights-builder/kit/contract/metricFormats.mjs
|
|
12149
|
+
var CURRENCY_PREFIXES = ['"$"', "\u20AC", "\xA3", "\u20AA"];
|
|
12150
|
+
var DIGITS = ["#,###", "####"];
|
|
12151
|
+
var DECIMALS = ["", ".#", ".##", ".###", ".####"];
|
|
12152
|
+
var SCALES = ["", "[kilo]k", "[mega]M"];
|
|
12153
|
+
var METRIC_FORMATS = Object.freeze(
|
|
12154
|
+
(() => {
|
|
12155
|
+
const signs = [{ prefix: "", suffix: "" }, ...CURRENCY_PREFIXES.map((prefix) => ({ prefix, suffix: "" })), { prefix: "", suffix: "%" }];
|
|
12156
|
+
const all = [];
|
|
12157
|
+
for (const { prefix, suffix } of signs) {
|
|
12158
|
+
for (const digits of DIGITS) {
|
|
12159
|
+
for (const decimals of DECIMALS) {
|
|
12160
|
+
for (const scale of SCALES) all.push(`${prefix}${digits}${decimals}${scale}${suffix}`);
|
|
12161
|
+
}
|
|
12162
|
+
}
|
|
12163
|
+
}
|
|
12164
|
+
return all;
|
|
12165
|
+
})()
|
|
12166
|
+
);
|
|
12167
|
+
var KNOWN = new Set(METRIC_FORMATS);
|
|
12168
|
+
function isMetricFormat(format) {
|
|
12169
|
+
return typeof format === "string" && KNOWN.has(format);
|
|
12170
|
+
}
|
|
12171
|
+
function metricFormatProblem(format) {
|
|
12172
|
+
if (isMetricFormat(format)) return null;
|
|
12173
|
+
if (typeof format !== "string") return `is ${JSON.stringify(format ?? null)}; a format is one of the strings the metric editor writes, such as "#,###", "\\"$\\"#,###.##" or "####.#%"`;
|
|
12174
|
+
const trimmed = format.trim();
|
|
12175
|
+
const quoted3 = trimmed.startsWith("$") ? `"$"${trimmed.slice(1)}` : trimmed;
|
|
12176
|
+
const hashed = quoted3.replace(/0/g, "#");
|
|
12177
|
+
const meant = [quoted3, hashed].find((one) => one !== format && KNOWN.has(one));
|
|
12178
|
+
return `is ${JSON.stringify(format)}, which the metric editor never writes${meant ? ` (did you mean ${JSON.stringify(meant)}?)` : ""}; build it from these parts, in this order: an optional currency prefix ("$" quoted, or \u20AC \xA3 \u20AA, never with %), #,### or ####, optional decimals .# to .####, an optional scale [kilo]k or [mega]M, and an optional % suffix`;
|
|
12179
|
+
}
|
|
12180
|
+
|
|
11892
12181
|
// ../flows/insights-builder/kit/contract/compileApp.mjs
|
|
11893
12182
|
var RUNTIME = "dr-app-runtime@1.11.0";
|
|
11894
12183
|
var UI_KIT2 = "ui-kit/v4";
|
|
@@ -12178,6 +12467,9 @@ function originOf(row) {
|
|
|
12178
12467
|
function sourcesOf(contract) {
|
|
12179
12468
|
return isPlainObject8(contract?.data?.sources) ? contract.data.sources : {};
|
|
12180
12469
|
}
|
|
12470
|
+
function readsMetric(read) {
|
|
12471
|
+
return isPlainObject8(read) && (read.metric_id !== void 0 && read.metric_id !== null || read.metric !== void 0 && read.metric !== null);
|
|
12472
|
+
}
|
|
12181
12473
|
var FINANCEOS_SCOPES = {
|
|
12182
12474
|
commands: {
|
|
12183
12475
|
"status:read": ["status", "whoami"],
|
|
@@ -12222,7 +12514,7 @@ function financeosNeeds(row, origin) {
|
|
|
12222
12514
|
}
|
|
12223
12515
|
const read = isPlainObject8(row.from.financeos) ? row.from.financeos : {};
|
|
12224
12516
|
const filterable = Array.isArray(row.filterable) && row.filterable.length > 0;
|
|
12225
|
-
if (read
|
|
12517
|
+
if (readsMetric(read)) {
|
|
12226
12518
|
if (read.drill_rows) need("data_models", scopesGranting("namespaces", "data_models"));
|
|
12227
12519
|
else need("metric_series", scopesGranting("commands", "metric_series"));
|
|
12228
12520
|
if (filterable) need("data_models", scopesGranting("namespaces", "data_models"), true);
|
|
@@ -12271,7 +12563,7 @@ function opensRows(row) {
|
|
|
12271
12563
|
if (row.transform !== void 0 && row.transform !== null) return false;
|
|
12272
12564
|
if (!MENU_SHAPES.includes(row.shape ?? "rows")) return false;
|
|
12273
12565
|
const read = isPlainObject8(row.from.financeos) ? row.from.financeos : {};
|
|
12274
|
-
if (read
|
|
12566
|
+
if (readsMetric(read)) return false;
|
|
12275
12567
|
return Boolean(read.table_alias) || read.table_id !== void 0 && read.table_id !== null || read.address === "id" || read.address === "alias";
|
|
12276
12568
|
}
|
|
12277
12569
|
function fillMenuScope(app, sources, elements) {
|
|
@@ -12301,6 +12593,34 @@ function servedSources(elements, sources) {
|
|
|
12301
12593
|
}
|
|
12302
12594
|
return [...out].sort(natural3);
|
|
12303
12595
|
}
|
|
12596
|
+
function ownedReaders(pages, extra) {
|
|
12597
|
+
const readers = {};
|
|
12598
|
+
const known = /* @__PURE__ */ new Set();
|
|
12599
|
+
for (const page of Array.isArray(pages) ? pages : []) {
|
|
12600
|
+
if (page?.owned !== true || !named5(page.path)) continue;
|
|
12601
|
+
const text3 = extra?.[page.path];
|
|
12602
|
+
if (typeof text3 !== "string" || !isOwnedPage(text3)) continue;
|
|
12603
|
+
const embedded = embeddedContractOf({ text: text3 });
|
|
12604
|
+
if (embedded === null) continue;
|
|
12605
|
+
known.add(page.path);
|
|
12606
|
+
const elements = isPlainObject8(embedded.elements) ? embedded.elements : {};
|
|
12607
|
+
for (const uid of Object.keys(elements).sort(natural3)) {
|
|
12608
|
+
const element = elements[uid];
|
|
12609
|
+
if (!isPlainObject8(element) || element.page !== page.path) continue;
|
|
12610
|
+
const props = isPlainObject8(element.props) ? element.props : {};
|
|
12611
|
+
const kept = {};
|
|
12612
|
+
if (named5(props.source)) kept.source = props.source;
|
|
12613
|
+
if (Array.isArray(props.declares?.reads)) kept.declares = { reads: props.declares.reads.filter(named5) };
|
|
12614
|
+
if (Object.keys(kept).length > 0) readers[`owned:${uid}`] = { page: page.path, props: kept };
|
|
12615
|
+
}
|
|
12616
|
+
}
|
|
12617
|
+
return { readers, known };
|
|
12618
|
+
}
|
|
12619
|
+
function withOwnedReaders(contract, readers) {
|
|
12620
|
+
if (Object.keys(readers).length === 0) return contract;
|
|
12621
|
+
const elements = isPlainObject8(contract?.elements) ? contract.elements : {};
|
|
12622
|
+
return { ...contract, elements: { ...elements, ...readers } };
|
|
12623
|
+
}
|
|
12304
12624
|
function sourceProblems(sources, app, handlers, extra) {
|
|
12305
12625
|
const problems = [];
|
|
12306
12626
|
const names = Object.keys(sources).sort(natural3);
|
|
@@ -12484,6 +12804,272 @@ function reaches2(sources, start, target, seen, isFormula) {
|
|
|
12484
12804
|
if (!isFormula(row) || !Array.isArray(row.from)) return false;
|
|
12485
12805
|
return row.from.filter(named5).some((one) => reaches2(sources, one, target, seen, isFormula));
|
|
12486
12806
|
}
|
|
12807
|
+
var METRIC_DECL_KEYS = ["name", "kind", "formula", "table", "value", "date", "aggregate", "time_aggregation", "filters", "format", "description", "reuse", "create"];
|
|
12808
|
+
var METRIC_KINDS = ["base", "calc"];
|
|
12809
|
+
var METRIC_CALC_KEYS = ["formula"];
|
|
12810
|
+
var METRIC_BASE_KEYS = ["table", "value", "date", "aggregate", "time_aggregation", "filters"];
|
|
12811
|
+
var METRIC_AGGREGATES = ["sum", "count", "cumulative_sum", "cumulative_count"];
|
|
12812
|
+
var METRIC_TIME_AGGREGATIONS = ["sum", "eop", "avg"];
|
|
12813
|
+
var METRIC_REUSE = ["definition", "name"];
|
|
12814
|
+
var METRIC_TABLE_KEYS = ["alias", "name", "id"];
|
|
12815
|
+
var METRIC_FILTER_KEYS = ["field", "values", "exclude"];
|
|
12816
|
+
var METRIC_NAME_MAX = 128;
|
|
12817
|
+
function metricNameProblem(name) {
|
|
12818
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
12819
|
+
return `is ${JSON.stringify(name ?? null)}; every declaration names its metric in Datarails, as a finance team would ("Revenue MoM growth")`;
|
|
12820
|
+
}
|
|
12821
|
+
if (name.length > METRIC_NAME_MAX) return `is ${name.length} characters; Datarails takes at most ${METRIC_NAME_MAX}`;
|
|
12822
|
+
if (/['[\]]/.test(name)) return `${JSON.stringify(name)} holds ' [ or ], which a CALC formula reads as the edges of a metric reference`;
|
|
12823
|
+
if (name.startsWith("@")) return `${JSON.stringify(name)} starts with @, which marks a key of this contract, not a name in Datarails`;
|
|
12824
|
+
if (SYSTEM_VARIABLES.includes(name)) return `is ${name}, which Datarails reserves for its own system variable`;
|
|
12825
|
+
return null;
|
|
12826
|
+
}
|
|
12827
|
+
function metricTableProblem(table) {
|
|
12828
|
+
if (!isPlainObject8(table)) return true;
|
|
12829
|
+
const stated2 = Object.keys(table);
|
|
12830
|
+
if (stated2.length !== 1 || !METRIC_TABLE_KEYS.includes(stated2[0])) return true;
|
|
12831
|
+
const value = table[stated2[0]];
|
|
12832
|
+
return stated2[0] === "id" ? !(Number.isInteger(value) && value >= 1) : typeof value !== "string" || value.trim() === "";
|
|
12833
|
+
}
|
|
12834
|
+
function metricFilterProblem(filter) {
|
|
12835
|
+
if (!isPlainObject8(filter)) return `is ${JSON.stringify(filter ?? null)}`;
|
|
12836
|
+
const extra = Object.keys(filter).filter((key) => !METRIC_FILTER_KEYS.includes(key)).sort(natural3);
|
|
12837
|
+
if (extra.length > 0) return `states ${extra.join(", ")}, which a filter does not take`;
|
|
12838
|
+
if (typeof filter.field !== "string" || filter.field.trim() === "") return `names field ${JSON.stringify(filter.field ?? null)}, which is no field's alias or name`;
|
|
12839
|
+
if (!Array.isArray(filter.values) || filter.values.length === 0 || !filter.values.every((one) => typeof one === "string" || typeof one === "number" && Number.isFinite(one))) {
|
|
12840
|
+
return `states values ${JSON.stringify(filter.values ?? null)}; values is a non-empty list of the field's values, each a string or a number`;
|
|
12841
|
+
}
|
|
12842
|
+
if (filter.exclude !== void 0 && typeof filter.exclude !== "boolean") return `states exclude ${JSON.stringify(filter.exclude)}; exclude is true or false`;
|
|
12843
|
+
return null;
|
|
12844
|
+
}
|
|
12845
|
+
function metricReaches(operandsOf, start, target, seen) {
|
|
12846
|
+
if (start === target) return true;
|
|
12847
|
+
if (seen.has(start)) return false;
|
|
12848
|
+
seen.add(start);
|
|
12849
|
+
return (operandsOf.get(start) ?? []).some((one) => metricReaches(operandsOf, one, target, seen));
|
|
12850
|
+
}
|
|
12851
|
+
function calcShaped(node) {
|
|
12852
|
+
if (node[0] === "num" || node[0] === "name") return true;
|
|
12853
|
+
if (node[0] === "neg") return calcShaped(node[1]);
|
|
12854
|
+
if (node[0] === "bin") return calcShaped(node[2]) && calcShaped(node[3]);
|
|
12855
|
+
return false;
|
|
12856
|
+
}
|
|
12857
|
+
function metricProblems(metrics, sources) {
|
|
12858
|
+
const problems = [];
|
|
12859
|
+
const at = (key, field) => `/data/metrics/${escapePointer3(key)}${field ? `/${field}` : ""}`;
|
|
12860
|
+
const say = (code, pointer2, message) => problems.push({ code, pointer: pointer2, message });
|
|
12861
|
+
const advise = (code, pointer2, message) => problems.push({ code, pointer: pointer2, message, advisory: true });
|
|
12862
|
+
if (metrics !== void 0 && metrics !== null && !isPlainObject8(metrics)) {
|
|
12863
|
+
say("metric_decl", "/data/metrics", `data.metrics is ${JSON.stringify(metrics)}; it is an object of declarations keyed by a local name ({"revenue": {"name": "Revenue", "kind": "base", ...}})`);
|
|
12864
|
+
}
|
|
12865
|
+
const declared = isPlainObject8(metrics) ? metrics : {};
|
|
12866
|
+
const keys2 = Object.keys(declared).sort(natural3);
|
|
12867
|
+
const declaredKeys = keys2.filter((key) => METRIC_KEY_RE.test(key));
|
|
12868
|
+
const formulas = /* @__PURE__ */ new Map();
|
|
12869
|
+
for (const key of keys2) {
|
|
12870
|
+
const decl = declared[key];
|
|
12871
|
+
if (!METRIC_KEY_RE.test(key)) {
|
|
12872
|
+
say("metric_decl", at(key), `data.metrics declares ${JSON.stringify(key)}, which is not a key: a key is a lowercase letter, then lowercase letters, digits or _ (revenue_mom), the handle a source reads as "@<key>" and a formula as [@<key>]`);
|
|
12873
|
+
continue;
|
|
12874
|
+
}
|
|
12875
|
+
if (!isPlainObject8(decl)) {
|
|
12876
|
+
say("metric_decl", at(key), `metric "${key}" is ${JSON.stringify(decl ?? null)}; a declaration is an object: {"name": "<its name in Datarails>", "kind": "base" | "calc", ...}`);
|
|
12877
|
+
continue;
|
|
12878
|
+
}
|
|
12879
|
+
for (const field of Object.keys(decl).sort(natural3)) {
|
|
12880
|
+
if (!METRIC_DECL_KEYS.includes(field)) say("metric_decl", at(key, field), `metric "${key}" states ${field}, which a declaration does not take; it takes ${either3(METRIC_DECL_KEYS)}`);
|
|
12881
|
+
}
|
|
12882
|
+
const nameProblem = metricNameProblem(decl.name);
|
|
12883
|
+
if (nameProblem !== null) say("metric_decl", at(key, "name"), `metric "${key}"'s name ${nameProblem}`);
|
|
12884
|
+
if (decl.reuse !== void 0 && !METRIC_REUSE.includes(decl.reuse)) {
|
|
12885
|
+
say(
|
|
12886
|
+
"metric_decl",
|
|
12887
|
+
at(key, "reuse"),
|
|
12888
|
+
`metric "${key}" states reuse ${JSON.stringify(decl.reuse)}; reuse is "definition" (the default: bind only a company metric with this definition) or "name" (bind the company's metric of this name, whatever its definition)`
|
|
12889
|
+
);
|
|
12890
|
+
}
|
|
12891
|
+
if (decl.create !== void 0 && typeof decl.create !== "boolean") {
|
|
12892
|
+
say("metric_decl", at(key, "create"), `metric "${key}" states create ${JSON.stringify(decl.create)}; create is true (the default: create the metric where the company has none to bind) or false (bind only)`);
|
|
12893
|
+
}
|
|
12894
|
+
if (decl.description !== void 0 && typeof decl.description !== "string") {
|
|
12895
|
+
say("metric_decl", at(key, "description"), `metric "${key}" states description ${JSON.stringify(decl.description)}; a description is text for the metric's page in Datarails`);
|
|
12896
|
+
}
|
|
12897
|
+
if (decl.format !== void 0) {
|
|
12898
|
+
const formatProblem = metricFormatProblem(decl.format);
|
|
12899
|
+
if (formatProblem !== null) say("metric_format", at(key, "format"), `metric "${key}"'s format ${formatProblem}`);
|
|
12900
|
+
}
|
|
12901
|
+
const kind = decl.kind;
|
|
12902
|
+
if (kind === void 0) {
|
|
12903
|
+
const defining = [...METRIC_CALC_KEYS, ...METRIC_BASE_KEYS].filter((field) => decl[field] !== void 0);
|
|
12904
|
+
if (defining.length > 0) {
|
|
12905
|
+
say("metric_decl", at(key, "kind"), `metric "${key}" states ${defining.join(", ")} and no kind; state "kind": "base" (a table's field, aggregated) or "calc" (a formula over declared metrics)`);
|
|
12906
|
+
} else if (decl.create !== false) {
|
|
12907
|
+
say(
|
|
12908
|
+
"metric_decl",
|
|
12909
|
+
at(key),
|
|
12910
|
+
`metric "${key}" has no definition (no kind), so nothing could create it; bind the company's own metric by name with "reuse": "name" and "create": false, or give it a definition`
|
|
12911
|
+
);
|
|
12912
|
+
} else if (decl.reuse !== "name") {
|
|
12913
|
+
say(
|
|
12914
|
+
"metric_decl",
|
|
12915
|
+
at(key, "reuse"),
|
|
12916
|
+
`metric "${key}" has no definition, so no company metric can match it by definition (reuse "definition", the default); bind the company's own metric by name with "reuse": "name"`
|
|
12917
|
+
);
|
|
12918
|
+
}
|
|
12919
|
+
continue;
|
|
12920
|
+
}
|
|
12921
|
+
if (kind === "driver") {
|
|
12922
|
+
say(
|
|
12923
|
+
"metric_kind_later",
|
|
12924
|
+
at(key, "kind"),
|
|
12925
|
+
`metric "${key}" is kind "driver", which is reserved: driver declarations are not compiled yet. Declare a base or calc metric, or create the driver in Datarails and bind it with {"name": "<its name>", "reuse": "name", "create": false}`
|
|
12926
|
+
);
|
|
12927
|
+
continue;
|
|
12928
|
+
}
|
|
12929
|
+
if (!METRIC_KINDS.includes(kind)) {
|
|
12930
|
+
say("metric_decl", at(key, "kind"), `metric "${key}" is kind ${JSON.stringify(kind)}; a declared metric is "base" (a table's field, aggregated) or "calc" (a formula over declared metrics)`);
|
|
12931
|
+
continue;
|
|
12932
|
+
}
|
|
12933
|
+
for (const field of kind === "calc" ? METRIC_BASE_KEYS : METRIC_CALC_KEYS) {
|
|
12934
|
+
if (decl[field] !== void 0) say("metric_decl", at(key, field), `metric "${key}" is a ${kind} metric and states ${field}, which only a ${kind === "calc" ? "base" : "calc"} metric takes`);
|
|
12935
|
+
}
|
|
12936
|
+
if (kind === "calc") {
|
|
12937
|
+
if (decl.formula === void 0) {
|
|
12938
|
+
say("metric_decl", at(key, "formula"), `metric "${key}" is a calc metric with no formula; state one over declared metrics, each written [@<key>] (([@revenue] - [@cogs]) / [@revenue])`);
|
|
12939
|
+
} else {
|
|
12940
|
+
formulas.set(key, decl.formula);
|
|
12941
|
+
}
|
|
12942
|
+
continue;
|
|
12943
|
+
}
|
|
12944
|
+
if (decl.table === void 0) {
|
|
12945
|
+
say("metric_decl", at(key, "table"), `metric "${key}" is a base metric with no table; state {"alias": "<table alias>"} or {"name": "<table name>"}`);
|
|
12946
|
+
} else if (metricTableProblem(decl.table)) {
|
|
12947
|
+
say(
|
|
12948
|
+
"metric_decl",
|
|
12949
|
+
at(key, "table"),
|
|
12950
|
+
`metric "${key}" states table ${JSON.stringify(decl.table)}; a table is exactly one of {"alias": "<table alias>"}, {"name": "<table name>"} or {"id": <id>} (which ties the app to one company)`
|
|
12951
|
+
);
|
|
12952
|
+
}
|
|
12953
|
+
for (const [field, what] of [["value", "the field it aggregates"], ["date", "the field its periods come from"]]) {
|
|
12954
|
+
if (decl[field] === void 0) say("metric_decl", at(key, field), `metric "${key}" is a base metric with no ${field}; state ${what}, by alias or name`);
|
|
12955
|
+
else if (typeof decl[field] !== "string" || decl[field].trim() === "") say("metric_decl", at(key, field), `metric "${key}" states ${field} ${JSON.stringify(decl[field])}; ${field} is ${what}, by alias or name`);
|
|
12956
|
+
}
|
|
12957
|
+
if (decl.aggregate === void 0) {
|
|
12958
|
+
say("metric_decl", at(key, "aggregate"), `metric "${key}" is a base metric with no aggregate; state ${either3(METRIC_AGGREGATES)}`);
|
|
12959
|
+
} else if (!METRIC_AGGREGATES.includes(decl.aggregate)) {
|
|
12960
|
+
say("metric_base", at(key, "aggregate"), `metric "${key}" aggregates ${JSON.stringify(decl.aggregate)}; a metric aggregates ${either3(METRIC_AGGREGATES)} (avg, min and max are not metric aggregates)`);
|
|
12961
|
+
}
|
|
12962
|
+
if (decl.time_aggregation !== void 0 && !METRIC_TIME_AGGREGATIONS.includes(decl.time_aggregation)) {
|
|
12963
|
+
say("metric_base", at(key, "time_aggregation"), `metric "${key}" rolls up with ${JSON.stringify(decl.time_aggregation)}; time_aggregation is ${either3(METRIC_TIME_AGGREGATIONS)}`);
|
|
12964
|
+
} else if (decl.time_aggregation === "sum" && (decl.aggregate === "cumulative_sum" || decl.aggregate === "cumulative_count")) {
|
|
12965
|
+
say("metric_base", at(key, "time_aggregation"), `metric "${key}" aggregates ${decl.aggregate} and rolls up with sum, which Datarails refuses for a cumulative aggregate; use eop (the default) or avg`);
|
|
12966
|
+
}
|
|
12967
|
+
if (decl.filters !== void 0) {
|
|
12968
|
+
if (!Array.isArray(decl.filters)) {
|
|
12969
|
+
say("metric_base", at(key, "filters"), `metric "${key}" states filters ${JSON.stringify(decl.filters)}; filters is a list of {"field": "<alias or name>", "values": [...], "exclude": false}`);
|
|
12970
|
+
} else {
|
|
12971
|
+
decl.filters.forEach((filter, index) => {
|
|
12972
|
+
const filterProblem = metricFilterProblem(filter);
|
|
12973
|
+
if (filterProblem !== null) say("metric_base", at(key, `filters/${index}`), `metric "${key}"'s filter ${index} ${filterProblem}; a filter is {"field": "<alias or name>", "values": [...], "exclude": false}`);
|
|
12974
|
+
});
|
|
12975
|
+
}
|
|
12976
|
+
}
|
|
12977
|
+
}
|
|
12978
|
+
const operandsOf = /* @__PURE__ */ new Map();
|
|
12979
|
+
for (const key of [...formulas.keys()].sort(natural3)) {
|
|
12980
|
+
const { problems: wrong, operands, windows } = metricFormulaProblems(formulas.get(key), declaredKeys);
|
|
12981
|
+
for (const one of wrong) say("metric_formula", at(key, "formula"), `metric "${key}"'s formula ${one}`);
|
|
12982
|
+
if (wrong.length > 0) continue;
|
|
12983
|
+
operandsOf.set(key, operands);
|
|
12984
|
+
if (windows.length > 0) {
|
|
12985
|
+
advise(
|
|
12986
|
+
"metric_window",
|
|
12987
|
+
at(key, "formula"),
|
|
12988
|
+
`metric "${key}"'s formula calls ${windows.join(" and ")}, a window function, which Datarails evaluates only where the company has scenario_bound_calculations on, and refuses to create elsewhere`
|
|
12989
|
+
);
|
|
12990
|
+
}
|
|
12991
|
+
}
|
|
12992
|
+
for (const key of [...operandsOf.keys()].sort(natural3)) {
|
|
12993
|
+
for (const dep of operandsOf.get(key)) {
|
|
12994
|
+
if (metricReaches(operandsOf, dep, key, /* @__PURE__ */ new Set())) {
|
|
12995
|
+
say("metric_formula", at(key, "formula"), `metric "${key}"'s formula reads itself${dep === key ? "" : `, through [@${dep}]`}; a calc metric's operands are metrics that exist before it, so none may read it back`);
|
|
12996
|
+
break;
|
|
12997
|
+
}
|
|
12998
|
+
}
|
|
12999
|
+
}
|
|
13000
|
+
const read = /* @__PURE__ */ new Set();
|
|
13001
|
+
for (const name of Object.keys(sources ?? {}).sort(natural3)) {
|
|
13002
|
+
const row = sources[name];
|
|
13003
|
+
if (!isPlainObject8(row) || originOf(row) !== "financeos" || !isPlainObject8(row.from.financeos)) continue;
|
|
13004
|
+
const metric = row.from.financeos.metric;
|
|
13005
|
+
if (metric === void 0 || metric === null) continue;
|
|
13006
|
+
const pointer2 = `/data/sources/${escapePointer3(name)}/from/financeos/metric`;
|
|
13007
|
+
const metricId = row.from.financeos.metric_id;
|
|
13008
|
+
if (metricId !== void 0 && metricId !== null) {
|
|
13009
|
+
say(
|
|
13010
|
+
"source_metric",
|
|
13011
|
+
pointer2,
|
|
13012
|
+
`source "${name}" states metric ${JSON.stringify(metric)} and metric_id ${JSON.stringify(metricId)}; a source reads one metric \u2014 keep metric (a key data.metrics declares, bound in each company at ship) and drop metric_id, which ties the app to one company`
|
|
13013
|
+
);
|
|
13014
|
+
continue;
|
|
13015
|
+
}
|
|
13016
|
+
if (typeof metric !== "string" || !metric.startsWith("@")) {
|
|
13017
|
+
const meant = typeof metric === "string" && Object.prototype.hasOwnProperty.call(declared, metric) ? ` \u2014 write "@${metric}"` : "";
|
|
13018
|
+
say("source_metric", pointer2, `source "${name}" reads metric ${JSON.stringify(metric)}; a metric is named "@<key>", a key data.metrics declares ("@revenue_mom")${meant}`);
|
|
13019
|
+
continue;
|
|
13020
|
+
}
|
|
13021
|
+
const key = metric.slice(1);
|
|
13022
|
+
if (!declaredKeys.includes(key)) {
|
|
13023
|
+
say(
|
|
13024
|
+
"source_metric",
|
|
13025
|
+
pointer2,
|
|
13026
|
+
`source "${name}" reads metric "@${key}", which data.metrics does not declare (it declares ${declaredKeys.length > 0 ? declaredKeys.map((one) => `"@${one}"`).join(", ") : "none"}); declare it there, or read one it declares`
|
|
13027
|
+
);
|
|
13028
|
+
continue;
|
|
13029
|
+
}
|
|
13030
|
+
read.add(key);
|
|
13031
|
+
}
|
|
13032
|
+
if (!problems.some((one) => one.code === "metric_formula")) {
|
|
13033
|
+
const used = /* @__PURE__ */ new Set();
|
|
13034
|
+
const visit = (key) => {
|
|
13035
|
+
if (used.has(key)) return;
|
|
13036
|
+
used.add(key);
|
|
13037
|
+
for (const one of operandsOf.get(key) ?? []) visit(one);
|
|
13038
|
+
};
|
|
13039
|
+
for (const key of read) visit(key);
|
|
13040
|
+
for (const key of declaredKeys) {
|
|
13041
|
+
if (used.has(key)) continue;
|
|
13042
|
+
advise(
|
|
13043
|
+
"metric_unused",
|
|
13044
|
+
at(key),
|
|
13045
|
+
`metric "${key}" is declared and no source reads it, directly or through another declared metric's formula; \`app-space ship\` would still bind or create it in every company the app ships to \u2014 read it from a source (from.financeos.metric "@${key}"), or drop it`
|
|
13046
|
+
);
|
|
13047
|
+
}
|
|
13048
|
+
}
|
|
13049
|
+
for (const name of Object.keys(sources ?? {}).sort(natural3)) {
|
|
13050
|
+
const row = sources[name];
|
|
13051
|
+
if (!isPlainObject8(row) || typeof row.expr !== "string" || !Array.isArray(row.from)) continue;
|
|
13052
|
+
const operands = [...new Set(row.from.filter(named5))];
|
|
13053
|
+
if (operands.length === 0) continue;
|
|
13054
|
+
const reads = operands.map((dep) => Object.hasOwn(sources, dep) ? sources[dep] : void 0);
|
|
13055
|
+
const metricRead = (one) => isPlainObject8(one) && originOf(one) === "financeos" && (one.transform === void 0 || one.transform === null) && readsMetric(one.from.financeos);
|
|
13056
|
+
if (!reads.every(metricRead)) continue;
|
|
13057
|
+
if (new Set(reads.map((one) => JSON.stringify(one.from.financeos.scenario ?? null))).size !== 1) continue;
|
|
13058
|
+
let tree;
|
|
13059
|
+
try {
|
|
13060
|
+
tree = parseFormula(row.expr);
|
|
13061
|
+
} catch {
|
|
13062
|
+
continue;
|
|
13063
|
+
}
|
|
13064
|
+
if (!calcShaped(tree)) continue;
|
|
13065
|
+
advise(
|
|
13066
|
+
"expr_could_be_metric",
|
|
13067
|
+
`/data/sources/${escapePointer3(name)}/expr`,
|
|
13068
|
+
`source "${name}"'s formula is arithmetic over Datarails metrics only (${operands.join(", ")}), in one scenario; declare it in data.metrics as a calc metric (its operands declared there too, each [@<key>]) and read it with from.financeos.metric, so it lives in Datarails, where MCP and DR.METRIC read the same number`
|
|
13069
|
+
);
|
|
13070
|
+
}
|
|
13071
|
+
return problems;
|
|
13072
|
+
}
|
|
12487
13073
|
var SOURCE_ORDERS = ["source", "declared"];
|
|
12488
13074
|
function orderProblems(name, row, at) {
|
|
12489
13075
|
const problems = [];
|
|
@@ -12662,22 +13248,22 @@ function themeProblems(app) {
|
|
|
12662
13248
|
const theme = app?.theme;
|
|
12663
13249
|
if (theme === void 0 || theme === null) return [];
|
|
12664
13250
|
const at = (key) => `/app/theme${key ? `/${key}` : ""}`;
|
|
12665
|
-
const
|
|
12666
|
-
if (!isPlainObject8(theme)) return [
|
|
13251
|
+
const refuse2 = (key, message) => ({ code: "theme_shape", pointer: at(key), message });
|
|
13252
|
+
if (!isPlainObject8(theme)) return [refuse2("", `app.theme is ${JSON.stringify(theme)}; it is {"file": "ui/theme.css", "tokens": {"--dr-\u2026": "\u2026"}}, or null for none`)];
|
|
12667
13253
|
const out = [];
|
|
12668
|
-
if (theme.file !== THEME_FILE4) out.push(
|
|
12669
|
-
if (!isPlainObject8(theme.tokens)) out.push(
|
|
13254
|
+
if (theme.file !== THEME_FILE4) out.push(refuse2("file", `app.theme.file is ${JSON.stringify(theme.file ?? null)}; an app's theme is written to ${THEME_FILE4}, the one file its pages link after runtime.css`));
|
|
13255
|
+
if (!isPlainObject8(theme.tokens)) out.push(refuse2("tokens", `app.theme.tokens is ${JSON.stringify(theme.tokens ?? null)}; it is an object of --dr-* tokens to the values the app overrides them with ({} for none)`));
|
|
12670
13256
|
else {
|
|
12671
13257
|
for (const token of Object.keys(theme.tokens).sort(natural3)) {
|
|
12672
13258
|
const value = theme.tokens[token];
|
|
12673
13259
|
const pointer2 = `tokens/${escapePointer3(token)}`;
|
|
12674
|
-
if (token === "--dr-app-theme") out.push(
|
|
12675
|
-
else if (!TOKEN_RE.test(token)) out.push(
|
|
13260
|
+
if (token === "--dr-app-theme") out.push(refuse2(pointer2, "app.theme.tokens states --dr-app-theme, which is the compiler's own mark on the file it writes; drop it"));
|
|
13261
|
+
else if (!TOKEN_RE.test(token)) out.push(refuse2(pointer2, `app.theme.tokens states ${JSON.stringify(token)}; an app theme overrides --dr-* tokens only (the shadcn triples are computed from them)`));
|
|
12676
13262
|
else {
|
|
12677
13263
|
const why = tokenValueProblem(value);
|
|
12678
13264
|
if (why !== null) {
|
|
12679
13265
|
out.push(
|
|
12680
|
-
|
|
13266
|
+
refuse2(
|
|
12681
13267
|
pointer2,
|
|
12682
13268
|
`app.theme.tokens["${token}"] is ${JSON.stringify(value)}, which ${why}; a token holds one CSS value, with no ; { } /* */ \\ < or url(, its quotes and brackets balanced`
|
|
12683
13269
|
)
|
|
@@ -12687,9 +13273,9 @@ function themeProblems(app) {
|
|
|
12687
13273
|
}
|
|
12688
13274
|
}
|
|
12689
13275
|
if (theme.preset !== void 0 && !THEME_PRESETS.includes(theme.preset)) {
|
|
12690
|
-
out.push(
|
|
13276
|
+
out.push(refuse2("preset", `app.theme.preset is ${JSON.stringify(theme.preset)}; the ui-kit this compiler pins (${UI_KIT2}) carries ${either3(THEME_PRESETS)}${theme.preset === "presentation" ? " \u2014 presentation was planned for ui-kit v4, which shipped without it" : ""}`));
|
|
12691
13277
|
}
|
|
12692
|
-
if (theme.density !== void 0 && !THEME_DENSITIES.includes(theme.density)) out.push(
|
|
13278
|
+
if (theme.density !== void 0 && !THEME_DENSITIES.includes(theme.density)) out.push(refuse2("density", `app.theme.density is ${JSON.stringify(theme.density)}; it is ${either3(THEME_DENSITIES)}`));
|
|
12693
13279
|
return out;
|
|
12694
13280
|
}
|
|
12695
13281
|
function themeValue(token, tokens, seen = /* @__PURE__ */ new Set()) {
|
|
@@ -12887,51 +13473,51 @@ function atPointer(root, pointer2) {
|
|
|
12887
13473
|
function paramValueProblems(param, said2, at) {
|
|
12888
13474
|
const problems = [];
|
|
12889
13475
|
const kind = param.kind;
|
|
12890
|
-
const
|
|
13476
|
+
const refuse2 = (key, why) => problems.push({ code: "param_kind_value", pointer: at(key), message: `${said2} (${kind}) ${why}` });
|
|
12891
13477
|
const finite = (value2) => typeof value2 === "number" && Number.isFinite(value2);
|
|
12892
13478
|
for (const key of ["min", "max", "step"]) {
|
|
12893
13479
|
if (param[key] === void 0) continue;
|
|
12894
|
-
if (!NUMERIC_PARAM_KINDS.has(kind))
|
|
12895
|
-
else if (!finite(param[key]))
|
|
13480
|
+
if (!NUMERIC_PARAM_KINDS.has(kind)) refuse2(key, `states ${key}, which only a slider or a number has`);
|
|
13481
|
+
else if (!finite(param[key])) refuse2(key, `states ${key} as ${JSON.stringify(param[key])}; it has to be a finite number \u2014 ${numberFix(param[key])}`);
|
|
12896
13482
|
}
|
|
12897
13483
|
if (param.values !== void 0) {
|
|
12898
|
-
if (!STRING_PARAM_KINDS.has(kind))
|
|
12899
|
-
else if (!Array.isArray(param.values) || param.values.length === 0 || !param.values.every(named5))
|
|
12900
|
-
else if (new Set(param.values).size !== param.values.length)
|
|
13484
|
+
if (!STRING_PARAM_KINDS.has(kind)) refuse2("values", `states values, which only a select, multi, period or month is held to`);
|
|
13485
|
+
else if (!Array.isArray(param.values) || param.values.length === 0 || !param.values.every(named5)) refuse2("values", `states values as ${JSON.stringify(param.values)}; it is a non-empty list of strings (leave it out to take the values from the source's catalog)`);
|
|
13486
|
+
else if (new Set(param.values).size !== param.values.length) refuse2("values", "lists one value twice");
|
|
12901
13487
|
}
|
|
12902
13488
|
if (param.max_selected !== void 0) {
|
|
12903
|
-
if (kind !== "multi")
|
|
12904
|
-
else if (!Number.isInteger(param.max_selected) || param.max_selected < 1)
|
|
12905
|
-
else if (Array.isArray(param.values) && param.max_selected > param.values.length)
|
|
13489
|
+
if (kind !== "multi") refuse2("max_selected", "states max_selected, which only a multi has");
|
|
13490
|
+
else if (!Number.isInteger(param.max_selected) || param.max_selected < 1) refuse2("max_selected", `states max_selected as ${JSON.stringify(param.max_selected)}; it is a whole number of at least 1`);
|
|
13491
|
+
else if (Array.isArray(param.values) && param.max_selected > param.values.length) refuse2("max_selected", `allows ${param.max_selected} of only ${param.values.length} values`);
|
|
12906
13492
|
}
|
|
12907
13493
|
if (problems.length > 0) return problems;
|
|
12908
13494
|
const { min, max, step } = param;
|
|
12909
13495
|
if (NUMERIC_PARAM_KINDS.has(kind)) {
|
|
12910
|
-
if (finite(min) && finite(max) && min > max)
|
|
12911
|
-
if (finite(step) && step <= 0)
|
|
13496
|
+
if (finite(min) && finite(max) && min > max) refuse2("min", `has min ${min} above max ${max}`);
|
|
13497
|
+
if (finite(step) && step <= 0) refuse2("step", `steps by ${step}; a step is above 0`);
|
|
12912
13498
|
}
|
|
12913
13499
|
if (param.default === void 0) return problems;
|
|
12914
13500
|
const value = param.default;
|
|
12915
13501
|
const values = Array.isArray(param.values) ? param.values : null;
|
|
12916
13502
|
if (NUMERIC_PARAM_KINDS.has(kind)) {
|
|
12917
|
-
if (!finite(value))
|
|
13503
|
+
if (!finite(value)) refuse2("default", `defaults to ${JSON.stringify(value)}; a ${kind} opens on a finite number`);
|
|
12918
13504
|
else if (finite(min) && value < min || finite(max) && value > max) {
|
|
12919
|
-
|
|
13505
|
+
refuse2("default", `defaults to ${value}, outside its own range [${finite(min) ? min : "\u2026"}, ${finite(max) ? max : "\u2026"}] \u2014 the page would open on no value at all`);
|
|
12920
13506
|
}
|
|
12921
13507
|
} else if (kind === "toggle") {
|
|
12922
|
-
if (typeof value !== "boolean")
|
|
13508
|
+
if (typeof value !== "boolean") refuse2("default", `defaults to ${JSON.stringify(value)}; a toggle opens on true or false`);
|
|
12923
13509
|
} else if (kind === "multi") {
|
|
12924
|
-
if (!Array.isArray(value) || !value.every((one) => typeof one === "string"))
|
|
12925
|
-
else if (values !== null && value.some((one) => !values.includes(one)))
|
|
12926
|
-
else if (Number.isInteger(param.max_selected) && value.length > param.max_selected)
|
|
13510
|
+
if (!Array.isArray(value) || !value.every((one) => typeof one === "string")) refuse2("default", `defaults to ${JSON.stringify(value)}; a multi opens on a list of strings`);
|
|
13511
|
+
else if (values !== null && value.some((one) => !values.includes(one))) refuse2("default", `defaults to ${JSON.stringify(value)}, which is not among its values ${JSON.stringify(values)}`);
|
|
13512
|
+
else if (Number.isInteger(param.max_selected) && value.length > param.max_selected) refuse2("default", `opens on ${value.length} values and allows ${param.max_selected}`);
|
|
12927
13513
|
} else if (typeof value !== "string") {
|
|
12928
|
-
|
|
13514
|
+
refuse2("default", `defaults to ${JSON.stringify(value)}; a ${kind} opens on a string`);
|
|
12929
13515
|
} else if (kind === "month" && !MONTH_RE.test(value)) {
|
|
12930
|
-
|
|
13516
|
+
refuse2("default", `defaults to ${JSON.stringify(value)}; a month is written YYYY-MM`);
|
|
12931
13517
|
} else if (kind === "date" && value !== "" && !isRealDay(value)) {
|
|
12932
|
-
|
|
13518
|
+
refuse2("default", `defaults to ${JSON.stringify(value)}; a date is a real day written YYYY-MM-DD, or '' for none`);
|
|
12933
13519
|
} else if (values !== null && !values.includes(value)) {
|
|
12934
|
-
|
|
13520
|
+
refuse2("default", `defaults to ${JSON.stringify(value)}, which is not among its values ${JSON.stringify(values)}`);
|
|
12935
13521
|
}
|
|
12936
13522
|
return problems;
|
|
12937
13523
|
}
|
|
@@ -13064,7 +13650,10 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
|
|
|
13064
13650
|
problems.push(...sourceFindings.filter((one) => !one.advisory));
|
|
13065
13651
|
problems.push(...compareProblems(stated2, model));
|
|
13066
13652
|
problems.push(...exprProblems(stated2));
|
|
13067
|
-
|
|
13653
|
+
const metricFindings = metricProblems(contract?.data?.metrics, stated2);
|
|
13654
|
+
problems.push(...metricFindings.filter((one) => !one.advisory));
|
|
13655
|
+
const owned = ownedReaders(contract.pages, extra);
|
|
13656
|
+
problems.push(...paramProblems(withOwnedReaders(contract, owned.readers), stated2));
|
|
13068
13657
|
problems.push(...themeProblems(app));
|
|
13069
13658
|
problems.push(...shellProblems(contract, typed, handlers, stated2));
|
|
13070
13659
|
const sources = fillFilterable(stated2);
|
|
@@ -13150,7 +13739,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
|
|
|
13150
13739
|
problems.push(...partPinProblems(declared, { "dr-components": appComponents, "dr-components-controls": appControls }));
|
|
13151
13740
|
problems.push(...runtimePinProblems(declared, runtime));
|
|
13152
13741
|
const widgetFindings = tree.problems.length === 0 ? widgetProblems(declared, tree, handlers, contract?.data?.sources, appWidgets) : [];
|
|
13153
|
-
const warnings = [...sourceFindings, ...widgetFindings].filter((one) => one.advisory).map(({ advisory: _advisory, ...one }) => one);
|
|
13742
|
+
const warnings = [...sourceFindings, ...metricFindings, ...widgetFindings].filter((one) => one.advisory).map(({ advisory: _advisory, ...one }) => one);
|
|
13154
13743
|
problems.push(...widgetFindings.filter((one) => !one.advisory));
|
|
13155
13744
|
if (tree.problems.length === 0) problems.push(...partProblems(declared, tree, handlers, sources, contract?.pages, model, { "dr-components": appComponents }));
|
|
13156
13745
|
const backendFacts = inferredBackendHandlers(contract?.backend?.handlers, handlers, extra);
|
|
@@ -13183,10 +13772,10 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
|
|
|
13183
13772
|
problems.push(...placementProblems(elements, pages, tree, order2));
|
|
13184
13773
|
if (problems.length > 0) return { files: {}, problems, contract, warnings };
|
|
13185
13774
|
const whole = restate(contract, elements, pages, typed, order2);
|
|
13186
|
-
const served = servedSources(elements, sources);
|
|
13775
|
+
const served = servedSources({ ...elements, ...owned.readers }, sources);
|
|
13187
13776
|
const transforms = transformTexts2(sources, extra);
|
|
13188
13777
|
const files = {};
|
|
13189
|
-
files["manifest.json"] = json(manifestOf(contract, manifestApp, handlers, names, model, served));
|
|
13778
|
+
files["manifest.json"] = json(manifestOf(contract, manifestApp, handlers, names, model, served, withOwnedReaders(contract, owned.readers), owned.known));
|
|
13190
13779
|
if (app.data_model) files["data_model.json"] = json(dataModelOf(app.data_model));
|
|
13191
13780
|
for (const name of names) {
|
|
13192
13781
|
const path3 = `handlers/${name}.py`;
|
|
@@ -13347,51 +13936,51 @@ function shellProblems(contract, elements, handlers, sources) {
|
|
|
13347
13936
|
const pages = Array.isArray(contract?.pages) ? contract.pages : [];
|
|
13348
13937
|
const paths = new Set(pages.map((page) => page?.path).filter(named5));
|
|
13349
13938
|
const params = new Set(pages.flatMap((page) => (Array.isArray(page?.params) ? page.params : []).map((param) => param?.name)).filter(named5));
|
|
13350
|
-
const
|
|
13939
|
+
const refuse2 = (pointer2, message) => problems.push({ code: "shell_shape", pointer: pointer2, message });
|
|
13351
13940
|
const roleNames = new Set(
|
|
13352
13941
|
(Array.isArray(contract?.backend?.roles) ? contract.backend.roles : []).map((role) => typeof role === "string" ? role : role?.name).filter(named5)
|
|
13353
13942
|
);
|
|
13354
13943
|
if (app.shell !== void 0 && app.shell !== null) {
|
|
13355
13944
|
const shell = app.shell;
|
|
13356
|
-
if (!isPlainObject8(shell))
|
|
13945
|
+
if (!isPlainObject8(shell)) refuse2("/app/shell", `app.shell is ${JSON.stringify(shell)}; it is {"nav": "tabs"|"rail", "topbar"?: true, "rail"?: {...}}`);
|
|
13357
13946
|
else {
|
|
13358
|
-
if (shell.nav !== void 0 && !SHELL_NAVS.includes(shell.nav))
|
|
13359
|
-
if (shell.topbar !== void 0 && typeof shell.topbar !== "boolean")
|
|
13360
|
-
if (shell.rail !== void 0 && !isPlainObject8(shell.rail))
|
|
13947
|
+
if (shell.nav !== void 0 && !SHELL_NAVS.includes(shell.nav)) refuse2("/app/shell/nav", `app.shell.nav is ${JSON.stringify(shell.nav)}; it is ${either3(SHELL_NAVS)}`);
|
|
13948
|
+
if (shell.topbar !== void 0 && typeof shell.topbar !== "boolean") refuse2("/app/shell/topbar", `app.shell.topbar is ${JSON.stringify(shell.topbar)}; it is true or false`);
|
|
13949
|
+
if (shell.rail !== void 0 && !isPlainObject8(shell.rail)) refuse2("/app/shell/rail", `app.shell.rail is ${JSON.stringify(shell.rail)}; it is {"title"?, "entries"?: [...], "footer"?: [...]}`);
|
|
13361
13950
|
else if (isPlainObject8(shell.rail)) {
|
|
13362
13951
|
for (const key of ["entries", "footer"]) {
|
|
13363
13952
|
const list3 = shell.rail[key];
|
|
13364
13953
|
if (list3 === void 0) continue;
|
|
13365
13954
|
if (!Array.isArray(list3)) {
|
|
13366
|
-
|
|
13955
|
+
refuse2(`/app/shell/rail/${key}`, `app.shell.rail.${key} is ${JSON.stringify(list3)}; it is a list of {"label", ...}`);
|
|
13367
13956
|
continue;
|
|
13368
13957
|
}
|
|
13369
13958
|
for (const [index, entry] of list3.entries()) {
|
|
13370
13959
|
const at = `/app/shell/rail/${key}/${index}`;
|
|
13371
13960
|
if (!isPlainObject8(entry) || !named5(entry.label)) {
|
|
13372
|
-
|
|
13961
|
+
refuse2(at, `app.shell.rail.${key}[${index}] is ${JSON.stringify(entry)}; every entry has a label (the runtime drops one without)`);
|
|
13373
13962
|
continue;
|
|
13374
13963
|
}
|
|
13375
13964
|
if (entry.page !== void 0 && !(named5(entry.page) && paths.has(entry.page))) {
|
|
13376
|
-
|
|
13965
|
+
refuse2(`${at}/page`, `rail entry "${entry.label}" links to ${JSON.stringify(entry.page)}, which pages[] does not declare; name a page of this app (${[...paths].sort(natural3).map((one) => JSON.stringify(one)).join(", ")})`);
|
|
13377
13966
|
}
|
|
13378
|
-
if (key === "footer" && !named5(entry.page))
|
|
13967
|
+
if (key === "footer" && !named5(entry.page)) refuse2(`${at}/page`, `rail footer entry "${entry.label}" links nowhere; a footer entry is {"label", "page"}`);
|
|
13379
13968
|
if (entry.param !== void 0) {
|
|
13380
13969
|
if (!isPlainObject8(entry.param) || Object.keys(entry.param).length === 0) {
|
|
13381
|
-
|
|
13970
|
+
refuse2(`${at}/param`, `rail entry "${entry.label}" states param as ${JSON.stringify(entry.param)}; it is {"<page param>": <value>}`);
|
|
13382
13971
|
} else {
|
|
13383
13972
|
for (const name of Object.keys(entry.param).sort(natural3)) {
|
|
13384
|
-
if (!params.has(name))
|
|
13973
|
+
if (!params.has(name)) refuse2(`${at}/param/${escapePointer3(name)}`, `rail entry "${entry.label}" sets the param "${name}", which no page declares in pages[].params; the param store keeps only declared params, so the entry would do nothing`);
|
|
13385
13974
|
}
|
|
13386
13975
|
}
|
|
13387
13976
|
}
|
|
13388
|
-
if (entry.measure !== void 0 && !named5(entry.measure))
|
|
13977
|
+
if (entry.measure !== void 0 && !named5(entry.measure)) refuse2(`${at}/measure`, `rail entry "${entry.label}" states measure as ${JSON.stringify(entry.measure)}; it is the name of one measure of the page's nav.counts.source`);
|
|
13389
13978
|
if (entry.role !== void 0) {
|
|
13390
13979
|
const names = named5(entry.role) ? entry.role.split(/\s+/).filter(named5) : [];
|
|
13391
13980
|
const unknown = names.filter((name) => !roleNames.has(name));
|
|
13392
|
-
if (names.length === 0)
|
|
13981
|
+
if (names.length === 0) refuse2(`${at}/role`, `rail entry "${entry.label}" states role as ${JSON.stringify(entry.role)}; it is one role name, or several space-separated`);
|
|
13393
13982
|
else if (unknown.length > 0) {
|
|
13394
|
-
|
|
13983
|
+
refuse2(
|
|
13395
13984
|
`${at}/role`,
|
|
13396
13985
|
`rail entry "${entry.label}" is for ${unknown.map((one) => JSON.stringify(one)).join(", ")}, which ${roleNames.size === 0 ? "the app does not declare (it declares no roles)" : `backend.roles does not declare (${[...roleNames].map((one) => JSON.stringify(one)).join(", ")})`}; the runtime would draw it for nobody`
|
|
13397
13986
|
);
|
|
@@ -13405,14 +13994,14 @@ function shellProblems(contract, elements, handlers, sources) {
|
|
|
13405
13994
|
for (const [index, page] of pages.entries()) {
|
|
13406
13995
|
if (!isPlainObject8(page) || page.owned === true) continue;
|
|
13407
13996
|
if (isPlainObject8(page.header) && page.header.title_from !== void 0 && page.header.title_from !== "rail") {
|
|
13408
|
-
|
|
13997
|
+
refuse2(`/pages/${index}/header/title_from`, `${page.path}'s header.title_from is ${JSON.stringify(page.header.title_from)}; the one this runtime reads is "rail" (the active rail entry's label)`);
|
|
13409
13998
|
}
|
|
13410
13999
|
if (page.nav !== void 0) {
|
|
13411
14000
|
const source = page.nav?.counts?.source;
|
|
13412
14001
|
if (!isPlainObject8(page.nav) || !isPlainObject8(page.nav.counts) || !named5(source)) {
|
|
13413
|
-
|
|
14002
|
+
refuse2(`/pages/${index}/nav`, `${page.path}'s nav is ${JSON.stringify(page.nav)}; it is {"counts": {"source": "<summary handler>"}}`);
|
|
13414
14003
|
} else if (!Object.prototype.hasOwnProperty.call(handlers, source) && !Object.prototype.hasOwnProperty.call(sources ?? {}, source)) {
|
|
13415
|
-
|
|
14004
|
+
refuse2(`/pages/${index}/nav/counts/source`, `${page.path}'s nav.counts.source is "${source}", which is not a handler or source of this app; name the summary handler whose measures are the rail's counts`);
|
|
13416
14005
|
}
|
|
13417
14006
|
}
|
|
13418
14007
|
}
|
|
@@ -13859,7 +14448,7 @@ function queryablePlatformColumns(table) {
|
|
|
13859
14448
|
function platformQueryProblems(name, row, table) {
|
|
13860
14449
|
const problems = [];
|
|
13861
14450
|
const allowed = queryablePlatformColumns(table);
|
|
13862
|
-
const
|
|
14451
|
+
const refuse2 = (key, column) => {
|
|
13863
14452
|
const why = column === "_created_by" ? `table "${table.name}" does not declare showAuthor, so no read returns the author and none may filter or sort by it \u2014 declare "showAuthor": true on the table, or drop it` : `no app read returns it, so none may filter or sort by it \u2014 use _created_at or _updated_at${table?.showAuthor ? ", or _created_by" : ""}`;
|
|
13864
14453
|
problems.push({
|
|
13865
14454
|
code: "platform_column_not_queryable",
|
|
@@ -13869,11 +14458,11 @@ function platformQueryProblems(name, row, table) {
|
|
|
13869
14458
|
};
|
|
13870
14459
|
if (named5(row.order_by)) {
|
|
13871
14460
|
const column = row.order_by.replace(/^-+/, "");
|
|
13872
|
-
if (column.startsWith("_") && !allowed.has(column))
|
|
14461
|
+
if (column.startsWith("_") && !allowed.has(column)) refuse2("order_by", column);
|
|
13873
14462
|
}
|
|
13874
14463
|
for (const key of Object.keys(isPlainObject8(row.where) ? row.where : {}).sort()) {
|
|
13875
14464
|
const column = key.split("__")[0];
|
|
13876
|
-
if (column.startsWith("_") && !allowed.has(column))
|
|
14465
|
+
if (column.startsWith("_") && !allowed.has(column)) refuse2("where", column);
|
|
13877
14466
|
}
|
|
13878
14467
|
return problems;
|
|
13879
14468
|
}
|
|
@@ -15347,12 +15936,12 @@ function computedProblems(elements, tree, handlers, model) {
|
|
|
15347
15936
|
const computed = computedIn(row.table);
|
|
15348
15937
|
if (computed.size === 0) continue;
|
|
15349
15938
|
const at = (...keys2) => `/data/app/handlers/${[name, ...keys2].map(escapePointer3).join("/")}`;
|
|
15350
|
-
const
|
|
15939
|
+
const refuse2 = (pointer2, column, how) => push(pointer2, `handler "${name}" ${how} "${column}", which is computed by the platform from its children; nothing writes it`);
|
|
15351
15940
|
if (row.role === "submit" || row.role === "save") {
|
|
15352
15941
|
for (const key of ["fields", "required"]) {
|
|
15353
|
-
for (const hit of (Array.isArray(row[key]) ? row[key] : []).filter((column) => computed.has(column)))
|
|
15942
|
+
for (const hit of (Array.isArray(row[key]) ? row[key] : []).filter((column) => computed.has(column))) refuse2(at(key), hit, key === "fields" ? "takes" : "requires");
|
|
15354
15943
|
}
|
|
15355
|
-
for (const fixed of Object.keys(isPlainObject8(row.fixed) ? row.fixed : {}).filter((column) => computed.has(column)))
|
|
15944
|
+
for (const fixed of Object.keys(isPlainObject8(row.fixed) ? row.fixed : {}).filter((column) => computed.has(column))) refuse2(at("fixed"), fixed, "fixes");
|
|
15356
15945
|
}
|
|
15357
15946
|
}
|
|
15358
15947
|
if (tree.problems.length > 0) return problems;
|
|
@@ -15999,7 +16588,7 @@ function statusColumn(model, row) {
|
|
|
15999
16588
|
const status = table.columns.status;
|
|
16000
16589
|
return status !== void 0 && status.enum === null ? "status" : null;
|
|
16001
16590
|
}
|
|
16002
|
-
function manifestOf(contract, app, handlers, names, model, served = []) {
|
|
16591
|
+
function manifestOf(contract, app, handlers, names, model, served = [], readers = contract, known = /* @__PURE__ */ new Set()) {
|
|
16003
16592
|
const manifest = {
|
|
16004
16593
|
apiVersion: app.api_version ?? DEFAULT_API_VERSION,
|
|
16005
16594
|
id: app.id ?? null,
|
|
@@ -16025,7 +16614,7 @@ function manifestOf(contract, app, handlers, names, model, served = []) {
|
|
|
16025
16614
|
}
|
|
16026
16615
|
for (const name of served) {
|
|
16027
16616
|
const entry = { http: "POST", entrypoint: `handlers/${name}.py` };
|
|
16028
|
-
const { roles: gate } = sourceRoles(
|
|
16617
|
+
const { roles: gate } = sourceRoles(readers, name, known);
|
|
16029
16618
|
if (gate.length > 0) entry.requiresRole = gate;
|
|
16030
16619
|
manifest.handlers[name] = entry;
|
|
16031
16620
|
}
|
|
@@ -24976,6 +25565,15 @@ function rendererOf3(pages) {
|
|
|
24976
25565
|
}
|
|
24977
25566
|
return {};
|
|
24978
25567
|
}
|
|
25568
|
+
function metricsOf2(pages) {
|
|
25569
|
+
for (const page of pages ?? []) {
|
|
25570
|
+
const embedded = embeddedContractOf2(page);
|
|
25571
|
+
if (embedded === null) continue;
|
|
25572
|
+
const metrics = embedded.data?.metrics;
|
|
25573
|
+
return isPlainObject12(metrics) ? { metrics } : {};
|
|
25574
|
+
}
|
|
25575
|
+
return {};
|
|
25576
|
+
}
|
|
24979
25577
|
function cardSpecOf2(element) {
|
|
24980
25578
|
const card = element?.props?.card;
|
|
24981
25579
|
return element?.type === "widget" && typeof card === "string" && card !== "" ? card : null;
|
|
@@ -25562,7 +26160,9 @@ function dataSection2(app, manifest, spec, elements, describe7, problems) {
|
|
|
25562
26160
|
...application === null ? {} : { app: application },
|
|
25563
26161
|
// Only when there are any: a contract written before sources existed
|
|
25564
26162
|
// reads back byte for byte as it always did.
|
|
25565
|
-
...Object.keys(sources).length > 0 ? { sources } : {}
|
|
26163
|
+
...Object.keys(sources).length > 0 ? { sources } : {},
|
|
26164
|
+
// The same for declared metrics, which only an app's pages carry.
|
|
26165
|
+
...isApp ? metricsOf2(app.pages) : {}
|
|
25566
26166
|
};
|
|
25567
26167
|
if (describe7) {
|
|
25568
26168
|
const widgets2 = isPlainObject12(describe7.widgets) ? describe7.widgets : {};
|
|
@@ -28836,7 +29436,7 @@ function rolesLandingNowhere2(contract) {
|
|
|
28836
29436
|
}
|
|
28837
29437
|
return out;
|
|
28838
29438
|
}
|
|
28839
|
-
function sourceRoles2(contract, name) {
|
|
29439
|
+
function sourceRoles2(contract, name, known = /* @__PURE__ */ new Set()) {
|
|
28840
29440
|
const source = contract?.data?.sources?.[name];
|
|
28841
29441
|
const stated2 = gateRoles2(source?.requiresRole);
|
|
28842
29442
|
const elements = isPlainObject17(contract?.elements) ? contract.elements : {};
|
|
@@ -28854,7 +29454,7 @@ function sourceRoles2(contract, name) {
|
|
|
28854
29454
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
28855
29455
|
}
|
|
28856
29456
|
for (const page of pagesOf2(contract)) {
|
|
28857
|
-
if (page.owned !== true || readers === 0) continue;
|
|
29457
|
+
if (page.owned !== true || readers === 0 || known.has(page.path)) continue;
|
|
28858
29458
|
const roles = pageRoles2(page);
|
|
28859
29459
|
if (roles.length === 0) open2 = true;
|
|
28860
29460
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
@@ -29092,6 +29692,14 @@ var REFUSALS = /* @__PURE__ */ new Set([
|
|
|
29092
29692
|
"source_compare_field",
|
|
29093
29693
|
// a source's formula, which the platform evaluates (lineage tier 3)
|
|
29094
29694
|
"source_expr",
|
|
29695
|
+
// a declared Datarails metric (data.metrics, design 06), and a source that
|
|
29696
|
+
// reads one by key
|
|
29697
|
+
"metric_decl",
|
|
29698
|
+
"metric_kind_later",
|
|
29699
|
+
"metric_formula",
|
|
29700
|
+
"metric_base",
|
|
29701
|
+
"metric_format",
|
|
29702
|
+
"source_metric",
|
|
29095
29703
|
// dr-components parts (DC-4)
|
|
29096
29704
|
"part_props",
|
|
29097
29705
|
"part_source_shape",
|
|
@@ -29178,6 +29786,14 @@ var KPI_REFUSALS = {
|
|
|
29178
29786
|
var EXPR_REFUSALS = {
|
|
29179
29787
|
source_expr: "a source's formula (its expr) has a from that is no list, or sits beside a transform; answers a shape other than kpi, series or breakdown; states compare (its compare is its formula over its operands' compares); cannot be evaluated (it is not text, a syntax error, a function other than abs, min, max, round and if, a wrong number of arguments, round decimals other than a whole number from 0 to 6, an if whose first argument is no comparison, a comparison anywhere but if's condition, a name not in from); can be, on some path, a number it states rather than an operand's (numbers alone such as 870000, a typed if() branch such as if(actual > 0, 870000, actual), or a typed min() or max() bound such as max(actual, 0): each branch of if() and each argument of min() and max() must come from an operand, and / already gives no figure for a zero divisor, so no fallback number is needed); names an operand it never reads; reads an operand that is not a FinanceOS read or metric or a formula over them (a transform, a composed source, a call, one of the app's own tables); reads an operand of a shape it cannot take (a kpi formula takes kpis; a series or breakdown formula its own shape and kpis), or, as a series or breakdown formula, has no operand of its own shape; reads a breakdown operand that does not state top: null; or reads itself \u2014 the message names which"
|
|
29180
29788
|
};
|
|
29789
|
+
var METRIC_REFUSALS = {
|
|
29790
|
+
metric_decl: `a data.metrics key that is not a lowercase letter then lowercase letters, digits or _; a declaration that is no object, or states a key a declaration does not take; a name that is empty, over 128 characters, holds ' [ or ], starts with @, or is DAYS_IN_PERIOD or PERIODS_IN_YEAR; a kind other than base or calc; a calc with no formula; a base with no table, value, date or aggregate, or a table that is not exactly one of alias, name and id; one kind's keys on the other; a reuse other than definition or name, a create that is not true or false, a description that is not text; or a declaration with no definition that does not say "reuse": "name" and "create": false \u2014 the message names which`,
|
|
29791
|
+
metric_kind_later: 'a declaration of kind driver, which is reserved until driver declarations ship \u2014 declare a base or calc metric, or bind the driver by name ("reuse": "name", "create": false)',
|
|
29792
|
+
metric_formula: "a calc formula Datarails would refuse (dr-cli's checks: empty or over 5000 characters, unbalanced brackets or grouping, a time modifier that is lowercase, chained, on no operand, malformed, a future SHIFT or one past 1200, an invalid unit or anchor, a modifier on a system variable, an unbracketed word, no metric read); arithmetic out of order (two operators in a row, a missing operand or operator, empty parentheses, a number such as 1.2.3); an operand that is not a declared [@key] \u2014 a metric named itself, quoted or bracketed ('Revenue', [Revenue]), so the contract names no company's metric; a scenario pin (|); a window function it cannot read; or a declaration that reads itself, however far round",
|
|
29793
|
+
metric_base: "a base aggregate other than sum, count, cumulative_sum or cumulative_count, a time_aggregation other than sum, eop or avg, a cumulative aggregate rolled up with sum, or a filter that is not {field, values, exclude}",
|
|
29794
|
+
metric_format: 'a format the metric editor never writes (optional "$" quoted, \u20AC \xA3 \u20AA or a % suffix, #,### or ####, .# to .####, [kilo]k or [mega]M) \u2014 the message names the stored spelling where one was plainly meant',
|
|
29795
|
+
source_metric: `a source's from.financeos.metric that is not "@<key>" of a key data.metrics declares, or that stands beside metric_id`
|
|
29796
|
+
};
|
|
29181
29797
|
var TRANSFORM_REFUSALS = {
|
|
29182
29798
|
transform_row: 'a row names a transform and is not a dataset series row ("kind": "series", "data": "<dataset>", and no source, compute, depends, drill, window or by), or its dataset reads with the period pushed down (a period other than "all")',
|
|
29183
29799
|
transform_missing: "a row or source names a transform not spelled transforms/<name>.py, or that file is not beside contract.json",
|
|
@@ -30286,6 +30902,9 @@ function registerCompileCommand(parent) {
|
|
|
30286
30902
|
...Object.entries(KPI_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
|
|
30287
30903
|
"A source that states its figure as a formula (expr over from, evaluated by the platform) is refused with:",
|
|
30288
30904
|
...Object.entries(EXPR_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
|
|
30905
|
+
'A declared Datarails metric (data.metrics, read by a source as from.financeos.metric "@<key>") is refused with:',
|
|
30906
|
+
...Object.entries(METRIC_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
|
|
30907
|
+
" (warnings, never refusals: metric_window, a window function Datarails gates by company; metric_unused, a declaration no source reads; expr_could_be_metric, an expr a calc metric could say)",
|
|
30289
30908
|
"",
|
|
30290
30909
|
"A transform (a dashboard dataset series row's transforms/<name>.py, or an app source's) is refused with:",
|
|
30291
30910
|
...Object.entries(TRANSFORM_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`)
|
|
@@ -30336,29 +30955,29 @@ var ONTO_LIVE_MARK = ".onto-live";
|
|
|
30336
30955
|
var ONTO_LIVE_OWNED = ["contract.json", "manifest.json", "data_model.json", "handlers", "ui", "transforms", ".appspace.json", ".appspace"];
|
|
30337
30956
|
async function runShipOntoLive(dir, opts) {
|
|
30338
30957
|
const appId = opts.appId;
|
|
30339
|
-
const
|
|
30958
|
+
const refuse2 = (why) => {
|
|
30340
30959
|
process.stderr.write(`ship --onto-live: ${why}
|
|
30341
30960
|
`);
|
|
30342
30961
|
return 1;
|
|
30343
30962
|
};
|
|
30344
|
-
if (appId === void 0 || appId.trim() === "") return
|
|
30345
|
-
if ((opts.draft ?? []).length === 0) return
|
|
30963
|
+
if (appId === void 0 || appId.trim() === "") return refuse2("pass --app-id <id>: the app to land on.");
|
|
30964
|
+
if ((opts.draft ?? []).length === 0) return refuse2("pass --draft <file>: the cards to land.");
|
|
30346
30965
|
if (opts.skeleton === void 0) {
|
|
30347
|
-
return
|
|
30966
|
+
return refuse2("pass --skeleton <file>: the frame the app is created from if nothing has landed yet.");
|
|
30348
30967
|
}
|
|
30349
30968
|
if ((opts.expect ?? []).length > 0) {
|
|
30350
|
-
return
|
|
30969
|
+
return refuse2("--expect holds the headline, which is the build's to confirm; a landing proves its own cards only.");
|
|
30351
30970
|
}
|
|
30352
30971
|
if (opts.force === true) {
|
|
30353
|
-
return
|
|
30972
|
+
return refuse2(
|
|
30354
30973
|
"does not take --force: every landing builds on the cards already live, and --force would let this one replace them. Run the same command without it. If it refuses over files it did not write, the live app could not be read whole: run it again, and report the refusal if it repeats."
|
|
30355
30974
|
);
|
|
30356
30975
|
}
|
|
30357
30976
|
const owned = await claimFolder(dir);
|
|
30358
|
-
if (owned !== null) return
|
|
30977
|
+
if (owned !== null) return refuse2(owned);
|
|
30359
30978
|
await defaultServerFromSession(opts);
|
|
30360
30979
|
if (!hasServer(opts)) {
|
|
30361
|
-
return
|
|
30980
|
+
return refuse2("needs a server: pass -s <server> (or set DR_APP_SPACE_HOST), or log in so a stored session supplies one.");
|
|
30362
30981
|
}
|
|
30363
30982
|
return landWithRetries(async (createGuarded) => {
|
|
30364
30983
|
for (const name of ONTO_LIVE_OWNED) await rm4(join15(dir, name), { recursive: true, force: true });
|
|
@@ -30652,7 +31271,7 @@ function unservedAfter(path3, lost) {
|
|
|
30652
31271
|
const one = lost.length === 1;
|
|
30653
31272
|
const names = lost.map((name) => JSON.stringify(name)).join(", ");
|
|
30654
31273
|
const files = lost.map((name) => `handlers/${name}.py`).join(", ");
|
|
30655
|
-
return
|
|
31274
|
+
return `after this eject the compiler would write no handler for source${one ? "" : "s"} ${names}, which ${path3} reads, so ${files} would go and the page would call ${one ? "a handler" : "handlers"} that ${one ? "is" : "are"} not there. The compiler counts an owned page as a reader of what the contract it embeds reads, and eject keeps that contract on the page, so this is a bug in the compiler or in eject, not in your contract. Meanwhile, keep a card that reads ${one ? "it" : "each"} on a compiled page, or eject ${one ? "the source" : "each source"} first (app-space eject ${lost.map((name) => `source:${name}`).join(", ")}), which makes it a custom handler the compiler always carries, where that eject is allowed: eject source: says when it is not.`;
|
|
30656
31275
|
}
|
|
30657
31276
|
function navGateOf(files, path3) {
|
|
30658
31277
|
const gates = navGates;
|
|
@@ -32788,7 +33407,7 @@ async function runUiSmoke(opts) {
|
|
|
32788
33407
|
const { JSDOM, ResourceLoader, VirtualConsole } = await loadJsdom();
|
|
32789
33408
|
let inFlight = 0;
|
|
32790
33409
|
let lastActivity = Date.now();
|
|
32791
|
-
async function record2(req, via,
|
|
33410
|
+
async function record2(req, via, refuse2) {
|
|
32792
33411
|
const call = { method: req.method, url: maskToken(req.url), via };
|
|
32793
33412
|
if (via === "fetch" && req.body) {
|
|
32794
33413
|
try {
|
|
@@ -32800,8 +33419,8 @@ async function runUiSmoke(opts) {
|
|
|
32800
33419
|
const auth = req.headers["authorization"];
|
|
32801
33420
|
if (auth !== void 0) call.authorization = summariseAuthorization(auth);
|
|
32802
33421
|
calls.push(call);
|
|
32803
|
-
if (
|
|
32804
|
-
call.failure =
|
|
33422
|
+
if (refuse2 !== void 0) {
|
|
33423
|
+
call.failure = refuse2;
|
|
32805
33424
|
return void 0;
|
|
32806
33425
|
}
|
|
32807
33426
|
if (!sameOrigin(req.url, target)) {
|