@datarails-ai/dr-appspace-cli 0.12.38 → 0.12.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1706 -314
- 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.39",
|
|
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-26T15:
|
|
60
|
+
var BUILD_TIME = "2026-10-26T15:11:56.868Z";
|
|
61
61
|
function formatBuildTime(iso) {
|
|
62
62
|
if (iso === "") return "";
|
|
63
63
|
const d = new Date(iso);
|
|
@@ -97,13 +97,13 @@ function compareSemver(a, b) {
|
|
|
97
97
|
async function readCache() {
|
|
98
98
|
try {
|
|
99
99
|
const raw = await fs.readFile(cachePath(), "utf8");
|
|
100
|
-
const
|
|
101
|
-
if (typeof
|
|
100
|
+
const parsed2 = JSON.parse(raw);
|
|
101
|
+
if (typeof parsed2.checked_at !== "number" || typeof parsed2.latest_version !== "string" || parsed2.latest_version === "") {
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
return {
|
|
105
|
-
checked_at:
|
|
106
|
-
latest_version:
|
|
105
|
+
checked_at: parsed2.checked_at,
|
|
106
|
+
latest_version: parsed2.latest_version
|
|
107
107
|
};
|
|
108
108
|
} catch {
|
|
109
109
|
return null;
|
|
@@ -400,14 +400,14 @@ var KeyringStore = class _KeyringStore {
|
|
|
400
400
|
return out;
|
|
401
401
|
}
|
|
402
402
|
if (raw === void 0) return out;
|
|
403
|
-
let
|
|
403
|
+
let parsed2;
|
|
404
404
|
try {
|
|
405
|
-
|
|
405
|
+
parsed2 = JSON.parse(raw);
|
|
406
406
|
} catch {
|
|
407
407
|
return out;
|
|
408
408
|
}
|
|
409
|
-
if (!Array.isArray(
|
|
410
|
-
for (const item of
|
|
409
|
+
if (!Array.isArray(parsed2)) return out;
|
|
410
|
+
for (const item of parsed2) {
|
|
411
411
|
const savedAt = parseDate(item?.savedAt);
|
|
412
412
|
const lastUsedAt = parseDate(item?.lastUsedAt);
|
|
413
413
|
if (typeof item?.email === "string" && savedAt !== void 0 && lastUsedAt !== void 0) {
|
|
@@ -434,22 +434,22 @@ var KeyringStore = class _KeyringStore {
|
|
|
434
434
|
const sessionAccount = `${serverKey}:${email}`;
|
|
435
435
|
const raw = await this.backend.get(SERVICE, sessionAccount);
|
|
436
436
|
if (raw === void 0) return void 0;
|
|
437
|
-
let
|
|
437
|
+
let parsed2;
|
|
438
438
|
try {
|
|
439
|
-
|
|
439
|
+
parsed2 = JSON.parse(raw);
|
|
440
440
|
} catch (err) {
|
|
441
441
|
throw new KeychainError(`decode session for ${serverKey}:${email}`, {
|
|
442
442
|
cause: err
|
|
443
443
|
});
|
|
444
444
|
}
|
|
445
|
-
const sessionId = typeof
|
|
446
|
-
const csrfToken = typeof
|
|
447
|
-
const savedAtStr = typeof
|
|
448
|
-
const impersonatorEmail = typeof
|
|
445
|
+
const sessionId = typeof parsed2.sessionId === "string" ? parsed2.sessionId : "";
|
|
446
|
+
const csrfToken = typeof parsed2.csrfToken === "string" ? parsed2.csrfToken : "";
|
|
447
|
+
const savedAtStr = typeof parsed2.savedAt === "string" ? parsed2.savedAt : "";
|
|
448
|
+
const impersonatorEmail = typeof parsed2.impersonatorEmail === "string" ? parsed2.impersonatorEmail : "";
|
|
449
449
|
const impersonated = impersonatorEmail !== "";
|
|
450
|
-
const cookielessFlag =
|
|
451
|
-
const apiKey = typeof
|
|
452
|
-
const apiKeyEnvVar = typeof
|
|
450
|
+
const cookielessFlag = parsed2.cookieless === true;
|
|
451
|
+
const apiKey = typeof parsed2.apiKey === "string" && parsed2.apiKey !== "" ? parsed2.apiKey : "";
|
|
452
|
+
const apiKeyEnvVar = typeof parsed2.apiKeyEnvVar === "string" && parsed2.apiKeyEnvVar !== "" ? parsed2.apiKeyEnvVar : "";
|
|
453
453
|
const keyed = apiKey !== "" || apiKeyEnvVar !== "";
|
|
454
454
|
const cookieless = impersonated || cookielessFlag || keyed;
|
|
455
455
|
if (savedAtStr === "" || !cookieless && (sessionId === "" || csrfToken === "")) {
|
|
@@ -463,11 +463,11 @@ var KeyringStore = class _KeyringStore {
|
|
|
463
463
|
};
|
|
464
464
|
if (impersonated) {
|
|
465
465
|
session.impersonatorEmail = impersonatorEmail;
|
|
466
|
-
if (typeof
|
|
467
|
-
session.impersonatedUserId =
|
|
466
|
+
if (typeof parsed2.impersonatedUserId === "number") {
|
|
467
|
+
session.impersonatedUserId = parsed2.impersonatedUserId;
|
|
468
468
|
}
|
|
469
|
-
if (typeof
|
|
470
|
-
session.impersonatedOrgName =
|
|
469
|
+
if (typeof parsed2.impersonatedOrgName === "string" && parsed2.impersonatedOrgName !== "") {
|
|
470
|
+
session.impersonatedOrgName = parsed2.impersonatedOrgName;
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
if (cookielessFlag) {
|
|
@@ -479,14 +479,14 @@ var KeyringStore = class _KeyringStore {
|
|
|
479
479
|
if (apiKeyEnvVar !== "") {
|
|
480
480
|
session.apiKeyEnvVar = apiKeyEnvVar;
|
|
481
481
|
}
|
|
482
|
-
if (typeof
|
|
483
|
-
session.apiKeyExpiresAt = new Date(
|
|
482
|
+
if (typeof parsed2.apiKeyExpiresAt === "string" && parsed2.apiKeyExpiresAt !== "") {
|
|
483
|
+
session.apiKeyExpiresAt = new Date(parsed2.apiKeyExpiresAt);
|
|
484
484
|
}
|
|
485
|
-
if (typeof
|
|
486
|
-
session.jwtApiKeyFingerprint =
|
|
485
|
+
if (typeof parsed2.jwtApiKeyFingerprint === "string" && parsed2.jwtApiKeyFingerprint !== "") {
|
|
486
|
+
session.jwtApiKeyFingerprint = parsed2.jwtApiKeyFingerprint;
|
|
487
487
|
}
|
|
488
|
-
if (typeof
|
|
489
|
-
session.jwtExpiresAt = new Date(
|
|
488
|
+
if (typeof parsed2.jwtExpiresAt === "string" && parsed2.jwtExpiresAt !== "") {
|
|
489
|
+
session.jwtExpiresAt = new Date(parsed2.jwtExpiresAt);
|
|
490
490
|
}
|
|
491
491
|
const jwtAccess = await this.backend.get(SERVICE, `${sessionAccount}:access`);
|
|
492
492
|
if (jwtAccess !== void 0 && jwtAccess !== "") {
|
|
@@ -768,12 +768,12 @@ async function requestDeviceCode(host, options) {
|
|
|
768
768
|
if (raw === null || typeof raw !== "object") {
|
|
769
769
|
throw new Error(`device/code parse: expected object`);
|
|
770
770
|
}
|
|
771
|
-
const
|
|
771
|
+
const obj5 = raw;
|
|
772
772
|
return {
|
|
773
|
-
deviceCode: typeof
|
|
774
|
-
userCode: typeof
|
|
775
|
-
verificationUriComplete: typeof
|
|
776
|
-
interval: typeof
|
|
773
|
+
deviceCode: typeof obj5.device_code === "string" ? obj5.device_code : "",
|
|
774
|
+
userCode: typeof obj5.user_code === "string" ? obj5.user_code : "",
|
|
775
|
+
verificationUriComplete: typeof obj5.verification_uri_complete === "string" ? obj5.verification_uri_complete : "",
|
|
776
|
+
interval: typeof obj5.interval === "number" ? obj5.interval : 0
|
|
777
777
|
};
|
|
778
778
|
}
|
|
779
779
|
var defaultSleep = (ms, signal) => new Promise((resolve17, reject) => {
|
|
@@ -802,13 +802,13 @@ function parseTokenResponse(text3) {
|
|
|
802
802
|
if (raw === null || typeof raw !== "object") {
|
|
803
803
|
throw new Error(`token parse: expected object`);
|
|
804
804
|
}
|
|
805
|
-
const
|
|
805
|
+
const obj5 = raw;
|
|
806
806
|
const tok = {
|
|
807
|
-
sessionId: typeof
|
|
808
|
-
csrfToken: typeof
|
|
807
|
+
sessionId: typeof obj5.session_id === "string" ? obj5.session_id : "",
|
|
808
|
+
csrfToken: typeof obj5.csrf_token === "string" ? obj5.csrf_token : ""
|
|
809
809
|
};
|
|
810
|
-
if (typeof
|
|
811
|
-
tok.accessToken =
|
|
810
|
+
if (typeof obj5.access_token === "string") {
|
|
811
|
+
tok.accessToken = obj5.access_token;
|
|
812
812
|
}
|
|
813
813
|
return tok;
|
|
814
814
|
}
|
|
@@ -946,16 +946,16 @@ function decodeJwtClaims(token) {
|
|
|
946
946
|
} catch (err) {
|
|
947
947
|
throw new Error(`jwt: decode payload: ${msgOf3(err)}`);
|
|
948
948
|
}
|
|
949
|
-
let
|
|
949
|
+
let parsed2;
|
|
950
950
|
try {
|
|
951
|
-
|
|
951
|
+
parsed2 = JSON.parse(decoded);
|
|
952
952
|
} catch (err) {
|
|
953
953
|
throw new Error(`jwt: parse claims: ${msgOf3(err)}`);
|
|
954
954
|
}
|
|
955
|
-
if (
|
|
955
|
+
if (parsed2 === null || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
956
956
|
throw new Error("jwt: parse claims: expected object");
|
|
957
957
|
}
|
|
958
|
-
return
|
|
958
|
+
return parsed2;
|
|
959
959
|
}
|
|
960
960
|
function decodeJwtExpiration(token) {
|
|
961
961
|
const claims = decodeJwtClaims(token);
|
|
@@ -975,10 +975,10 @@ function parseTokenBody(text3, label) {
|
|
|
975
975
|
if (raw === null || typeof raw !== "object") {
|
|
976
976
|
throw new Error(`jwt ${label} parse: expected object`);
|
|
977
977
|
}
|
|
978
|
-
const
|
|
978
|
+
const obj5 = raw;
|
|
979
979
|
return {
|
|
980
|
-
access: typeof
|
|
981
|
-
refresh: typeof
|
|
980
|
+
access: typeof obj5.access === "string" ? obj5.access : "",
|
|
981
|
+
refresh: typeof obj5.refresh === "string" ? obj5.refresh : ""
|
|
982
982
|
};
|
|
983
983
|
}
|
|
984
984
|
async function mintJwt(host, sessionId, csrfToken, options) {
|
|
@@ -1081,9 +1081,9 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1081
1081
|
let code;
|
|
1082
1082
|
let detail;
|
|
1083
1083
|
try {
|
|
1084
|
-
const
|
|
1085
|
-
if (typeof
|
|
1086
|
-
if (typeof
|
|
1084
|
+
const parsed2 = JSON.parse(text3);
|
|
1085
|
+
if (typeof parsed2["code"] === "string") code = parsed2["code"];
|
|
1086
|
+
if (typeof parsed2["detail"] === "string") detail = parsed2["detail"];
|
|
1087
1087
|
} catch {
|
|
1088
1088
|
}
|
|
1089
1089
|
throw new LoginAsError(res.status, {
|
|
@@ -1101,9 +1101,9 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1101
1101
|
if (raw === null || typeof raw !== "object") {
|
|
1102
1102
|
throw new Error("jwt loginas parse: expected object");
|
|
1103
1103
|
}
|
|
1104
|
-
const
|
|
1105
|
-
const access = typeof
|
|
1106
|
-
const refresh = typeof
|
|
1104
|
+
const obj5 = raw;
|
|
1105
|
+
const access = typeof obj5["access"] === "string" ? obj5["access"] : "";
|
|
1106
|
+
const refresh = typeof obj5["refresh"] === "string" ? obj5["refresh"] : "";
|
|
1107
1107
|
if (access === "") {
|
|
1108
1108
|
throw new Error("jwt loginas: empty access token");
|
|
1109
1109
|
}
|
|
@@ -1113,7 +1113,7 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1113
1113
|
} catch (err) {
|
|
1114
1114
|
throw new Error(`jwt loginas: ${msgOf3(err)}`);
|
|
1115
1115
|
}
|
|
1116
|
-
return { access, refresh, expiresAt, user:
|
|
1116
|
+
return { access, refresh, expiresAt, user: obj5["user"] };
|
|
1117
1117
|
}
|
|
1118
1118
|
async function exchangeApiKey(host, apiKey, options) {
|
|
1119
1119
|
const key = apiKey.trim();
|
|
@@ -2135,14 +2135,14 @@ async function readCredentialsFile(path3) {
|
|
|
2135
2135
|
if (err.code === "ENOENT") return void 0;
|
|
2136
2136
|
throw err;
|
|
2137
2137
|
}
|
|
2138
|
-
let
|
|
2138
|
+
let parsed2;
|
|
2139
2139
|
try {
|
|
2140
|
-
|
|
2140
|
+
parsed2 = JSON.parse(raw);
|
|
2141
2141
|
} catch {
|
|
2142
2142
|
throw new Error(`credential file ${path3} is not valid JSON`);
|
|
2143
2143
|
}
|
|
2144
2144
|
try {
|
|
2145
|
-
return credentialsFileSchema.parse(
|
|
2145
|
+
return credentialsFileSchema.parse(parsed2);
|
|
2146
2146
|
} catch (err) {
|
|
2147
2147
|
const reason = err instanceof Error ? err.message : String(err);
|
|
2148
2148
|
throw new Error(`credential file ${path3} is malformed: ${reason}`, { cause: err });
|
|
@@ -2227,14 +2227,14 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2227
2227
|
}
|
|
2228
2228
|
/** Cookie fields out of a metadata blob, dropped when empty. */
|
|
2229
2229
|
static cookieOf(blob) {
|
|
2230
|
-
let
|
|
2230
|
+
let parsed2;
|
|
2231
2231
|
try {
|
|
2232
|
-
|
|
2232
|
+
parsed2 = JSON.parse(blob);
|
|
2233
2233
|
} catch {
|
|
2234
2234
|
return {};
|
|
2235
2235
|
}
|
|
2236
|
-
const sessionId = typeof
|
|
2237
|
-
const csrfToken = typeof
|
|
2236
|
+
const sessionId = typeof parsed2.sessionId === "string" ? parsed2.sessionId : "";
|
|
2237
|
+
const csrfToken = typeof parsed2.csrfToken === "string" ? parsed2.csrfToken : "";
|
|
2238
2238
|
return {
|
|
2239
2239
|
...sessionId !== "" && { sessionId },
|
|
2240
2240
|
...csrfToken !== "" && { csrfToken }
|
|
@@ -2248,8 +2248,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2248
2248
|
*/
|
|
2249
2249
|
static fingerprintOf(blob) {
|
|
2250
2250
|
try {
|
|
2251
|
-
const
|
|
2252
|
-
return typeof
|
|
2251
|
+
const parsed2 = JSON.parse(blob);
|
|
2252
|
+
return typeof parsed2.jwtApiKeyFingerprint === "string" ? parsed2.jwtApiKeyFingerprint : "";
|
|
2253
2253
|
} catch {
|
|
2254
2254
|
return "";
|
|
2255
2255
|
}
|
|
@@ -2467,11 +2467,11 @@ function registerLoginCommand(root) {
|
|
|
2467
2467
|
}
|
|
2468
2468
|
let timeoutMs;
|
|
2469
2469
|
if (opts.timeout !== void 0) {
|
|
2470
|
-
const
|
|
2471
|
-
if (!Number.isFinite(
|
|
2470
|
+
const parsed2 = Number(opts.timeout);
|
|
2471
|
+
if (!Number.isFinite(parsed2) || parsed2 <= 0) {
|
|
2472
2472
|
throw new Error(`--timeout must be a positive number of milliseconds`);
|
|
2473
2473
|
}
|
|
2474
|
-
timeoutMs =
|
|
2474
|
+
timeoutMs = parsed2;
|
|
2475
2475
|
}
|
|
2476
2476
|
const store = fileStore?.store ?? await KeyringStore.defaultWithCleanup();
|
|
2477
2477
|
await runLogin({
|
|
@@ -2962,6 +2962,16 @@ async function connect(opts) {
|
|
|
2962
2962
|
return {
|
|
2963
2963
|
serverKey: client.serverKey,
|
|
2964
2964
|
baseUrl,
|
|
2965
|
+
host: client.host,
|
|
2966
|
+
email: client.email,
|
|
2967
|
+
claims() {
|
|
2968
|
+
try {
|
|
2969
|
+
const access = client.session.jwtAccess ?? "";
|
|
2970
|
+
return access === "" ? null : decodeJwtClaims(access);
|
|
2971
|
+
} catch {
|
|
2972
|
+
return null;
|
|
2973
|
+
}
|
|
2974
|
+
},
|
|
2965
2975
|
cleanup,
|
|
2966
2976
|
upload: (appId, file) => uploadVia(
|
|
2967
2977
|
(path3, fields, blob, options) => client.postMultipart(path3, fields, blob, options),
|
|
@@ -3008,16 +3018,16 @@ async function connect(opts) {
|
|
|
3008
3018
|
`the request was intercepted before reaching App Space (HTTP ${res.status}, HTML page returned \u2014 likely the Cloudflare WAF). The CLI gzips UI bundles to avoid this; if you are calling the API yourself, send frontend_bundle_gz.`
|
|
3009
3019
|
);
|
|
3010
3020
|
}
|
|
3011
|
-
let
|
|
3021
|
+
let parsed2 = void 0;
|
|
3012
3022
|
try {
|
|
3013
|
-
|
|
3023
|
+
parsed2 = text3 === "" ? void 0 : JSON.parse(text3);
|
|
3014
3024
|
} catch {
|
|
3015
|
-
|
|
3025
|
+
parsed2 = text3;
|
|
3016
3026
|
}
|
|
3017
3027
|
if (!res.ok) {
|
|
3018
|
-
throw new AppSpaceHttpError(describeFailure(res.status, method, path3,
|
|
3028
|
+
throw new AppSpaceHttpError(describeFailure(res.status, method, path3, parsed2), res.status, parsed2);
|
|
3019
3029
|
}
|
|
3020
|
-
return
|
|
3030
|
+
return parsed2;
|
|
3021
3031
|
}
|
|
3022
3032
|
};
|
|
3023
3033
|
}
|
|
@@ -3131,11 +3141,11 @@ function formatProblems(problems) {
|
|
|
3131
3141
|
if (problems.length === 0) return "";
|
|
3132
3142
|
const width = Math.max(...problems.map((p) => p.code.length + 2)) + 2;
|
|
3133
3143
|
let out = "";
|
|
3134
|
-
for (const
|
|
3135
|
-
const head = `[${
|
|
3136
|
-
out += `${head}${
|
|
3144
|
+
for (const problem6 of problems) {
|
|
3145
|
+
const head = `[${problem6.code}]`.padEnd(width);
|
|
3146
|
+
out += `${head}${problem6.pointer}: ${problem6.message}
|
|
3137
3147
|
`;
|
|
3138
|
-
out += `${" ".repeat(head.length)}${
|
|
3148
|
+
out += `${" ".repeat(head.length)}${problem6.fix}
|
|
3139
3149
|
`;
|
|
3140
3150
|
}
|
|
3141
3151
|
return out;
|
|
@@ -3164,13 +3174,13 @@ var NO_CATALOG = {
|
|
|
3164
3174
|
};
|
|
3165
3175
|
async function readCatalogFile(path3) {
|
|
3166
3176
|
const raw = await readFile2(path3, "utf8");
|
|
3167
|
-
let
|
|
3177
|
+
let parsed2;
|
|
3168
3178
|
try {
|
|
3169
|
-
|
|
3179
|
+
parsed2 = JSON.parse(raw);
|
|
3170
3180
|
} catch {
|
|
3171
3181
|
throw new Error(`${path3} is not valid JSON.`);
|
|
3172
3182
|
}
|
|
3173
|
-
const root = unwrap(
|
|
3183
|
+
const root = unwrap(parsed2);
|
|
3174
3184
|
const metrics = rows(root["metrics"] ?? root["kpis"] ?? root["business_metrics"]).map(
|
|
3175
3185
|
(row) => ({
|
|
3176
3186
|
id: idOf(row),
|
|
@@ -3332,8 +3342,8 @@ function findTable(catalog, named13) {
|
|
|
3332
3342
|
(table) => table.id.toLowerCase() === want || (table.alias ?? "").toLowerCase() === want || table.name.toLowerCase() === want
|
|
3333
3343
|
);
|
|
3334
3344
|
}
|
|
3335
|
-
function unwrap(
|
|
3336
|
-
let node = isObject(
|
|
3345
|
+
function unwrap(parsed2) {
|
|
3346
|
+
let node = isObject(parsed2) ? parsed2 : {};
|
|
3337
3347
|
for (const key of ["catalog", "data", "result"]) {
|
|
3338
3348
|
const held = node[key];
|
|
3339
3349
|
if (isObject(held) && ("tables" in held || "metrics" in held || "kpis" in held)) {
|
|
@@ -6830,7 +6840,7 @@ function specTransforms(handlers, spec, problems) {
|
|
|
6830
6840
|
out.push({ path: path3, text: text3 });
|
|
6831
6841
|
}
|
|
6832
6842
|
}
|
|
6833
|
-
const unreadable = problems.some((
|
|
6843
|
+
const unreadable = problems.some((problem6) => problem6.code === "transform_unreadable");
|
|
6834
6844
|
for (const key of Object.keys(spec.table).filter(isWidgetKey).sort(natural3)) {
|
|
6835
6845
|
const path3 = isPlainObject(spec.table[key]) ? spec.table[key].transform : void 0;
|
|
6836
6846
|
if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
|
|
@@ -8732,8 +8742,8 @@ function unproducedPayloadKeys(contract, { handlerSource = () => void 0, moduleS
|
|
|
8732
8742
|
if (produced.includes(key)) continue;
|
|
8733
8743
|
const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
|
|
8734
8744
|
const fix = reads.generated ? `Add a control that sets it, or take "${key}" out of the handler's required in contract.json.` : `Add a control that sets it, or read it with payload.get("${key}", <default>).`;
|
|
8735
|
-
const
|
|
8736
|
-
out.push({ handler: name, key, pages, produced, generated: reads.generated, problem:
|
|
8745
|
+
const problem6 = `handler "${name}" reads payload "${key}", and nothing on ${either(pages)} that calls it produces that key \u2014 ${there}.`;
|
|
8746
|
+
out.push({ handler: name, key, pages, produced, generated: reads.generated, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
8737
8747
|
}
|
|
8738
8748
|
}
|
|
8739
8749
|
return out;
|
|
@@ -8767,9 +8777,9 @@ function undeclaredCustomCalls(contract, { moduleSource = () => void 0 } = {}) {
|
|
|
8767
8777
|
}
|
|
8768
8778
|
function undeclaredCallMessage(id, module, handler, calls, how) {
|
|
8769
8779
|
const listed = calls.length === 0 ? "lists none" : `lists ${calls.join(", ")}`;
|
|
8770
|
-
const
|
|
8780
|
+
const problem6 = `custom "${id}" calls handler "${handler}" (ui.call ${how}, ui/${module}), and its declares.calls ${listed}.`;
|
|
8771
8781
|
const fix = `Add "${handler}" to declares.calls \u2014 and what it sends to declares.produces \u2014 or stop calling it.`;
|
|
8772
|
-
return { problem:
|
|
8782
|
+
return { problem: problem6, fix, message: `${problem6} ${fix}` };
|
|
8773
8783
|
}
|
|
8774
8784
|
function unreadUploadKeys(contract, { handlerSource = () => void 0 } = {}) {
|
|
8775
8785
|
const elements = isPlainObject2(contract?.elements) ? contract.elements : {};
|
|
@@ -8812,9 +8822,9 @@ function unreadUploadKeys(contract, { handlerSource = () => void 0 } = {}) {
|
|
|
8812
8822
|
}
|
|
8813
8823
|
if (read) continue;
|
|
8814
8824
|
const id = idOf2(element, uid);
|
|
8815
|
-
const
|
|
8825
|
+
const problem6 = `file_upload "${id}" writes the stored file under "${field}" to handler "${handler}" (${via}), and "${handler}" never reads payload["${field}"] \u2014 the file is stored and nothing uses it.`;
|
|
8816
8826
|
const fix = role === "custom" ? `Read payload["${field}"]["upload"] in "${handler}" and hand it to the parser (or write payload["${field}"] into a file column), or drop the upload.` : `Add "${field}" to "${handler}"'s fields (a file column), or drop the upload.`;
|
|
8817
|
-
out.push({ uid, id, page: element.page ?? null, field, handler, via, problem:
|
|
8827
|
+
out.push({ uid, id, page: element.page ?? null, field, handler, via, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
8818
8828
|
}
|
|
8819
8829
|
}
|
|
8820
8830
|
return out;
|
|
@@ -8830,9 +8840,9 @@ function stepsWithoutAction(contract) {
|
|
|
8830
8840
|
const id = idOf2(element, uid);
|
|
8831
8841
|
const name = named2(step.id) ? step.id : String(index);
|
|
8832
8842
|
const next = named2(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
|
|
8833
|
-
const
|
|
8843
|
+
const problem6 = `stepper "${id}" step "${name}" names no action, so no handler moves the flow on from it \u2014 a flow that reaches it stays there.`;
|
|
8834
8844
|
const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
|
|
8835
|
-
out.push({ uid, id, page: element.page ?? null, step: name, index, problem:
|
|
8845
|
+
out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
8836
8846
|
});
|
|
8837
8847
|
}
|
|
8838
8848
|
return out;
|
|
@@ -8905,7 +8915,7 @@ var MAX_HINT2 = 120;
|
|
|
8905
8915
|
function validate2(instance, schema, options = {}) {
|
|
8906
8916
|
const problems = [];
|
|
8907
8917
|
check2(instance, schema, "", schema, problems, { quote: options.quote === true, inBranch: false });
|
|
8908
|
-
return problems.map((
|
|
8918
|
+
return problems.map((problem6) => `${problem6.at}: ${problem6.why}`);
|
|
8909
8919
|
}
|
|
8910
8920
|
function problem2(at, why, kind = "other") {
|
|
8911
8921
|
return { at: at || "/", why, kind };
|
|
@@ -9318,9 +9328,9 @@ function unreachablePages(contract, files = {}) {
|
|
|
9318
9328
|
else if (roles.length > 0) why = `it is for ${both(quoted(roles))}, and no page ${either2(quoted(roles))} reaches links to it`;
|
|
9319
9329
|
else if (gates.length > 0) why = `every nav entry to it is gated to ${either2(quoted(gates))}, and no role shown that entry reaches a page carrying it`;
|
|
9320
9330
|
else why = "the only pages that link to it are pages no role reaches";
|
|
9321
|
-
const
|
|
9331
|
+
const problem6 = `${page.path} is a page of the app, and no role reaches it from the nav \u2014 ${why} \u2014 so only a typed URL opens it.`;
|
|
9322
9332
|
const fix = roles.length > 0 || gates.length > 0 ? `Gate its nav entry to a role that should open it (pages[].roles on the page, a role's home, or the element's role), or drop the page.` : "Give the pages a nav (a nav element on each page), or drop the page.";
|
|
9323
|
-
out.push({ page: page.path, problem:
|
|
9333
|
+
out.push({ page: page.path, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
9324
9334
|
}
|
|
9325
9335
|
return out;
|
|
9326
9336
|
}
|
|
@@ -9466,9 +9476,9 @@ function unconfirmedStateChanges(contract, files = {}) {
|
|
|
9466
9476
|
named_.push(one.module ? `${one.what} (on ${one.page})` : `${one.what} "${one.id}" (on ${one.page})`);
|
|
9467
9477
|
}
|
|
9468
9478
|
const kind = handlers[handler]?.role === "decide" ? "decides a row" : "changes a record's state";
|
|
9469
|
-
const
|
|
9479
|
+
const problem6 = `handler "${handler}" ${kind}, and ${both(named_)} ${named_.length === 1 ? "calls" : "call"} it on one click, with no confirm and no dialog in front of it.`;
|
|
9470
9480
|
const fix = 'Put the decision behind a confirm: a dialog part whose on_confirm is the handler (with the reason as one of its inputs), a row action with "confirm", a map-form decide whose decision takes a note, or \u2014 in a custom module \u2014 ui.confirm before ui.call.';
|
|
9471
|
-
out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem:
|
|
9481
|
+
out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
9472
9482
|
}
|
|
9473
9483
|
return out;
|
|
9474
9484
|
}
|
|
@@ -9546,18 +9556,18 @@ function unreadReasons(contract, { handlerSource = () => void 0, moduleSource, p
|
|
|
9546
9556
|
for (const [key, pages] of [...sent.entries()].sort()) {
|
|
9547
9557
|
if (read.has(key)) continue;
|
|
9548
9558
|
const where2 = pages.size === 0 ? "the page" : either2([...pages].sort());
|
|
9549
|
-
const
|
|
9559
|
+
const problem6 = `${where2} sends handler "${name}" a "${key}", and the handler never reads it \u2014 the reason a person types is thrown away.`;
|
|
9550
9560
|
const fix = `Read it (payload.get("${key}")) and keep it with the decision \u2014 a column, or the audit note \u2014 or stop asking for it.`;
|
|
9551
|
-
out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem:
|
|
9561
|
+
out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
9552
9562
|
}
|
|
9553
9563
|
const lines = pyLines(source);
|
|
9554
9564
|
if (!changesState(name, row, source) || !REJECTS.test(lines.join("\n"))) continue;
|
|
9555
9565
|
for (const key of REASON_KEYS) {
|
|
9556
9566
|
if (!reads.optional.includes(key) || reads.required.includes(key)) continue;
|
|
9557
9567
|
if (requiresReason(lines, key, reasonVars(lines, key))) continue;
|
|
9558
|
-
const
|
|
9568
|
+
const problem6 = `handler "${name}" rejects (or overrides), and reads "${key}" without ever requiring it \u2014 a rejection goes through with no reason given.`;
|
|
9559
9569
|
const fix = `Refuse the reject path without one: if decision == "rejected" and not ${key}: return {"error": "Give a reason to reject."}.`;
|
|
9560
|
-
out.push({ handler: name, key, kind: "optional", pages: [], problem:
|
|
9570
|
+
out.push({ handler: name, key, kind: "optional", pages: [], problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
9561
9571
|
}
|
|
9562
9572
|
}
|
|
9563
9573
|
return out;
|
|
@@ -9594,9 +9604,9 @@ function unliftableRefusals(contract, { handlerSource = () => void 0 } = {}) {
|
|
|
9594
9604
|
}
|
|
9595
9605
|
if (!refuses) continue;
|
|
9596
9606
|
const shown2 = line2.trim().replace(/:$/, "");
|
|
9597
|
-
const
|
|
9607
|
+
const problem6 = `handler "${name}" refuses outright at line ${si + 1} (${shown2}) on the same path where it demands a "${key}" (line ${gi + 1}), and no reason lifts that refusal \u2014 the page asks for a reason to go ahead, and the reason is ignored.`;
|
|
9598
9608
|
const fix = `Let the reason override it (check "${key}" before refusing, and record it), or tell the reader that this state cannot be overridden and stop asking for a reason.`;
|
|
9599
|
-
out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem:
|
|
9609
|
+
out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
9600
9610
|
}
|
|
9601
9611
|
}
|
|
9602
9612
|
}
|
|
@@ -9612,9 +9622,9 @@ function rolesLandingNowhere(contract) {
|
|
|
9612
9622
|
const exists = pagesOf(contract).some((page) => page.path === landing);
|
|
9613
9623
|
if (exists && opens(contract, model, role, landing)) continue;
|
|
9614
9624
|
const stated2 = model.home.get(role);
|
|
9615
|
-
const
|
|
9625
|
+
const problem6 = !exists ? `role "${role}" lands on ${landing} (its home), which is not a page of the app.` : stated2 ? `role "${role}" lands on its home, ${landing}, which is for ${both(quoted(pageRoles(pagesOf(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.` : `role "${role}" states no home and lands on ${landing}, which is for ${both(quoted(pageRoles(pagesOf(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.`;
|
|
9616
9626
|
const fix = `Give "${role}" a home it may open (backend.roles[].home: one of its pages), or add it to that page's pages[].roles.`;
|
|
9617
|
-
out.push({ role, page: landing, problem:
|
|
9627
|
+
out.push({ role, page: landing, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
9618
9628
|
}
|
|
9619
9629
|
return out;
|
|
9620
9630
|
}
|
|
@@ -9653,8 +9663,8 @@ function openToEveryRole(contract) {
|
|
|
9653
9663
|
const out = [];
|
|
9654
9664
|
const say = (kind, names, what, fix) => {
|
|
9655
9665
|
if (names.length === 0) return;
|
|
9656
|
-
const
|
|
9657
|
-
out.push({ kind, names, problem:
|
|
9666
|
+
const problem6 = `${names.length === 1 ? `${what.one} ${names[0]} states` : `${names.length} ${what.many} \u2014 ${both(names)} \u2014 state`} no roles, so every role (${all}) may ${what.verb} ${names.length === 1 ? "it" : "them"}.`;
|
|
9667
|
+
out.push({ kind, names, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
9658
9668
|
};
|
|
9659
9669
|
say(
|
|
9660
9670
|
"page",
|
|
@@ -10572,7 +10582,7 @@ function compile(contract, { kit, extra = {} } = {}) {
|
|
|
10572
10582
|
if (handler.unwritable !== null) return refuse2(handler.unwritable.code, handler.unwritable.where, handler.unwritable.message);
|
|
10573
10583
|
for (const note of handler.notes) problems.push(note);
|
|
10574
10584
|
if (handler.refusals.length > 0) {
|
|
10575
|
-
for (const
|
|
10585
|
+
for (const problem6 of handler.refusals) problems.push(problem6);
|
|
10576
10586
|
return { files: {}, problems: problems.sort(byProblem2) };
|
|
10577
10587
|
}
|
|
10578
10588
|
const files = {};
|
|
@@ -10605,7 +10615,7 @@ function dashboardHandler(composed, handlerName, extra = {}) {
|
|
|
10605
10615
|
out.notes.push(...missingExpressions(composed.data?.widgets));
|
|
10606
10616
|
const carried = [...transformRowProblems(composed.data), ...transformFileProblems(composed.data, extra)];
|
|
10607
10617
|
if (carried.length > 0) {
|
|
10608
|
-
out.refusals.push(...carried.map((
|
|
10618
|
+
out.refusals.push(...carried.map((problem6) => ({ ...problem6, lossy: false })));
|
|
10609
10619
|
return out;
|
|
10610
10620
|
}
|
|
10611
10621
|
const transforms = transformTexts(composed.data, extra);
|
|
@@ -11141,7 +11151,7 @@ function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt
|
|
|
11141
11151
|
}
|
|
11142
11152
|
problems.push(...handler.notes);
|
|
11143
11153
|
if (handler.refusals.length > 0) {
|
|
11144
|
-
for (const
|
|
11154
|
+
for (const problem6 of handler.refusals) refuse2(problem6.code, problem6.where, problem6.message);
|
|
11145
11155
|
return refused();
|
|
11146
11156
|
}
|
|
11147
11157
|
const restated = restateDashboard(composed, pages, handlerName, handler, { refuse: refuse2, note, stated: Array.isArray(source.pages) ? source.pages : [] });
|
|
@@ -11178,17 +11188,17 @@ function compileDashboard(contract, { kit = null, extra = {}, uiKit, charts, fmt
|
|
|
11178
11188
|
for (const page of whole.pages) files[page.path] = pageHtml(whole, page, elements, tree, shelf2);
|
|
11179
11189
|
return { files, problems, contract: whole, order: order2, warnings: narrativeOrderWarnings(composed, applied) };
|
|
11180
11190
|
}
|
|
11181
|
-
function isRefusal(
|
|
11182
|
-
return
|
|
11191
|
+
function isRefusal(problem6) {
|
|
11192
|
+
return problem6.lossy !== false;
|
|
11183
11193
|
}
|
|
11184
11194
|
function restateDashboard(composed, pages, handlerName, handler, { refuse: refuse2, note, stated: stated2 }) {
|
|
11185
11195
|
const app = isPlainObject7(composed.app) ? composed.app : {};
|
|
11186
11196
|
const typed = isPlainObject7(composed.elements) ? composed.elements : {};
|
|
11187
11197
|
const declared = treeOf(typed);
|
|
11188
|
-
const conflicts = new Map(declared.problems.map((
|
|
11198
|
+
const conflicts = new Map(declared.problems.map((problem6) => [uidOfPointer(problem6.pointer), problem6]));
|
|
11189
11199
|
const paths = new Set(pages.map((page) => page.path));
|
|
11190
11200
|
const unplaced = Object.fromEntries(Object.keys(typed).filter((uid) => isPlainObject7(typed[uid]) && !paths.has(typed[uid].page)).map((uid) => [uid, typed[uid]]));
|
|
11191
|
-
for (const
|
|
11201
|
+
for (const problem6 of placementProblems(unplaced, pages, declared, [])) refuse2(problem6.code, problem6.pointer, problem6.message);
|
|
11192
11202
|
const elements = {};
|
|
11193
11203
|
const outPages = [];
|
|
11194
11204
|
for (const page of pages) {
|
|
@@ -11996,8 +12006,8 @@ function timeModifierProblem(mod) {
|
|
|
11996
12006
|
return `has a malformed time modifier "${mod}" (expected .SHIFT(<unit>, <offset>) or .AS_OF(<anchor>))`;
|
|
11997
12007
|
}
|
|
11998
12008
|
function checkModifier(mod) {
|
|
11999
|
-
const
|
|
12000
|
-
if (
|
|
12009
|
+
const problem6 = timeModifierProblem(mod);
|
|
12010
|
+
if (problem6 !== null) refuse(problem6);
|
|
12001
12011
|
}
|
|
12002
12012
|
function namedItself(spelled) {
|
|
12003
12013
|
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>]`;
|
|
@@ -12144,6 +12154,27 @@ function metricFormulaProblems(formula, declared) {
|
|
|
12144
12154
|
return { problems: [error.message], operands: [], windows: [] };
|
|
12145
12155
|
}
|
|
12146
12156
|
}
|
|
12157
|
+
function wireFormula(formula, names) {
|
|
12158
|
+
const known = names !== null && typeof names === "object" ? names : {};
|
|
12159
|
+
const { padded, stash } = scan(formula, Object.keys(known));
|
|
12160
|
+
const nameOf5 = (key) => {
|
|
12161
|
+
const name = known[key];
|
|
12162
|
+
if (typeof name !== "string" || name.trim() === "" || /['[\]]/.test(name)) {
|
|
12163
|
+
refuse(`reads [@${key}], bound to a metric named ${JSON.stringify(name ?? null)}, which a CALC formula cannot reference (an operand's name is non-empty, with no ' [ or ])`);
|
|
12164
|
+
}
|
|
12165
|
+
return name;
|
|
12166
|
+
};
|
|
12167
|
+
const wire = padded.replace(STASH_RE, (_m, index) => {
|
|
12168
|
+
const entry = stash[Number(index)];
|
|
12169
|
+
if (entry.kind === "token") return `'${entry.token}'`;
|
|
12170
|
+
if (entry.kind === "window") return `${entry.fn}('${nameOf5(entry.key)}', ${entry.unit}, ${entry.periods})`;
|
|
12171
|
+
return `'${nameOf5(entry.key)}'${entry.mod}`;
|
|
12172
|
+
});
|
|
12173
|
+
if (wire.length > MAX_FORMULA_LENGTH) {
|
|
12174
|
+
refuse(`is ${wire.length} characters with the company's metric names written in, over the server maximum of ${MAX_FORMULA_LENGTH}; shorten the formula or the names`);
|
|
12175
|
+
}
|
|
12176
|
+
return wire;
|
|
12177
|
+
}
|
|
12147
12178
|
|
|
12148
12179
|
// ../flows/insights-builder/kit/contract/metricFormats.mjs
|
|
12149
12180
|
var CURRENCY_PREFIXES = ['"$"', "\u20AC", "\xA3", "\u20AA"];
|
|
@@ -12842,11 +12873,11 @@ function metricFilterProblem(filter) {
|
|
|
12842
12873
|
if (filter.exclude !== void 0 && typeof filter.exclude !== "boolean") return `states exclude ${JSON.stringify(filter.exclude)}; exclude is true or false`;
|
|
12843
12874
|
return null;
|
|
12844
12875
|
}
|
|
12845
|
-
function metricReaches(
|
|
12876
|
+
function metricReaches(operandsOf2, start, target, seen) {
|
|
12846
12877
|
if (start === target) return true;
|
|
12847
12878
|
if (seen.has(start)) return false;
|
|
12848
12879
|
seen.add(start);
|
|
12849
|
-
return (
|
|
12880
|
+
return (operandsOf2.get(start) ?? []).some((one) => metricReaches(operandsOf2, one, target, seen));
|
|
12850
12881
|
}
|
|
12851
12882
|
function calcShaped(node) {
|
|
12852
12883
|
if (node[0] === "num" || node[0] === "name") return true;
|
|
@@ -12975,12 +13006,12 @@ function metricProblems(metrics, sources) {
|
|
|
12975
13006
|
}
|
|
12976
13007
|
}
|
|
12977
13008
|
}
|
|
12978
|
-
const
|
|
13009
|
+
const operandsOf2 = /* @__PURE__ */ new Map();
|
|
12979
13010
|
for (const key of [...formulas.keys()].sort(natural3)) {
|
|
12980
13011
|
const { problems: wrong, operands, windows } = metricFormulaProblems(formulas.get(key), declaredKeys);
|
|
12981
13012
|
for (const one of wrong) say("metric_formula", at(key, "formula"), `metric "${key}"'s formula ${one}`);
|
|
12982
13013
|
if (wrong.length > 0) continue;
|
|
12983
|
-
|
|
13014
|
+
operandsOf2.set(key, operands);
|
|
12984
13015
|
if (windows.length > 0) {
|
|
12985
13016
|
advise(
|
|
12986
13017
|
"metric_window",
|
|
@@ -12989,9 +13020,9 @@ function metricProblems(metrics, sources) {
|
|
|
12989
13020
|
);
|
|
12990
13021
|
}
|
|
12991
13022
|
}
|
|
12992
|
-
for (const key of [...
|
|
12993
|
-
for (const dep of
|
|
12994
|
-
if (metricReaches(
|
|
13023
|
+
for (const key of [...operandsOf2.keys()].sort(natural3)) {
|
|
13024
|
+
for (const dep of operandsOf2.get(key)) {
|
|
13025
|
+
if (metricReaches(operandsOf2, dep, key, /* @__PURE__ */ new Set())) {
|
|
12995
13026
|
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
13027
|
break;
|
|
12997
13028
|
}
|
|
@@ -13034,7 +13065,7 @@ function metricProblems(metrics, sources) {
|
|
|
13034
13065
|
const visit = (key) => {
|
|
13035
13066
|
if (used.has(key)) return;
|
|
13036
13067
|
used.add(key);
|
|
13037
|
-
for (const one of
|
|
13068
|
+
for (const one of operandsOf2.get(key) ?? []) visit(one);
|
|
13038
13069
|
};
|
|
13039
13070
|
for (const key of read) visit(key);
|
|
13040
13071
|
for (const key of declaredKeys) {
|
|
@@ -13449,8 +13480,8 @@ function paramProblems(contract, sources) {
|
|
|
13449
13480
|
}
|
|
13450
13481
|
function numberFix(value, integer = false) {
|
|
13451
13482
|
const text3 = typeof value === "string" ? value.trim() : null;
|
|
13452
|
-
const
|
|
13453
|
-
if (Number.isFinite(
|
|
13483
|
+
const parsed2 = text3 !== null && text3 !== "" ? Number(text3) : NaN;
|
|
13484
|
+
if (Number.isFinite(parsed2) && (!integer || Number.isInteger(parsed2))) return `write it as the number ${parsed2}, unquoted`;
|
|
13454
13485
|
return `write ${integer ? "a whole number" : "a number"}, or leave the key out`;
|
|
13455
13486
|
}
|
|
13456
13487
|
function uidHint(value, why, elements) {
|
|
@@ -16466,7 +16497,7 @@ function measureProblems(name, row, model, handlers = {}) {
|
|
|
16466
16497
|
const found = whereProblems(spec.where, EVENTS_SHAPE, at, who, {
|
|
16467
16498
|
noColumn: (column) => `filters on "${column}", which is no column of the events; filter on at, action, handler, actor or note`
|
|
16468
16499
|
});
|
|
16469
|
-
problems.push(...found, ...eventHandlerProblems(spec.where, row.table, handlers, at, who, new Set(found.map((
|
|
16500
|
+
problems.push(...found, ...eventHandlerProblems(spec.where, row.table, handlers, at, who, new Set(found.map((problem6) => problem6.pointer))));
|
|
16470
16501
|
} else problems.push(...whereProblems(spec.where, table, at, who, { platform: true }));
|
|
16471
16502
|
}
|
|
16472
16503
|
}
|
|
@@ -17722,6 +17753,7 @@ function validateContract(contract, catalog) {
|
|
|
17722
17753
|
checkKind(app, problems);
|
|
17723
17754
|
checkPreset(app, problems);
|
|
17724
17755
|
if (kind === "dashboard") checkSources(doc, catalog, problems, skipped);
|
|
17756
|
+
checkSourceMetricIds(doc, catalog, problems, skipped);
|
|
17725
17757
|
checkVocabulary(doc, kind, problems, renderer);
|
|
17726
17758
|
checkPages(doc, kind, problems);
|
|
17727
17759
|
if (kind === "dashboard") {
|
|
@@ -17744,22 +17776,22 @@ function validateContract(contract, catalog) {
|
|
|
17744
17776
|
return { problems: dedupe(problems), skipped: dedupeSkipped(skipped), warnings: dedupe(warnings) };
|
|
17745
17777
|
}
|
|
17746
17778
|
function checkCustomDeclares(doc, problems) {
|
|
17747
|
-
for (const
|
|
17779
|
+
for (const problem6 of declaresProblems2(doc)) {
|
|
17748
17780
|
problems.push({
|
|
17749
|
-
code:
|
|
17750
|
-
pointer:
|
|
17751
|
-
message: `${
|
|
17781
|
+
code: problem6.code,
|
|
17782
|
+
pointer: problem6.pointer,
|
|
17783
|
+
message: `${problem6.message}.`,
|
|
17752
17784
|
fix: 'Name only data.sources entries and data.app.handlers in declares \u2014 {"reads": [...], "calls": [...], "produces": {"<handler>": ["<key>", ...]}, "shows": [{"field": ..., "format": ...}]} (docs/appspace/components/catalog.md, custom).'
|
|
17753
17785
|
});
|
|
17754
17786
|
}
|
|
17755
17787
|
}
|
|
17756
17788
|
function checkTransforms(doc, problems) {
|
|
17757
|
-
for (const
|
|
17758
|
-
const row =
|
|
17789
|
+
for (const problem6 of transformRowProblems2(obj(doc["data"]))) {
|
|
17790
|
+
const row = problem6.code === "transform_row";
|
|
17759
17791
|
problems.push({
|
|
17760
17792
|
code: row ? "transform-row" : "transform-missing",
|
|
17761
|
-
pointer:
|
|
17762
|
-
message: `${
|
|
17793
|
+
pointer: problem6.where,
|
|
17794
|
+
message: `${problem6.message}.`,
|
|
17763
17795
|
fix: row ? "Make each change the message names, on this row and its dataset's read; a KPI, a plain read or a computed card takes no transform in this release." : "Name the file transforms/<name>.py \u2014 lowercase letters, digits and underscores, at most 64 of them, with the worker prefix (transforms/w3_revenue_ma.py)."
|
|
17764
17796
|
});
|
|
17765
17797
|
}
|
|
@@ -18059,6 +18091,30 @@ function checkPreset(app, problems) {
|
|
|
18059
18091
|
fix: `Name one of ${PRESETS2.join(", ")}, or drop the key and take the default.`
|
|
18060
18092
|
});
|
|
18061
18093
|
}
|
|
18094
|
+
function checkSourceMetricIds(doc, catalog, problems, skipped) {
|
|
18095
|
+
const sources = obj(obj(doc["data"])["sources"]);
|
|
18096
|
+
const byId = [];
|
|
18097
|
+
for (const [name, row] of Object.entries(sources)) {
|
|
18098
|
+
const id = obj(obj(obj(row)["from"])["financeos"])["metric_id"];
|
|
18099
|
+
if (typeof id === "number" || typeof id === "string" && id.trim() !== "") byId.push([name, String(id).trim()]);
|
|
18100
|
+
}
|
|
18101
|
+
if (byId.length === 0) return;
|
|
18102
|
+
if (!checksMetrics(catalog)) {
|
|
18103
|
+
const why = catalog.source === "none" ? "no --catalog file and no -s server to read the metric list from" : catalog.checks_skipped.find((one) => one.what === "metric ids")?.why ?? "this tenant's metric list could not be read";
|
|
18104
|
+
if (!skipped.some((one) => one.what === "source metric ids")) skipped.push({ what: "source metric ids", why });
|
|
18105
|
+
return;
|
|
18106
|
+
}
|
|
18107
|
+
const known = new Set(catalog.metrics.map((metric) => metric.id));
|
|
18108
|
+
for (const [name, id] of byId) {
|
|
18109
|
+
if (known.has(id)) continue;
|
|
18110
|
+
problems.push({
|
|
18111
|
+
code: "unknown-metric",
|
|
18112
|
+
pointer: pointer("data", "sources", name, "from", "financeos", "metric_id"),
|
|
18113
|
+
message: `${id} is not a metric this company has.`,
|
|
18114
|
+
fix: `Use an id from this company's metric list, or declare the metric in data.metrics and read it as "@<key>", which ship binds in each company.`
|
|
18115
|
+
});
|
|
18116
|
+
}
|
|
18117
|
+
}
|
|
18062
18118
|
function checkSources(doc, catalog, problems, skipped) {
|
|
18063
18119
|
const widgets = obj(obj(doc["data"])["widgets"]);
|
|
18064
18120
|
if (Object.keys(widgets).length === 0) return;
|
|
@@ -18300,9 +18356,9 @@ function calendarDateIn(filter) {
|
|
|
18300
18356
|
}
|
|
18301
18357
|
function epochOf(date) {
|
|
18302
18358
|
const day = date.slice(0, 10);
|
|
18303
|
-
const
|
|
18304
|
-
if (Number.isNaN(
|
|
18305
|
-
return String(Math.floor(
|
|
18359
|
+
const parsed2 = Date.parse(`${day}T00:00:00Z`);
|
|
18360
|
+
if (Number.isNaN(parsed2)) return void 0;
|
|
18361
|
+
return String(Math.floor(parsed2 / 1e3));
|
|
18306
18362
|
}
|
|
18307
18363
|
function addressOf(source, hasAlias, hasId) {
|
|
18308
18364
|
const declared = source["address"];
|
|
@@ -19264,11 +19320,11 @@ function walk3(node, path3, visit) {
|
|
|
19264
19320
|
function dedupe(problems) {
|
|
19265
19321
|
const seen = /* @__PURE__ */ new Set();
|
|
19266
19322
|
const out = [];
|
|
19267
|
-
for (const
|
|
19268
|
-
const key = `${
|
|
19323
|
+
for (const problem6 of problems) {
|
|
19324
|
+
const key = `${problem6.code}\0${problem6.pointer}\0${problem6.message}`;
|
|
19269
19325
|
if (seen.has(key)) continue;
|
|
19270
19326
|
seen.add(key);
|
|
19271
|
-
out.push(
|
|
19327
|
+
out.push(problem6);
|
|
19272
19328
|
}
|
|
19273
19329
|
return out;
|
|
19274
19330
|
}
|
|
@@ -20580,16 +20636,16 @@ async function readManifest(dir) {
|
|
|
20580
20636
|
if (raw === void 0) {
|
|
20581
20637
|
throw new Error(`no manifest.json in ${dir} \u2014 is this an app folder?`);
|
|
20582
20638
|
}
|
|
20583
|
-
let
|
|
20639
|
+
let parsed2;
|
|
20584
20640
|
try {
|
|
20585
|
-
|
|
20641
|
+
parsed2 = JSON.parse(raw);
|
|
20586
20642
|
} catch {
|
|
20587
20643
|
throw new Error(`manifest.json in ${dir} is not valid JSON.`);
|
|
20588
20644
|
}
|
|
20589
|
-
if (typeof
|
|
20645
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) {
|
|
20590
20646
|
throw new Error("manifest.json is not a JSON object.");
|
|
20591
20647
|
}
|
|
20592
|
-
const manifest =
|
|
20648
|
+
const manifest = parsed2;
|
|
20593
20649
|
const id = manifest["id"];
|
|
20594
20650
|
if (typeof id !== "string" || id === "") {
|
|
20595
20651
|
throw new Error('manifest.json has no "id".');
|
|
@@ -20603,9 +20659,9 @@ async function readState(dir) {
|
|
|
20603
20659
|
const raw = await readIfPresent(join5(dir, ".appspace.json"));
|
|
20604
20660
|
if (raw === void 0) return {};
|
|
20605
20661
|
try {
|
|
20606
|
-
const
|
|
20607
|
-
if (typeof
|
|
20608
|
-
return
|
|
20662
|
+
const parsed2 = JSON.parse(raw);
|
|
20663
|
+
if (typeof parsed2 === "object" && parsed2 !== null) {
|
|
20664
|
+
return parsed2;
|
|
20609
20665
|
}
|
|
20610
20666
|
} catch {
|
|
20611
20667
|
}
|
|
@@ -20722,8 +20778,8 @@ function summarise(contract) {
|
|
|
20722
20778
|
}
|
|
20723
20779
|
function describeFindings(contract) {
|
|
20724
20780
|
let out = "";
|
|
20725
|
-
for (const
|
|
20726
|
-
out += ` ${
|
|
20781
|
+
for (const problem6 of contract.problems) {
|
|
20782
|
+
out += ` ${problem6.lossy ? "lossy" : "ok "} ${problem6.code} at ${problem6.where}: ${problem6.message}
|
|
20727
20783
|
`;
|
|
20728
20784
|
}
|
|
20729
20785
|
for (const entry of contract.unresolved) {
|
|
@@ -21636,6 +21692,1082 @@ async function previewOne(document, file, opts, shared = {}) {
|
|
|
21636
21692
|
import { readFile as readFile7 } from "fs/promises";
|
|
21637
21693
|
import { join as join8 } from "path";
|
|
21638
21694
|
import { gunzipSync, gzipSync } from "zlib";
|
|
21695
|
+
|
|
21696
|
+
// src/scopes/matcher.ts
|
|
21697
|
+
var SCOPE_RE = /^(\*|[a-z][a-z0-9-]*):(\*|read|write|destructive)$/;
|
|
21698
|
+
var ACTION_RANK = new Map(
|
|
21699
|
+
["read", "write", "destructive"].map((action, rank) => [action, rank])
|
|
21700
|
+
);
|
|
21701
|
+
function actionSatisfies(granted, wanted) {
|
|
21702
|
+
if (granted === "*" || granted === wanted) return true;
|
|
21703
|
+
const g = ACTION_RANK.get(granted);
|
|
21704
|
+
const w = ACTION_RANK.get(wanted);
|
|
21705
|
+
if (g === void 0 || w === void 0) return false;
|
|
21706
|
+
return g > w;
|
|
21707
|
+
}
|
|
21708
|
+
var WHITESPACE = "\\t\\n\\v\\f\\r \\x1c-\\x1f\\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff";
|
|
21709
|
+
var TRIM_RE = new RegExp(`^[${WHITESPACE}]+|[${WHITESPACE}]+$`, "g");
|
|
21710
|
+
var SPLIT_RE = new RegExp(`[${WHITESPACE}]+`);
|
|
21711
|
+
function trimShared(value) {
|
|
21712
|
+
return value.replace(TRIM_RE, "");
|
|
21713
|
+
}
|
|
21714
|
+
function splitScopeString(value) {
|
|
21715
|
+
return value.split(SPLIT_RE).filter((part) => part !== "");
|
|
21716
|
+
}
|
|
21717
|
+
function parseScope(value) {
|
|
21718
|
+
if (typeof value !== "string") return null;
|
|
21719
|
+
const m = SCOPE_RE.exec(trimShared(value).toLowerCase());
|
|
21720
|
+
return m ? [m[1], m[2]] : null;
|
|
21721
|
+
}
|
|
21722
|
+
function parsed(scopes) {
|
|
21723
|
+
const list3 = scopes ?? [];
|
|
21724
|
+
const entries = typeof list3 === "string" ? splitScopeString(list3) : Array.isArray(list3) ? list3 : [];
|
|
21725
|
+
return entries.map(parseScope).filter((p) => p !== null);
|
|
21726
|
+
}
|
|
21727
|
+
function scopeEntries(scopes) {
|
|
21728
|
+
return parsed(scopes);
|
|
21729
|
+
}
|
|
21730
|
+
function isAgentScoped(scopes) {
|
|
21731
|
+
return parsed(scopes).length > 0;
|
|
21732
|
+
}
|
|
21733
|
+
function isAllowed(scopes, resource, action) {
|
|
21734
|
+
const entries = parsed(scopes);
|
|
21735
|
+
if (entries.length === 0) return true;
|
|
21736
|
+
const res = trimShared(resource).toLowerCase();
|
|
21737
|
+
const act = trimShared(action).toLowerCase();
|
|
21738
|
+
return entries.some(([r, a]) => (r === "*" || r === res) && actionSatisfies(a, act));
|
|
21739
|
+
}
|
|
21740
|
+
|
|
21741
|
+
// src/commands/appspace/metrics.ts
|
|
21742
|
+
function metricsTransport(client) {
|
|
21743
|
+
const host = (client.host ?? client.baseUrl.replace(/\/api\/app-space$/, "")).replace(/\/+$/, "");
|
|
21744
|
+
return {
|
|
21745
|
+
serverKey: client.serverKey,
|
|
21746
|
+
host,
|
|
21747
|
+
appSpace: client.baseUrl,
|
|
21748
|
+
overridden: client.baseUrl !== `${host}/api/app-space`,
|
|
21749
|
+
email: client.email ?? null,
|
|
21750
|
+
async call(method, url, body) {
|
|
21751
|
+
const headers = {};
|
|
21752
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
21753
|
+
const res = await client.raw({
|
|
21754
|
+
method,
|
|
21755
|
+
url,
|
|
21756
|
+
headers,
|
|
21757
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
21758
|
+
});
|
|
21759
|
+
const text3 = res.body.toString("utf8");
|
|
21760
|
+
let parsed2 = text3 === "" ? null : text3;
|
|
21761
|
+
try {
|
|
21762
|
+
parsed2 = text3 === "" ? null : JSON.parse(text3);
|
|
21763
|
+
} catch {
|
|
21764
|
+
}
|
|
21765
|
+
return { status: res.status, body: parsed2 };
|
|
21766
|
+
},
|
|
21767
|
+
claims: () => client.claims === void 0 ? null : client.claims()
|
|
21768
|
+
};
|
|
21769
|
+
}
|
|
21770
|
+
function declaredMetrics(contract) {
|
|
21771
|
+
const data = obj2(obj2(contract)["data"]);
|
|
21772
|
+
const metrics = obj2(data["metrics"]);
|
|
21773
|
+
const out = {};
|
|
21774
|
+
for (const [key, decl] of Object.entries(metrics)) {
|
|
21775
|
+
if (isObj3(decl) && typeof decl["name"] === "string") out[key] = decl;
|
|
21776
|
+
}
|
|
21777
|
+
return out;
|
|
21778
|
+
}
|
|
21779
|
+
function metricSources(contract) {
|
|
21780
|
+
const sources = obj2(obj2(obj2(contract)["data"])["sources"]);
|
|
21781
|
+
const out = {};
|
|
21782
|
+
for (const [name, row] of Object.entries(sources)) {
|
|
21783
|
+
const metric = obj2(obj2(obj2(row)["from"])["financeos"])["metric"];
|
|
21784
|
+
if (typeof metric === "string" && metric.startsWith("@") && metric.length > 1) out[name] = metric.slice(1);
|
|
21785
|
+
}
|
|
21786
|
+
return out;
|
|
21787
|
+
}
|
|
21788
|
+
var ENTITY_RE2 = /&(quot|amp|lt|gt|#x27|#39);/g;
|
|
21789
|
+
var ENTITIES = { quot: '"', amp: "&", lt: "<", gt: ">", "#x27": "'", "#39": "'" };
|
|
21790
|
+
function decodeEntities2(value) {
|
|
21791
|
+
if (typeof value === "string") return value.replace(ENTITY_RE2, (_m, name) => ENTITIES[name] ?? "");
|
|
21792
|
+
if (Array.isArray(value)) return value.map(decodeEntities2);
|
|
21793
|
+
if (isObj3(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, decodeEntities2(v)]));
|
|
21794
|
+
return value;
|
|
21795
|
+
}
|
|
21796
|
+
function listOf(body) {
|
|
21797
|
+
const rows2 = Array.isArray(body) ? body : isObj3(body) && Array.isArray(body["results"]) ? body["results"] : null;
|
|
21798
|
+
return rows2 === null ? [] : rows2.filter(isObj3);
|
|
21799
|
+
}
|
|
21800
|
+
function idIn(value) {
|
|
21801
|
+
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
21802
|
+
if (typeof value === "string" && /^\d+$/.test(value)) return Number(value);
|
|
21803
|
+
if (isObj3(value)) return idIn(value["id"]);
|
|
21804
|
+
return null;
|
|
21805
|
+
}
|
|
21806
|
+
function catalogRow(raw) {
|
|
21807
|
+
const id = idIn(raw["id"]);
|
|
21808
|
+
const name = typeof raw["name"] === "string" ? raw["name"] : null;
|
|
21809
|
+
if (id === null || name === null) return null;
|
|
21810
|
+
const configuration = obj2(raw["configuration"]);
|
|
21811
|
+
const status = obj2(raw["status_info"]);
|
|
21812
|
+
const filters = (Array.isArray(raw["filters"]) ? raw["filters"] : []).filter(isObj3).flatMap((one) => {
|
|
21813
|
+
const field = idIn(one["field"]);
|
|
21814
|
+
if (field === null) return [];
|
|
21815
|
+
const values = Array.isArray(one["values"]) ? one["values"].map(String) : one["values"] === void 0 || one["values"] === null ? [] : [String(one["values"])];
|
|
21816
|
+
return [{ field, values, is_excluded: one["is_excluded"] === true, allow_nulls: one["allow_nulls"] === true }];
|
|
21817
|
+
});
|
|
21818
|
+
return {
|
|
21819
|
+
id,
|
|
21820
|
+
name,
|
|
21821
|
+
kind: typeof raw["kind"] === "string" ? raw["kind"].toUpperCase() : "",
|
|
21822
|
+
template: idIn(raw["template"] ?? raw["template_id"]),
|
|
21823
|
+
value_field: idIn(raw["value_field"]),
|
|
21824
|
+
date_field: idIn(raw["date_field"]),
|
|
21825
|
+
aggregator: typeof raw["aggregator"] === "string" ? raw["aggregator"].toUpperCase() : idIn(raw["aggregator"]) === null && isObj3(raw["aggregator"]) && typeof raw["aggregator"]["name"] === "string" ? String(raw["aggregator"]["name"]).toUpperCase() : null,
|
|
21826
|
+
is_cumulative: raw["is_cumulative"] === true,
|
|
21827
|
+
time_aggregation: typeof raw["time_aggregation"] === "string" ? raw["time_aggregation"].toUpperCase() : null,
|
|
21828
|
+
filters,
|
|
21829
|
+
formula: typeof configuration["presented_formula"] === "string" && configuration["presented_formula"] !== "" ? configuration["presented_formula"] : null,
|
|
21830
|
+
format: typeof configuration["format"] === "string" ? configuration["format"] : null,
|
|
21831
|
+
base_metrics: (Array.isArray(raw["base_metrics"]) ? raw["base_metrics"] : []).map(idIn).filter((one) => one !== null),
|
|
21832
|
+
description: typeof raw["description"] === "string" ? raw["description"] : null,
|
|
21833
|
+
status: typeof status["status"] === "string" ? status["status"].toUpperCase() : null,
|
|
21834
|
+
created_at: typeof raw["created_at"] === "string" ? raw["created_at"] : null
|
|
21835
|
+
};
|
|
21836
|
+
}
|
|
21837
|
+
function authTableRow(raw) {
|
|
21838
|
+
const id = idIn(raw["id"]);
|
|
21839
|
+
if (id === null) return null;
|
|
21840
|
+
const fields = Array.isArray(raw["fields"]) ? raw["fields"].filter(isObj3).map(
|
|
21841
|
+
(one) => ({
|
|
21842
|
+
id: idIn(one["id"]),
|
|
21843
|
+
name: typeof one["name"] === "string" ? one["name"] : "",
|
|
21844
|
+
alias: typeof one["alias"] === "string" && one["alias"].trim() !== "" ? one["alias"].trim() : null
|
|
21845
|
+
})
|
|
21846
|
+
) : null;
|
|
21847
|
+
return {
|
|
21848
|
+
id,
|
|
21849
|
+
name: typeof raw["name"] === "string" ? raw["name"] : String(id),
|
|
21850
|
+
alias: typeof raw["alias"] === "string" && raw["alias"].trim() !== "" ? raw["alias"].trim() : null,
|
|
21851
|
+
blocked: raw["authoring_blocked"] === true,
|
|
21852
|
+
fields: fields !== null && fields.length > 0 ? fields : null
|
|
21853
|
+
};
|
|
21854
|
+
}
|
|
21855
|
+
var MetricsReadError = class extends Error {
|
|
21856
|
+
};
|
|
21857
|
+
async function readJson2(t, url, what) {
|
|
21858
|
+
let answer;
|
|
21859
|
+
try {
|
|
21860
|
+
answer = await t.call("GET", url);
|
|
21861
|
+
} catch (err) {
|
|
21862
|
+
throw new MetricsReadError(`${what} could not be read (${firstLine(err)})`);
|
|
21863
|
+
}
|
|
21864
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
21865
|
+
throw new MetricsReadError(`${what} could not be read (HTTP ${String(answer.status)}${sentenceOf(answer.body) === "" ? "" : `: ${sentenceOf(answer.body)}`})`);
|
|
21866
|
+
}
|
|
21867
|
+
return decodeEntities2(answer.body);
|
|
21868
|
+
}
|
|
21869
|
+
async function readCatalog(t) {
|
|
21870
|
+
const body = await readJson2(t, `${t.host}/api/insights/metrics`, "this company's Datarails metrics");
|
|
21871
|
+
return listOf(body).map(catalogRow).filter((one) => one !== null);
|
|
21872
|
+
}
|
|
21873
|
+
async function readAuthTables(t) {
|
|
21874
|
+
const body = await readJson2(t, `${t.host}/api/insights/metrics/auth_tables`, "the tables you may build metrics on");
|
|
21875
|
+
return listOf(body).map(authTableRow).filter((one) => one !== null);
|
|
21876
|
+
}
|
|
21877
|
+
async function readTableFields(t, id) {
|
|
21878
|
+
const body = obj2(await readJson2(t, `${t.appSpace}/financeos/tables/${String(id)}`, `the fields of table ${String(id)}`));
|
|
21879
|
+
if (body["reachable"] !== true) throw new MetricsReadError(`the fields of table ${String(id)} could not be read (FinanceOS did not answer)`);
|
|
21880
|
+
return (Array.isArray(body["fields"]) ? body["fields"] : []).filter(isObj3).map(
|
|
21881
|
+
(one) => ({
|
|
21882
|
+
id: idIn(one["id"]),
|
|
21883
|
+
name: typeof one["name"] === "string" ? one["name"] : "",
|
|
21884
|
+
alias: typeof one["alias"] === "string" && one["alias"].trim() !== "" ? one["alias"].trim() : null
|
|
21885
|
+
})
|
|
21886
|
+
);
|
|
21887
|
+
}
|
|
21888
|
+
var AGGREGATES = {
|
|
21889
|
+
sum: ["SUM", false],
|
|
21890
|
+
count: ["COUNT", false],
|
|
21891
|
+
cumulative_sum: ["SUM", true],
|
|
21892
|
+
cumulative_count: ["COUNT", true]
|
|
21893
|
+
};
|
|
21894
|
+
function baseReuseKey(def) {
|
|
21895
|
+
const time = rollupOf(def);
|
|
21896
|
+
const filters = def.filters.map((one) => [one.field, [...one.values].map(String).sort(), one.is_excluded, one.allow_nulls]).sort((a, b) => a[0] - b[0] || JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
21897
|
+
return `base:${JSON.stringify([def.template, def.value_field, def.date_field, def.aggregator.toUpperCase(), time, def.is_cumulative, filters])}`;
|
|
21898
|
+
}
|
|
21899
|
+
function rollupOf(def) {
|
|
21900
|
+
return (def.time_aggregation ?? (def.is_cumulative ? "EOP" : "SUM")).toUpperCase();
|
|
21901
|
+
}
|
|
21902
|
+
var OPERAND_RE = /'([^']*)'|\[([^[\]]*)\]/g;
|
|
21903
|
+
var MARK = "";
|
|
21904
|
+
function calcSkeleton(formula, idOf4) {
|
|
21905
|
+
let missing = false;
|
|
21906
|
+
const ids = /* @__PURE__ */ new Set();
|
|
21907
|
+
const marked2 = formula.replace(OPERAND_RE, (_m, quoted3, bracketed) => {
|
|
21908
|
+
const name = quoted3 ?? bracketed ?? "";
|
|
21909
|
+
if (SYSTEM_VARIABLES.includes(name)) return `${MARK}${name.toLowerCase()}${MARK}`;
|
|
21910
|
+
const id = idOf4(name);
|
|
21911
|
+
if (id === null) {
|
|
21912
|
+
missing = true;
|
|
21913
|
+
return MARK + MARK;
|
|
21914
|
+
}
|
|
21915
|
+
ids.add(id);
|
|
21916
|
+
return `${MARK}#${String(id)}${MARK}`;
|
|
21917
|
+
});
|
|
21918
|
+
if (missing) return null;
|
|
21919
|
+
const skeleton = marked2.split(MARK).map((part, index) => index % 2 === 1 ? part : part.toLowerCase().replace(/\s+/g, "")).join("");
|
|
21920
|
+
return { skeleton, ids: [...ids].sort((a, b) => a - b) };
|
|
21921
|
+
}
|
|
21922
|
+
function calcReuseKey(formula, idOf4) {
|
|
21923
|
+
const shape = calcSkeleton(formula, idOf4);
|
|
21924
|
+
return shape === null ? null : `calc:${shape.skeleton}|${shape.ids.join(",")}`;
|
|
21925
|
+
}
|
|
21926
|
+
function rowReuseKey(row, byName) {
|
|
21927
|
+
if (row.kind === "BASE") {
|
|
21928
|
+
if (row.template === null || row.value_field === null || row.date_field === null || row.aggregator === null) return null;
|
|
21929
|
+
return baseReuseKey({
|
|
21930
|
+
template: row.template,
|
|
21931
|
+
value_field: row.value_field,
|
|
21932
|
+
date_field: row.date_field,
|
|
21933
|
+
aggregator: row.aggregator,
|
|
21934
|
+
is_cumulative: row.is_cumulative,
|
|
21935
|
+
time_aggregation: row.time_aggregation,
|
|
21936
|
+
filters: row.filters
|
|
21937
|
+
});
|
|
21938
|
+
}
|
|
21939
|
+
if (row.kind === "CALC" && row.formula !== null) return calcReuseKey(row.formula, (name) => byName.get(name)?.id ?? null);
|
|
21940
|
+
return null;
|
|
21941
|
+
}
|
|
21942
|
+
var METRIC_CREATED_FOOTER = "Created by App Space for the app";
|
|
21943
|
+
var METRIC_CREATED_CATEGORY = "App Space";
|
|
21944
|
+
function createdFooter(appName) {
|
|
21945
|
+
return `${METRIC_CREATED_FOOTER} "${appName === "" ? "this app" : appName}".`;
|
|
21946
|
+
}
|
|
21947
|
+
var METRIC_SHIP_REFUSALS = {
|
|
21948
|
+
metric_name_taken: 'this company already has a metric of that name with a different definition, which App Space may not change in place (it did not create it through you, another app or key reads it, or the published version does) \u2014 use theirs with "reuse": "name", or name yours differently',
|
|
21949
|
+
metric_missing: 'a declaration with "create": false names a metric this company does not have',
|
|
21950
|
+
metric_scope: "creating or changing a metric needs the Datarails scope metrics:write, and this session's credential ceiling does not allow it",
|
|
21951
|
+
metric_table: "a BASE names a table you cannot build metrics on: not one of yours, or a permission filter applies to you (authoring blocked)",
|
|
21952
|
+
metric_field: "a BASE names a field its table does not have, or one with no numeric id",
|
|
21953
|
+
metric_preview: "Datarails could not evaluate a CALC's formula (preview_calc_data) \u2014 its own sentence",
|
|
21954
|
+
metric_operand: "a CALC reads a declaration this ship could not bind",
|
|
21955
|
+
metric_unchecked: "this company's metrics could not be read, so nothing could be bound: ship again shortly",
|
|
21956
|
+
metric_create: "Datarails refused to create or change a metric after the checks passed; the metrics already created are kept, and the next ship reuses them",
|
|
21957
|
+
metric_changed: "a metric this ship created or changed, read back from Datarails before the push, does not hold the definition it wrote (another ship or person changed it meanwhile, or Datarails did not take the whole change); nothing is pushed, and the metrics are kept"
|
|
21958
|
+
};
|
|
21959
|
+
var FIXES = {
|
|
21960
|
+
metric_name_taken: `Use theirs with "reuse": "name", or give the contract's metric another name.`,
|
|
21961
|
+
metric_missing: 'Create the metric in Datarails, or give the declaration a definition and drop "create": false.',
|
|
21962
|
+
metric_scope: `Ask the agent's owner to add metrics:write to its CLI scope ceiling, or create the metric in Datarails and declare it with "reuse": "name".`,
|
|
21963
|
+
metric_table: "Ask an admin for access to the table, or build the metric on a table you can author on.",
|
|
21964
|
+
metric_field: "Name a field of that table by its alias or its name.",
|
|
21965
|
+
metric_preview: "Fix the formula so Datarails can evaluate it.",
|
|
21966
|
+
metric_operand: "Fix the declaration it reads first.",
|
|
21967
|
+
metric_unchecked: "Ship again shortly; nothing was created.",
|
|
21968
|
+
metric_create: "Fix what it names and ship again; the metrics already created are kept, and the next ship reuses them.",
|
|
21969
|
+
metric_changed: "Ship again: the next plan starts from what Datarails holds now. If it happens again, someone else is changing this metric; check it in Datarails."
|
|
21970
|
+
};
|
|
21971
|
+
function problem4(code, key, message) {
|
|
21972
|
+
return {
|
|
21973
|
+
code,
|
|
21974
|
+
pointer: key === null ? pointer("data", "metrics") : pointer("data", "metrics", key),
|
|
21975
|
+
message: message.endsWith(".") ? message : `${message}.`,
|
|
21976
|
+
fix: FIXES[code] ?? "Change data.metrics so this company can bind it."
|
|
21977
|
+
};
|
|
21978
|
+
}
|
|
21979
|
+
function dependencyOrder(decls) {
|
|
21980
|
+
const keys2 = Object.keys(decls).sort();
|
|
21981
|
+
const out = [];
|
|
21982
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21983
|
+
const visit = (key, path3) => {
|
|
21984
|
+
if (seen.has(key) || path3.has(key)) return;
|
|
21985
|
+
path3.add(key);
|
|
21986
|
+
for (const dep of operandsOf(decls, key)) if (dep in decls) visit(dep, path3);
|
|
21987
|
+
path3.delete(key);
|
|
21988
|
+
seen.add(key);
|
|
21989
|
+
out.push(key);
|
|
21990
|
+
};
|
|
21991
|
+
for (const key of keys2) visit(key, /* @__PURE__ */ new Set());
|
|
21992
|
+
return out;
|
|
21993
|
+
}
|
|
21994
|
+
function operandsOf(decls, key) {
|
|
21995
|
+
const decl = decls[key];
|
|
21996
|
+
if (decl === void 0 || decl.kind !== "calc" || typeof decl.formula !== "string") return [];
|
|
21997
|
+
const found = metricFormulaProblems(decl.formula, Object.keys(decls));
|
|
21998
|
+
return found.operands;
|
|
21999
|
+
}
|
|
22000
|
+
function credentialOf(claims) {
|
|
22001
|
+
const scopes = claims === null ? void 0 : claims["scopes"];
|
|
22002
|
+
const scoped = isAgentScoped(scopes);
|
|
22003
|
+
const entries = scopeEntries(scopes).map(([resource, action]) => `${resource}:${action}`);
|
|
22004
|
+
const canWrite = isAllowed(scopes, "metrics", "write");
|
|
22005
|
+
if (claims === null) {
|
|
22006
|
+
return { scoped: false, can_write: true, scopes: [], line: "scope ceiling not read (this session holds no JWT yet); Datarails decides on create" };
|
|
22007
|
+
}
|
|
22008
|
+
if (!scoped) return { scoped: false, can_write: true, scopes: [], line: "can create metrics (no scope ceiling on this session)" };
|
|
22009
|
+
if (canWrite) return { scoped: true, can_write: true, scopes: entries, line: `can create metrics (this session's scope ceiling, ${entries.join(", ")}, allows metrics:write)` };
|
|
22010
|
+
return { scoped: true, can_write: false, scopes: entries, line: `cannot create or change metrics: this session's scope ceiling (${entries.join(", ")}) does not allow metrics:write` };
|
|
22011
|
+
}
|
|
22012
|
+
function scopeSentence(key, name, credential, verb) {
|
|
22013
|
+
return `metric "${key}": ${verb} "${name}" needs the Datarails scope metrics:write; this build's credential allows ${credential.scopes.join(", ") || "no write scope"}. Ask the agent's owner to add metrics:write to its CLI scope ceiling, or create the metric in Datarails and declare it with "reuse": "name"`;
|
|
22014
|
+
}
|
|
22015
|
+
function findAuthTable(tables, ref) {
|
|
22016
|
+
if (ref === void 0) return void 0;
|
|
22017
|
+
if (typeof ref.id === "number") return tables.find((one) => one.id === ref.id);
|
|
22018
|
+
const want = (ref.alias ?? ref.name ?? "").trim();
|
|
22019
|
+
if (want === "") return void 0;
|
|
22020
|
+
const exact = tables.find((one) => ref.alias !== void 0 ? one.alias === want : one.name === want);
|
|
22021
|
+
if (exact !== void 0) return exact;
|
|
22022
|
+
const lower = want.toLowerCase();
|
|
22023
|
+
return tables.find((one) => ref.alias !== void 0 ? (one.alias ?? "").toLowerCase() === lower : one.name.toLowerCase() === lower);
|
|
22024
|
+
}
|
|
22025
|
+
function findField(fields, named13) {
|
|
22026
|
+
const want = named13.trim();
|
|
22027
|
+
const lower = want.toLowerCase();
|
|
22028
|
+
return fields.find((one) => one.alias === want) ?? fields.find((one) => one.name === want) ?? fields.find((one) => (one.alias ?? "").toLowerCase() === lower) ?? fields.find((one) => one.name.toLowerCase() === lower);
|
|
22029
|
+
}
|
|
22030
|
+
function tableLabel(ref, found) {
|
|
22031
|
+
if (found !== void 0) return found.name;
|
|
22032
|
+
return ref?.alias ?? ref?.name ?? (ref?.id === void 0 ? "?" : `table ${String(ref.id)}`);
|
|
22033
|
+
}
|
|
22034
|
+
function describeBase(decl, tableName) {
|
|
22035
|
+
const filters = (decl.filters ?? []).map((one) => `${one.field} ${one.exclude === true ? "not in" : "in"} [${one.values.map(String).join(", ")}]`).join("; ");
|
|
22036
|
+
return `BASE on "${tableName}" (${decl.value ?? "?"} by ${decl.date ?? "?"}, ${(decl.aggregate ?? "?").toUpperCase()}${decl.time_aggregation === void 0 ? "" : `, ${decl.time_aggregation.toUpperCase()} over time`})${filters === "" ? "" : ` where ${filters}`}`;
|
|
22037
|
+
}
|
|
22038
|
+
function describeRow(row, tables) {
|
|
22039
|
+
if (row.kind === "CALC") return `CALC ${row.formula ?? "(no formula)"}`;
|
|
22040
|
+
if (row.kind !== "BASE") return row.kind === "" ? "a metric" : `a ${row.kind} metric`;
|
|
22041
|
+
const table = tables?.find((one) => one.id === row.template);
|
|
22042
|
+
const field = (id) => {
|
|
22043
|
+
const found = table?.fields?.find((one) => one.id === id);
|
|
22044
|
+
return found === void 0 ? `field ${String(id)}` : found.alias ?? found.name;
|
|
22045
|
+
};
|
|
22046
|
+
const filters = row.filters.map((one) => `${field(one.field)} ${one.is_excluded ? "not in" : "in"} [${one.values.join(", ")}]`).join("; ");
|
|
22047
|
+
const aggregate = `${row.is_cumulative ? "CUMULATIVE_" : ""}${row.aggregator ?? "?"}`;
|
|
22048
|
+
return `BASE on "${table?.name ?? `table ${String(row.template)}`}" (${field(row.value_field)} by ${field(row.date_field)}, ${aggregate}${row.time_aggregation === null ? "" : `, ${row.time_aggregation} over time`})${filters === "" ? "" : ` where ${filters}`}`;
|
|
22049
|
+
}
|
|
22050
|
+
async function readUsage(t, appId, metricId) {
|
|
22051
|
+
let answer;
|
|
22052
|
+
try {
|
|
22053
|
+
answer = await t.call("GET", `${t.appSpace}/apps/${encodeURIComponent(appId)}/metric-bindings/usage?metric_id=${String(metricId)}`);
|
|
22054
|
+
} catch (err) {
|
|
22055
|
+
return `App Space could not be asked whether it may change it (${firstLine(err)})`;
|
|
22056
|
+
}
|
|
22057
|
+
if (answer.status === 404) return "App Space holds no binding of this app it could show you (a first ship, a server without the usage read, or an app that is not yours)";
|
|
22058
|
+
if (answer.status !== 200) return `App Space could not say whether it may change it (HTTP ${String(answer.status)})`;
|
|
22059
|
+
const one = obj2(obj2(obj2(answer.body)["metrics"])[String(metricId)]);
|
|
22060
|
+
const app = obj2(one["this_app"]);
|
|
22061
|
+
return {
|
|
22062
|
+
created_by_app_space: one["created_by_app_space"] === true,
|
|
22063
|
+
created_by_you: one["created_by_you"] === true,
|
|
22064
|
+
keys: Array.isArray(app["keys"]) ? app["keys"].map(String) : [],
|
|
22065
|
+
published: app["published"] === true,
|
|
22066
|
+
read_elsewhere: one["read_elsewhere"] === true,
|
|
22067
|
+
in_place_allowed: one["in_place_allowed"] === true
|
|
22068
|
+
};
|
|
22069
|
+
}
|
|
22070
|
+
function editRefusal(usage, key, row) {
|
|
22071
|
+
if (typeof usage === "string") return usage;
|
|
22072
|
+
if (!usage.created_by_app_space) return "App Space did not create it";
|
|
22073
|
+
if (!usage.created_by_you) return "App Space created it for someone else, and only its creator may change it in place";
|
|
22074
|
+
if (usage.read_elsewhere) return "another app reads it";
|
|
22075
|
+
if (usage.keys.some((one) => one !== key)) return `this app also reads it as ${usage.keys.filter((one) => one !== key).map((one) => `"${one}"`).join(", ")}`;
|
|
22076
|
+
if (usage.published) return "the app's published version reads it, and its viewers would see the change before you publish";
|
|
22077
|
+
if (!usage.in_place_allowed) return "App Space does not allow it";
|
|
22078
|
+
if (!(row.description ?? "").includes(METRIC_CREATED_FOOTER)) return "its description no longer carries App Space's marker, so the push would not keep it as App Space's";
|
|
22079
|
+
return null;
|
|
22080
|
+
}
|
|
22081
|
+
async function planMetrics(contract, t, opts) {
|
|
22082
|
+
const decls = declaredMetrics(contract);
|
|
22083
|
+
const catalog = await readCatalog(t);
|
|
22084
|
+
const claims = t.claims();
|
|
22085
|
+
const credential = credentialOf(claims);
|
|
22086
|
+
const byName = new Map(catalog.map((row) => [row.name, row]));
|
|
22087
|
+
const byId = new Map(catalog.map((row) => [row.id, row]));
|
|
22088
|
+
const rowKeys2 = new Map(catalog.map((row) => [row.id, rowReuseKey(row, byName)]));
|
|
22089
|
+
const plan = {
|
|
22090
|
+
server: t.serverKey,
|
|
22091
|
+
host: t.host,
|
|
22092
|
+
org: typeof claims?.["organization_id"] === "number" ? claims["organization_id"] : null,
|
|
22093
|
+
user: typeof claims?.["email"] === "string" ? claims["email"] : t.email,
|
|
22094
|
+
app_id: opts.appId,
|
|
22095
|
+
entries: [],
|
|
22096
|
+
credential,
|
|
22097
|
+
not_checked: [],
|
|
22098
|
+
notes: [],
|
|
22099
|
+
problems: [],
|
|
22100
|
+
internal: { decls, catalog, tables: null, base: {}, tableOf: {}, appName: opts.appName }
|
|
22101
|
+
};
|
|
22102
|
+
if (t.overridden) {
|
|
22103
|
+
plan.notes.push(`metrics are read and created on ${t.host} (the Datarails host of ${t.serverKey}), not on --app-space-host ${t.appSpace}`);
|
|
22104
|
+
}
|
|
22105
|
+
const needsTables = Object.values(decls).some((decl) => decl.kind === "base" && decl.table !== void 0);
|
|
22106
|
+
let tables = null;
|
|
22107
|
+
let tablesWhy = null;
|
|
22108
|
+
if (needsTables) {
|
|
22109
|
+
try {
|
|
22110
|
+
tables = await readAuthTables(t);
|
|
22111
|
+
} catch (err) {
|
|
22112
|
+
tablesWhy = err instanceof Error ? err.message : String(err);
|
|
22113
|
+
}
|
|
22114
|
+
}
|
|
22115
|
+
plan.internal.tables = tables;
|
|
22116
|
+
const fieldsRead = /* @__PURE__ */ new Map();
|
|
22117
|
+
const fieldsOf = async (table) => {
|
|
22118
|
+
if (table.fields !== null) return table.fields;
|
|
22119
|
+
const held = fieldsRead.get(table.id);
|
|
22120
|
+
if (held !== void 0) return held;
|
|
22121
|
+
let got;
|
|
22122
|
+
try {
|
|
22123
|
+
got = await readTableFields(t, table.id);
|
|
22124
|
+
} catch (err) {
|
|
22125
|
+
got = err instanceof Error ? err.message : String(err);
|
|
22126
|
+
}
|
|
22127
|
+
fieldsRead.set(table.id, got);
|
|
22128
|
+
return got;
|
|
22129
|
+
};
|
|
22130
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
22131
|
+
for (const key of dependencyOrder(decls)) {
|
|
22132
|
+
const decl = decls[key];
|
|
22133
|
+
const kind = decl.kind === "base" || decl.kind === "calc" ? decl.kind : null;
|
|
22134
|
+
const entry = { key, name: decl.name, kind, action: "refuse", metric_id: null, metric_name: null, origin: null, reuse_key: null, definition: "", note: "" };
|
|
22135
|
+
const refuse2 = (code, message, note) => {
|
|
22136
|
+
entry.action = "refuse";
|
|
22137
|
+
entry.code = code;
|
|
22138
|
+
entry.message = message;
|
|
22139
|
+
entry.note = note ?? message;
|
|
22140
|
+
plan.problems.push(problem4(code, key, message));
|
|
22141
|
+
};
|
|
22142
|
+
resolved.set(key, entry);
|
|
22143
|
+
let wire = null;
|
|
22144
|
+
let pendingOperands = [];
|
|
22145
|
+
let defProblem = null;
|
|
22146
|
+
if (kind === "calc" && typeof decl.formula === "string") {
|
|
22147
|
+
const operands = operandsOf(decls, key);
|
|
22148
|
+
const names = {};
|
|
22149
|
+
const ids = {};
|
|
22150
|
+
for (const operand of operands) {
|
|
22151
|
+
const dep = resolved.get(operand);
|
|
22152
|
+
if (dep === void 0 || dep.action === "refuse") {
|
|
22153
|
+
defProblem = { code: "metric_operand", message: `metric "${key}" reads [@${operand}], which this ship cannot bind (see "${operand}")` };
|
|
22154
|
+
break;
|
|
22155
|
+
}
|
|
22156
|
+
if (dep.action === "create") {
|
|
22157
|
+
pendingOperands.push(operand);
|
|
22158
|
+
names[operand] = dep.name;
|
|
22159
|
+
} else {
|
|
22160
|
+
names[operand] = dep.metric_name ?? dep.name;
|
|
22161
|
+
if (dep.metric_id !== null) ids[operand] = dep.metric_id;
|
|
22162
|
+
}
|
|
22163
|
+
}
|
|
22164
|
+
if (defProblem === null) {
|
|
22165
|
+
try {
|
|
22166
|
+
wire = wireFormula(decl.formula, names);
|
|
22167
|
+
} catch (err) {
|
|
22168
|
+
defProblem = { code: "metric_preview", message: `metric "${key}"'s formula ${firstLine(err)}` };
|
|
22169
|
+
}
|
|
22170
|
+
}
|
|
22171
|
+
if (wire !== null) {
|
|
22172
|
+
entry.definition = `CALC ${wire}`;
|
|
22173
|
+
if (pendingOperands.length === 0) {
|
|
22174
|
+
const nameToId = new Map(Object.entries(names).map(([operand, name]) => [name, ids[operand] ?? null]));
|
|
22175
|
+
entry.reuse_key = calcReuseKey(wire, (name) => nameToId.get(name) ?? null);
|
|
22176
|
+
}
|
|
22177
|
+
}
|
|
22178
|
+
} else if (kind === "base") {
|
|
22179
|
+
const table = tables === null ? void 0 : findAuthTable(tables, decl.table);
|
|
22180
|
+
entry.definition = describeBase(decl, tableLabel(decl.table, table));
|
|
22181
|
+
if (tables === null) {
|
|
22182
|
+
defProblem = { code: "metric_table", message: `metric "${key}": ${tablesWhy ?? "the tables could not be read"}` };
|
|
22183
|
+
} else if (table === void 0) {
|
|
22184
|
+
defProblem = { code: "metric_table", message: `metric "${key}": this company has no table "${tableLabel(decl.table)}" you can build metrics on` };
|
|
22185
|
+
} else {
|
|
22186
|
+
plan.internal.tableOf[key] = table;
|
|
22187
|
+
const fields = await fieldsOf(table);
|
|
22188
|
+
if (typeof fields === "string") {
|
|
22189
|
+
defProblem = { code: "metric_field", message: `metric "${key}": ${fields}` };
|
|
22190
|
+
} else {
|
|
22191
|
+
const want = [decl.value ?? "", decl.date ?? "", ...(decl.filters ?? []).map((one) => one.field)];
|
|
22192
|
+
const found = want.map((named14) => findField(fields, named14));
|
|
22193
|
+
const missing = want.find((_named, index) => found[index] === void 0);
|
|
22194
|
+
const noId = found.find((one) => one !== void 0 && one.id === null);
|
|
22195
|
+
const aggregate = AGGREGATES[(decl.aggregate ?? "").toLowerCase()];
|
|
22196
|
+
if (missing !== void 0) {
|
|
22197
|
+
defProblem = { code: "metric_field", message: `metric "${key}": the table "${table.name}" has no field "${missing}" (by alias or name)` };
|
|
22198
|
+
} else if (noId !== void 0) {
|
|
22199
|
+
defProblem = { code: "metric_field", message: `metric "${key}": the field "${noId.alias ?? noId.name}" of "${table.name}" has no numeric id, so it cannot be a BASE operand` };
|
|
22200
|
+
} else if (aggregate === void 0) {
|
|
22201
|
+
defProblem = { code: "metric_field", message: `metric "${key}": ${JSON.stringify(decl.aggregate)} is not an aggregate Datarails takes` };
|
|
22202
|
+
} else {
|
|
22203
|
+
const def = {
|
|
22204
|
+
template: table.id,
|
|
22205
|
+
value_field: found[0]?.id,
|
|
22206
|
+
date_field: found[1]?.id,
|
|
22207
|
+
aggregator: aggregate[0],
|
|
22208
|
+
is_cumulative: aggregate[1],
|
|
22209
|
+
time_aggregation: decl.time_aggregation === void 0 ? null : decl.time_aggregation.toUpperCase(),
|
|
22210
|
+
filters: (decl.filters ?? []).map((one, index) => ({
|
|
22211
|
+
field: found[index + 2]?.id,
|
|
22212
|
+
values: one.values.map(String),
|
|
22213
|
+
is_excluded: one.exclude === true,
|
|
22214
|
+
allow_nulls: false
|
|
22215
|
+
}))
|
|
22216
|
+
};
|
|
22217
|
+
plan.internal.base[key] = def;
|
|
22218
|
+
entry.reuse_key = baseReuseKey(def);
|
|
22219
|
+
}
|
|
22220
|
+
}
|
|
22221
|
+
}
|
|
22222
|
+
}
|
|
22223
|
+
const named13 = byName.get(decl.name);
|
|
22224
|
+
if (entry.reuse_key !== null) {
|
|
22225
|
+
const same5 = catalog.filter((row) => rowKeys2.get(row.id) === entry.reuse_key).sort((a, b) => Number(b.name === decl.name) - Number(a.name === decl.name) || (a.created_at ?? "").localeCompare(b.created_at ?? "") || a.id - b.id);
|
|
22226
|
+
const hit = same5[0];
|
|
22227
|
+
if (hit !== void 0) {
|
|
22228
|
+
entry.action = "reuse";
|
|
22229
|
+
entry.metric_id = hit.id;
|
|
22230
|
+
entry.metric_name = hit.name;
|
|
22231
|
+
entry.origin = "reused";
|
|
22232
|
+
entry.note = hit.name === decl.name ? "the same definition" : `the same definition, named "${hit.name}" in this company (not a clash)`;
|
|
22233
|
+
continue;
|
|
22234
|
+
}
|
|
22235
|
+
}
|
|
22236
|
+
if (decl.reuse === "name" && named13 !== void 0) {
|
|
22237
|
+
entry.action = "bind";
|
|
22238
|
+
entry.metric_id = named13.id;
|
|
22239
|
+
entry.metric_name = named13.name;
|
|
22240
|
+
entry.origin = "named";
|
|
22241
|
+
const theirs = describeRow(named13, tables);
|
|
22242
|
+
entry.note = kind === null ? "by name" : entry.reuse_key === null ? `by name (the contract's definition could not be compared here: ${defProblem?.message ?? "an operand is created on push"})` : `by name; its definition differs: ${theirs} against the contract's ${entry.definition}`;
|
|
22243
|
+
continue;
|
|
22244
|
+
}
|
|
22245
|
+
if (defProblem !== null) {
|
|
22246
|
+
refuse2(defProblem.code, defProblem.message);
|
|
22247
|
+
continue;
|
|
22248
|
+
}
|
|
22249
|
+
if (named13 !== void 0 && kind !== null) {
|
|
22250
|
+
const theirs = describeRow(named13, tables);
|
|
22251
|
+
const sameKind = named13.kind === kind.toUpperCase();
|
|
22252
|
+
const previous = rowKeys2.get(named13.id) ?? null;
|
|
22253
|
+
let why = null;
|
|
22254
|
+
if (decl.create === false) why = 'the declaration says "create": false, so this ship writes nothing';
|
|
22255
|
+
else if (!sameKind) why = `it is a ${named13.kind || "different"} metric, and a ${kind.toUpperCase()} cannot replace it in place`;
|
|
22256
|
+
else if (opts.appId === null || opts.appId === "") why = "the contract names no app id to ask App Space about";
|
|
22257
|
+
else if (previous === null) why = "its current definition could not be read in full, so a change could not be recorded";
|
|
22258
|
+
else if (entry.reuse_key === null && pendingOperands.length === 0) why = "the contract's definition could not be compared";
|
|
22259
|
+
else why = editRefusal(await readUsage(t, opts.appId, named13.id), key, named13);
|
|
22260
|
+
if (why === null) {
|
|
22261
|
+
if (!credential.can_write) {
|
|
22262
|
+
refuse2("metric_scope", scopeSentence(key, decl.name, credential, "changing"), `would change in place, but ${credential.line}`);
|
|
22263
|
+
continue;
|
|
22264
|
+
}
|
|
22265
|
+
entry.action = "edit";
|
|
22266
|
+
entry.metric_id = named13.id;
|
|
22267
|
+
entry.metric_name = named13.name;
|
|
22268
|
+
entry.origin = "created";
|
|
22269
|
+
entry.previous_reuse_key = previous;
|
|
22270
|
+
entry.old_definition = theirs;
|
|
22271
|
+
entry.new_definition = entry.definition;
|
|
22272
|
+
entry.note = `in place (App Space made it for this app): was ${theirs}; becomes ${entry.definition}`;
|
|
22273
|
+
if (kind === "calc" && wire !== null && pendingOperands.length === 0) {
|
|
22274
|
+
const preview2 = await previewFormula(t, wire);
|
|
22275
|
+
if (preview2 !== null) {
|
|
22276
|
+
refuse2("metric_preview", `metric "${key}": Datarails could not evaluate ${wire}: ${preview2}`);
|
|
22277
|
+
continue;
|
|
22278
|
+
}
|
|
22279
|
+
entry.preview = "ok";
|
|
22280
|
+
} else if (pendingOperands.length > 0) {
|
|
22281
|
+
entry.after = pendingOperands;
|
|
22282
|
+
entry.preview = "not checked";
|
|
22283
|
+
plan.not_checked.push(`the formula of "${decl.name}" (its operand ${pendingOperands.map((one) => `"${resolved.get(one)?.name ?? one}"`).join(", ")} is created on push)`);
|
|
22284
|
+
}
|
|
22285
|
+
continue;
|
|
22286
|
+
}
|
|
22287
|
+
const message = `metric "${key}": this company already has a metric named "${named13.name}" (id ${String(named13.id)}) with a different definition: ${theirs} against the contract's ${entry.definition}. Either use theirs ("reuse": "name"), or give the contract's metric another name (App Space may not change it in place: ${why})`;
|
|
22288
|
+
refuse2("metric_name_taken", message, `name taken by id ${String(named13.id)} with another definition: see above`);
|
|
22289
|
+
continue;
|
|
22290
|
+
}
|
|
22291
|
+
if (decl.create === false || kind === null) {
|
|
22292
|
+
refuse2("metric_missing", `metric "${key}": this company has no metric named "${decl.name}"; create it in Datarails, or give the declaration a definition`);
|
|
22293
|
+
continue;
|
|
22294
|
+
}
|
|
22295
|
+
if (!credential.can_write) {
|
|
22296
|
+
refuse2("metric_scope", scopeSentence(key, decl.name, credential, "creating"), `would create, but ${credential.line}`);
|
|
22297
|
+
continue;
|
|
22298
|
+
}
|
|
22299
|
+
if (kind === "base") {
|
|
22300
|
+
const table = plan.internal.tableOf[key];
|
|
22301
|
+
if (table?.blocked === true) {
|
|
22302
|
+
refuse2("metric_table", `metric "${key}": you can't build metrics on the table "${table.name}" (a permission filter applies to you); ask an admin for access`);
|
|
22303
|
+
continue;
|
|
22304
|
+
}
|
|
22305
|
+
entry.action = "create";
|
|
22306
|
+
entry.note = `${entry.definition}; extracts after creation`;
|
|
22307
|
+
continue;
|
|
22308
|
+
}
|
|
22309
|
+
entry.action = "create";
|
|
22310
|
+
if (pendingOperands.length > 0) {
|
|
22311
|
+
entry.after = pendingOperands;
|
|
22312
|
+
entry.preview = "not checked";
|
|
22313
|
+
entry.note = `${entry.definition}; created after ${pendingOperands.map((one) => `"${one}"`).join(", ")}`;
|
|
22314
|
+
plan.not_checked.push(`the formula of "${decl.name}" (its operand ${pendingOperands.map((one) => `"${resolved.get(one)?.name ?? one}"`).join(", ")} is created on push)`);
|
|
22315
|
+
continue;
|
|
22316
|
+
}
|
|
22317
|
+
const preview = await previewFormula(t, wire);
|
|
22318
|
+
if (preview !== null) {
|
|
22319
|
+
refuse2("metric_preview", `metric "${key}": Datarails could not evaluate ${wire}: ${preview}`);
|
|
22320
|
+
continue;
|
|
22321
|
+
}
|
|
22322
|
+
entry.preview = "ok";
|
|
22323
|
+
entry.note = `${entry.definition} preview ok`;
|
|
22324
|
+
}
|
|
22325
|
+
const order2 = Object.keys(decls);
|
|
22326
|
+
plan.entries = [...resolved.values()].sort((a, b) => order2.indexOf(a.key) - order2.indexOf(b.key));
|
|
22327
|
+
for (const entry of plan.entries) {
|
|
22328
|
+
if (entry.metric_id !== null && !byId.has(entry.metric_id)) entry.metric_id = null;
|
|
22329
|
+
}
|
|
22330
|
+
return plan;
|
|
22331
|
+
}
|
|
22332
|
+
async function previewFormula(t, wire) {
|
|
22333
|
+
let answer;
|
|
22334
|
+
try {
|
|
22335
|
+
answer = await t.call("PUT", `${t.host}/api/insights/metrics/preview_calc_data`, { formula: wire, time_aggregation: "SUM" });
|
|
22336
|
+
} catch (err) {
|
|
22337
|
+
return `the preview could not be run (${firstLine(err)})`;
|
|
22338
|
+
}
|
|
22339
|
+
const body = obj2(decodeEntities2(answer.body));
|
|
22340
|
+
if (answer.status >= 200 && answer.status < 300 && body["success"] !== false) return null;
|
|
22341
|
+
return sentenceOf(body) || `HTTP ${String(answer.status)}`;
|
|
22342
|
+
}
|
|
22343
|
+
var METRICS_WAIT_CAP = 120;
|
|
22344
|
+
var METRICS_WAIT_DEFAULT = 60;
|
|
22345
|
+
var POLL_MS = 5e3;
|
|
22346
|
+
function metricsWaitOf(raw) {
|
|
22347
|
+
if (raw === void 0 || raw === "") return { seconds: METRICS_WAIT_DEFAULT, note: null };
|
|
22348
|
+
const value = typeof raw === "number" ? raw : Number(raw);
|
|
22349
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
22350
|
+
throw new Error(`--metrics-wait ${String(raw)} is not a number of seconds; give 0 to ${String(METRICS_WAIT_CAP)}`);
|
|
22351
|
+
}
|
|
22352
|
+
if (value > METRICS_WAIT_CAP) {
|
|
22353
|
+
return { seconds: METRICS_WAIT_CAP, note: `--metrics-wait ${String(raw)} is over the cap; waiting at most ${String(METRICS_WAIT_CAP)} s` };
|
|
22354
|
+
}
|
|
22355
|
+
return { seconds: Math.floor(value), note: null };
|
|
22356
|
+
}
|
|
22357
|
+
async function applyPlan(plan, t, opts) {
|
|
22358
|
+
const out = { ok: true, bindings: {}, created: [], problems: [], notes: [] };
|
|
22359
|
+
if (plan.problems.length > 0) {
|
|
22360
|
+
out.ok = false;
|
|
22361
|
+
out.problems = [...plan.problems];
|
|
22362
|
+
return out;
|
|
22363
|
+
}
|
|
22364
|
+
const { decls } = plan.internal;
|
|
22365
|
+
const byKey2 = new Map(plan.entries.map((entry) => [entry.key, entry]));
|
|
22366
|
+
let category;
|
|
22367
|
+
const fail = (key, message) => {
|
|
22368
|
+
out.ok = false;
|
|
22369
|
+
out.problems.push(problem4("metric_create", key, message));
|
|
22370
|
+
return out;
|
|
22371
|
+
};
|
|
22372
|
+
const waitFor = [];
|
|
22373
|
+
for (const key of dependencyOrder(decls)) {
|
|
22374
|
+
const entry = byKey2.get(key);
|
|
22375
|
+
const decl = decls[key];
|
|
22376
|
+
if (entry === void 0 || decl === void 0) continue;
|
|
22377
|
+
if (entry.action === "reuse" || entry.action === "bind") continue;
|
|
22378
|
+
if (entry.action !== "create" && entry.action !== "edit") continue;
|
|
22379
|
+
let wire = null;
|
|
22380
|
+
if (entry.kind === "calc" && typeof decl.formula === "string") {
|
|
22381
|
+
const names = {};
|
|
22382
|
+
const ids = /* @__PURE__ */ new Map();
|
|
22383
|
+
for (const operand of operandsOf(decls, key)) {
|
|
22384
|
+
const dep = byKey2.get(operand);
|
|
22385
|
+
names[operand] = dep?.metric_name ?? dep?.name ?? operand;
|
|
22386
|
+
ids.set(names[operand], dep?.metric_id ?? null);
|
|
22387
|
+
}
|
|
22388
|
+
wire = wireFormula(decl.formula, names);
|
|
22389
|
+
entry.definition = `CALC ${wire}`;
|
|
22390
|
+
if (entry.action === "edit") entry.new_definition = entry.definition;
|
|
22391
|
+
entry.reuse_key = calcReuseKey(wire, (name) => ids.get(name) ?? null);
|
|
22392
|
+
if (entry.preview !== "ok") {
|
|
22393
|
+
const preview = await previewFormula(t, wire);
|
|
22394
|
+
if (preview !== null) return fail(key, `metric "${key}": Datarails could not evaluate ${wire}: ${preview}`);
|
|
22395
|
+
entry.preview = "ok";
|
|
22396
|
+
}
|
|
22397
|
+
}
|
|
22398
|
+
if (entry.action === "edit") {
|
|
22399
|
+
const body2 = entry.kind === "calc" ? { configuration: { presented_formula: wire, ...formatOf(decl, plan, entry) } } : baseBody(plan.internal.base[key]);
|
|
22400
|
+
const answer2 = await t.call("PATCH", `${t.host}/api/insights/metrics/${String(entry.metric_id)}`, body2).catch((err) => ({ status: 0, body: firstLine(err) }));
|
|
22401
|
+
if (answer2.status < 200 || answer2.status >= 300) {
|
|
22402
|
+
return fail(key, `metric "${key}": Datarails refused to change "${entry.metric_name ?? entry.name}" (id ${String(entry.metric_id)}) in place: ${refusalSentence(answer2, entry, plan)}`);
|
|
22403
|
+
}
|
|
22404
|
+
out.created.push({ key, id: entry.metric_id, name: entry.metric_name ?? entry.name, action: "edit" });
|
|
22405
|
+
if (entry.kind === "base") waitFor.push(entry);
|
|
22406
|
+
continue;
|
|
22407
|
+
}
|
|
22408
|
+
if (category === void 0) {
|
|
22409
|
+
category = await appSpaceCategory(t, out.notes);
|
|
22410
|
+
}
|
|
22411
|
+
const description = [decl.description ?? "", createdFooter(plan.internal.appName)].filter((one) => one !== "").join("\n\n");
|
|
22412
|
+
const body = entry.kind === "calc" ? { name: decl.name, kind: "CALC", configuration: { presented_formula: wire, ...decl.format === void 0 ? {} : { format: decl.format } } } : { name: decl.name, kind: "BASE", ...baseBody(plan.internal.base[key]), ...decl.format === void 0 ? {} : { configuration: { presented_formula: "", format: decl.format } } };
|
|
22413
|
+
body["description"] = description;
|
|
22414
|
+
body["is_approved_by_user"] = true;
|
|
22415
|
+
if (category !== null) body["category"] = category;
|
|
22416
|
+
const answer = await t.call("POST", `${t.host}/api/insights/metrics`, body).catch((err) => ({ status: 0, body: firstLine(err) }));
|
|
22417
|
+
if (answer.status === 422) {
|
|
22418
|
+
let again;
|
|
22419
|
+
try {
|
|
22420
|
+
again = await readCatalog(t);
|
|
22421
|
+
} catch (err) {
|
|
22422
|
+
return fail(key, `metric "${key}": Datarails says a metric named "${decl.name}" exists, and the catalog could not be read again to tell whose (${firstLine(err)})`);
|
|
22423
|
+
}
|
|
22424
|
+
const byName = new Map(again.map((row) => [row.name, row]));
|
|
22425
|
+
const holder = byName.get(decl.name);
|
|
22426
|
+
if (holder === void 0) {
|
|
22427
|
+
return fail(key, `metric "${key}": a metric named "${decl.name}" exists in this company, but its source tables are not shared with you`);
|
|
22428
|
+
}
|
|
22429
|
+
if (entry.reuse_key !== null && rowReuseKey(holder, byName) === entry.reuse_key) {
|
|
22430
|
+
entry.action = "reuse";
|
|
22431
|
+
entry.origin = "reused";
|
|
22432
|
+
entry.metric_id = holder.id;
|
|
22433
|
+
entry.metric_name = holder.name;
|
|
22434
|
+
entry.note = "the same definition (another ship created it a moment ago)";
|
|
22435
|
+
continue;
|
|
22436
|
+
}
|
|
22437
|
+
return fail(
|
|
22438
|
+
key,
|
|
22439
|
+
`metric "${key}": this company now has a metric named "${decl.name}" (id ${String(holder.id)}) with a different definition: ${describeRow(holder, plan.internal.tables)} against the contract's ${entry.definition}. Either use theirs ("reuse": "name"), or give the contract's metric another name`
|
|
22440
|
+
);
|
|
22441
|
+
}
|
|
22442
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
22443
|
+
return fail(key, `metric "${key}": Datarails refused to create "${decl.name}": ${refusalSentence(answer, entry, plan)}`);
|
|
22444
|
+
}
|
|
22445
|
+
const created = obj2(decodeEntities2(answer.body));
|
|
22446
|
+
const id = idIn(created["id"]);
|
|
22447
|
+
if (id === null || id === 0) return fail(key, `metric "${key}": Datarails created "${decl.name}" and answered no id for it`);
|
|
22448
|
+
entry.metric_id = id;
|
|
22449
|
+
entry.metric_name = typeof created["name"] === "string" && created["name"] !== "" ? created["name"] : decl.name;
|
|
22450
|
+
entry.origin = "created";
|
|
22451
|
+
out.created.push({ key, id, name: entry.metric_name, action: "create" });
|
|
22452
|
+
if (entry.kind === "base") waitFor.push(entry);
|
|
22453
|
+
}
|
|
22454
|
+
if (waitFor.length > 0) await waitForExtraction(t, waitFor, opts, out.notes);
|
|
22455
|
+
const written = plan.entries.filter((entry) => out.created.some((one) => one.key === entry.key));
|
|
22456
|
+
if (written.length > 0) {
|
|
22457
|
+
const drifted = await readBack(t, plan, written);
|
|
22458
|
+
if (drifted.length > 0) {
|
|
22459
|
+
out.ok = false;
|
|
22460
|
+
out.problems.push(...drifted);
|
|
22461
|
+
return out;
|
|
22462
|
+
}
|
|
22463
|
+
}
|
|
22464
|
+
for (const entry of plan.entries) {
|
|
22465
|
+
if (entry.metric_id === null || entry.origin === null) continue;
|
|
22466
|
+
out.bindings[entry.key] = {
|
|
22467
|
+
metric_id: entry.metric_id,
|
|
22468
|
+
origin: entry.origin,
|
|
22469
|
+
...entry.reuse_key === null ? {} : { reuse_key: entry.reuse_key },
|
|
22470
|
+
...entry.action === "edit" && typeof entry.previous_reuse_key === "string" ? { previous_reuse_key: entry.previous_reuse_key } : {}
|
|
22471
|
+
};
|
|
22472
|
+
}
|
|
22473
|
+
return out;
|
|
22474
|
+
}
|
|
22475
|
+
async function readBack(t, plan, written) {
|
|
22476
|
+
let catalog;
|
|
22477
|
+
try {
|
|
22478
|
+
catalog = await readCatalog(t);
|
|
22479
|
+
} catch (err) {
|
|
22480
|
+
return [
|
|
22481
|
+
problem4(
|
|
22482
|
+
"metric_changed",
|
|
22483
|
+
null,
|
|
22484
|
+
`${firstLine(err)} after this ship wrote ${written.map((entry) => `"${entry.metric_name ?? entry.name}"`).join(", ")}, so what Datarails holds could not be checked against the contract; nothing was pushed`
|
|
22485
|
+
)
|
|
22486
|
+
];
|
|
22487
|
+
}
|
|
22488
|
+
const byName = new Map(catalog.map((row) => [row.name, row]));
|
|
22489
|
+
const problems = [];
|
|
22490
|
+
for (const entry of written) {
|
|
22491
|
+
const label = `"${entry.metric_name ?? entry.name}" (id ${String(entry.metric_id)})`;
|
|
22492
|
+
const row = catalog.find((one) => one.id === entry.metric_id);
|
|
22493
|
+
if (row === void 0) {
|
|
22494
|
+
problems.push(problem4("metric_changed", entry.key, `metric "${entry.key}": ${label} is not among this company's metrics you can read, just after this ship wrote it; nothing was pushed`));
|
|
22495
|
+
continue;
|
|
22496
|
+
}
|
|
22497
|
+
const held = rowReuseKey(row, byName);
|
|
22498
|
+
if (held === null || entry.reuse_key === null) {
|
|
22499
|
+
problems.push(
|
|
22500
|
+
problem4(
|
|
22501
|
+
"metric_changed",
|
|
22502
|
+
entry.key,
|
|
22503
|
+
`metric "${entry.key}": ${label} could not be read back in full after this ship wrote it (Datarails holds ${describeRow(row, plan.internal.tables)}), so it could not be compared with the contract's ${entry.definition}; nothing was pushed`
|
|
22504
|
+
)
|
|
22505
|
+
);
|
|
22506
|
+
continue;
|
|
22507
|
+
}
|
|
22508
|
+
if (held !== entry.reuse_key) {
|
|
22509
|
+
problems.push(
|
|
22510
|
+
problem4(
|
|
22511
|
+
"metric_changed",
|
|
22512
|
+
entry.key,
|
|
22513
|
+
`metric "${entry.key}": Datarails now holds a different definition for ${label} than this ship wrote: ${describeRow(row, plan.internal.tables)} against the contract's ${entry.definition}. Another ship or person changed it after this ship wrote it, or Datarails did not take the whole change; nothing was pushed`
|
|
22514
|
+
)
|
|
22515
|
+
);
|
|
22516
|
+
continue;
|
|
22517
|
+
}
|
|
22518
|
+
entry.reuse_key = held;
|
|
22519
|
+
entry.metric_name = row.name;
|
|
22520
|
+
}
|
|
22521
|
+
return problems;
|
|
22522
|
+
}
|
|
22523
|
+
function formatOf(decl, plan, entry) {
|
|
22524
|
+
if (decl.format !== void 0) return { format: decl.format };
|
|
22525
|
+
const row = plan.internal.catalog.find((one) => one.id === entry.metric_id);
|
|
22526
|
+
return row?.format === null || row?.format === void 0 ? {} : { format: row.format };
|
|
22527
|
+
}
|
|
22528
|
+
function baseBody(def) {
|
|
22529
|
+
return {
|
|
22530
|
+
template: def.template,
|
|
22531
|
+
value_field: def.value_field,
|
|
22532
|
+
date_field: def.date_field,
|
|
22533
|
+
aggregator: def.aggregator,
|
|
22534
|
+
is_cumulative: def.is_cumulative,
|
|
22535
|
+
time_aggregation: rollupOf(def),
|
|
22536
|
+
filters: def.filters.map((one) => ({
|
|
22537
|
+
field: one.field,
|
|
22538
|
+
values: one.values,
|
|
22539
|
+
is_excluded: one.is_excluded,
|
|
22540
|
+
allow_nulls: one.allow_nulls,
|
|
22541
|
+
override_global_filter: false
|
|
22542
|
+
}))
|
|
22543
|
+
};
|
|
22544
|
+
}
|
|
22545
|
+
async function appSpaceCategory(t, notes) {
|
|
22546
|
+
try {
|
|
22547
|
+
const listed = await t.call("GET", `${t.host}/api/insights/metric_categories`);
|
|
22548
|
+
if (listed.status >= 200 && listed.status < 300) {
|
|
22549
|
+
const found = listOf(decodeEntities2(listed.body)).find((one) => one["name"] === METRIC_CREATED_CATEGORY);
|
|
22550
|
+
const id = found === void 0 ? null : idIn(found["id"]);
|
|
22551
|
+
if (id !== null) return id;
|
|
22552
|
+
const made = await t.call("POST", `${t.host}/api/insights/metric_categories`, { name: METRIC_CREATED_CATEGORY });
|
|
22553
|
+
const madeId = made.status >= 200 && made.status < 300 ? idIn(obj2(made.body)["id"]) : null;
|
|
22554
|
+
if (madeId !== null) return madeId;
|
|
22555
|
+
notes.push(`the metrics are not filed under the "${METRIC_CREATED_CATEGORY}" category (creating it answered HTTP ${String(made.status)}); their description still says App Space made them`);
|
|
22556
|
+
return null;
|
|
22557
|
+
}
|
|
22558
|
+
notes.push(`the metrics are not filed under the "${METRIC_CREATED_CATEGORY}" category (the categories answered HTTP ${String(listed.status)}); their description still says App Space made them`);
|
|
22559
|
+
} catch (err) {
|
|
22560
|
+
notes.push(`the metrics are not filed under the "${METRIC_CREATED_CATEGORY}" category (${firstLine(err)}); their description still says App Space made them`);
|
|
22561
|
+
}
|
|
22562
|
+
return null;
|
|
22563
|
+
}
|
|
22564
|
+
function refusalSentence(answer, entry, plan) {
|
|
22565
|
+
const said2 = sentenceOf(decodeEntities2(answer.body));
|
|
22566
|
+
const table = plan.internal.tableOf[entry.key];
|
|
22567
|
+
if (answer.status === 400 && /invalid pk/i.test(JSON.stringify(answer.body ?? "")) && /template/.test(JSON.stringify(answer.body ?? ""))) {
|
|
22568
|
+
return `you can't build metrics on the table "${table?.name ?? "?"}"; ask an admin for access`;
|
|
22569
|
+
}
|
|
22570
|
+
if (answer.status === 403 && /do not have access to this metric/i.test(said2)) {
|
|
22571
|
+
return entry.kind === "base" ? `the table "${table?.name ?? "?"}" has a permission filter on you (authoring blocked); ask an admin for access` : "one of its operands reads data you cannot see; ask an admin to share its source tables";
|
|
22572
|
+
}
|
|
22573
|
+
if (answer.status === 403) {
|
|
22574
|
+
return `this credential was refused (403${said2 === "" ? "" : `: ${said2}`}); it may lack the Datarails scope metrics:write \u2014 ask the agent's owner to add it to its CLI scope ceiling, or create the metric in Datarails and declare it with "reuse": "name"`;
|
|
22575
|
+
}
|
|
22576
|
+
if (answer.status === 404) return "it no longer exists in this company";
|
|
22577
|
+
if (answer.status === 0) return `the request did not complete (${String(answer.body)})`;
|
|
22578
|
+
return said2 === "" ? `HTTP ${String(answer.status)}` : `${said2} (HTTP ${String(answer.status)})`;
|
|
22579
|
+
}
|
|
22580
|
+
async function waitForExtraction(t, entries, opts, notes) {
|
|
22581
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
22582
|
+
const now = opts.now ?? (() => Date.now());
|
|
22583
|
+
const deadline = now() + Math.max(0, Math.min(opts.waitSeconds, METRICS_WAIT_CAP)) * 1e3;
|
|
22584
|
+
let waiting = entries;
|
|
22585
|
+
for (; ; ) {
|
|
22586
|
+
const still = [];
|
|
22587
|
+
for (const entry of waiting) {
|
|
22588
|
+
const answer = await t.call("GET", `${t.host}/api/insights/metrics/${String(entry.metric_id)}`).catch(() => null);
|
|
22589
|
+
const row = answer !== null && answer.status === 200 && isObj3(answer.body) ? catalogRow(answer.body) : null;
|
|
22590
|
+
entry.status = row?.status ?? entry.status ?? null;
|
|
22591
|
+
if (entry.status === null || entry.status === "PROCESSING") still.push(entry);
|
|
22592
|
+
}
|
|
22593
|
+
waiting = still;
|
|
22594
|
+
if (waiting.length === 0 || now() + POLL_MS > deadline) break;
|
|
22595
|
+
await sleep3(POLL_MS);
|
|
22596
|
+
}
|
|
22597
|
+
for (const entry of waiting) {
|
|
22598
|
+
entry.status = entry.status ?? "PROCESSING";
|
|
22599
|
+
notes.push(`"${entry.metric_name ?? entry.name}" is still being prepared in Datarails; its cards say so until it is ready`);
|
|
22600
|
+
}
|
|
22601
|
+
}
|
|
22602
|
+
function bindingWarnings(answer) {
|
|
22603
|
+
const block = obj2(answer);
|
|
22604
|
+
const rows2 = (value) => (Array.isArray(value) ? value : []).map(
|
|
22605
|
+
(one) => isObj3(one) ? {
|
|
22606
|
+
key: String(one["key"] ?? "?"),
|
|
22607
|
+
field: typeof one["field"] === "string" ? one["field"] : null,
|
|
22608
|
+
reason: typeof one["reason"] === "string" && one["reason"] !== "" ? one["reason"] : null
|
|
22609
|
+
} : { key: String(one), field: null, reason: null }
|
|
22610
|
+
);
|
|
22611
|
+
const lines = [];
|
|
22612
|
+
for (const one of rows2(block["downgraded"])) {
|
|
22613
|
+
lines.push(
|
|
22614
|
+
`metric "${one.key}" was sent as created by App Space and kept as reused${one.reason === null ? "" : ` (${one.reason})`}: App Space will not change it in place later`
|
|
22615
|
+
);
|
|
22616
|
+
}
|
|
22617
|
+
for (const one of rows2(block["ignored"])) {
|
|
22618
|
+
if (one.field === null || one.field === "previous_reuse_key") {
|
|
22619
|
+
lines.push(
|
|
22620
|
+
`metric "${one.key}" was changed in place in Datarails, and the platform did NOT record the change${one.reason === null ? "" : ` (${one.reason})`}: the metric holds the new definition; check it in Datarails`
|
|
22621
|
+
);
|
|
22622
|
+
} else {
|
|
22623
|
+
lines.push(`metric "${one.key}": the platform did not record ${one.field}${one.reason === null ? "" : ` (${one.reason})`}`);
|
|
22624
|
+
}
|
|
22625
|
+
}
|
|
22626
|
+
return lines;
|
|
22627
|
+
}
|
|
22628
|
+
function renderPlan(plan) {
|
|
22629
|
+
const head = `Metrics on ${plan.server}${plan.org === null ? "" : ` (org ${String(plan.org)})`}${plan.user === null ? "" : `, as ${plan.user}`}:
|
|
22630
|
+
`;
|
|
22631
|
+
const label = (entry) => `${entry.metric_name ?? entry.name}${entry.metric_id === null ? "" : ` (id ${String(entry.metric_id)})`}`;
|
|
22632
|
+
const keyWidth = Math.max(...plan.entries.map((entry) => entry.key.length), 3) + 2;
|
|
22633
|
+
const nameWidth = Math.max(...plan.entries.map((entry) => label(entry).length), 4) + 2;
|
|
22634
|
+
let out = head;
|
|
22635
|
+
for (const entry of plan.entries) {
|
|
22636
|
+
const action = entry.action === "edit" ? "change" : entry.action;
|
|
22637
|
+
out += ` ${entry.key.padEnd(keyWidth)}${label(entry).padEnd(nameWidth)}${action.padEnd(9)}${entry.note}
|
|
22638
|
+
`;
|
|
22639
|
+
if (entry.action === "edit") {
|
|
22640
|
+
out += ` ${" ".repeat(keyWidth + nameWidth + 9)}was: ${entry.old_definition ?? "?"}
|
|
22641
|
+
`;
|
|
22642
|
+
out += ` ${" ".repeat(keyWidth + nameWidth + 9)}now: ${entry.new_definition ?? "?"}
|
|
22643
|
+
`;
|
|
22644
|
+
}
|
|
22645
|
+
}
|
|
22646
|
+
out += `Credential: ${plan.credential.line}
|
|
22647
|
+
`;
|
|
22648
|
+
for (const note of plan.notes) out += `note: ${note}
|
|
22649
|
+
`;
|
|
22650
|
+
for (const one of plan.not_checked) out += `Not checked: ${one}
|
|
22651
|
+
`;
|
|
22652
|
+
return out;
|
|
22653
|
+
}
|
|
22654
|
+
function renderApplied(plan, applied) {
|
|
22655
|
+
let out = "";
|
|
22656
|
+
for (const one of applied.created) {
|
|
22657
|
+
const entry = plan.entries.find((candidate) => candidate.key === one.key);
|
|
22658
|
+
if (one.action === "edit") {
|
|
22659
|
+
out += `metric "${one.key}": changed "${one.name}" (id ${String(one.id)}) in place
|
|
22660
|
+
was: ${entry?.old_definition ?? "?"}
|
|
22661
|
+
now: ${entry?.new_definition ?? "?"}
|
|
22662
|
+
`;
|
|
22663
|
+
} else {
|
|
22664
|
+
out += `metric "${one.key}": created "${one.name}" (id ${String(one.id)})${entry?.status === void 0 || entry.status === null ? "" : `, ${entry.status.toLowerCase()}`}
|
|
22665
|
+
`;
|
|
22666
|
+
}
|
|
22667
|
+
}
|
|
22668
|
+
for (const note of applied.notes) out += `note: ${note}
|
|
22669
|
+
`;
|
|
22670
|
+
return out;
|
|
22671
|
+
}
|
|
22672
|
+
function planReport(plan, applied) {
|
|
22673
|
+
return {
|
|
22674
|
+
server: plan.server,
|
|
22675
|
+
host: plan.host,
|
|
22676
|
+
org: plan.org,
|
|
22677
|
+
user: plan.user,
|
|
22678
|
+
credential: plan.credential,
|
|
22679
|
+
entries: plan.entries.map((entry) => ({
|
|
22680
|
+
key: entry.key,
|
|
22681
|
+
name: entry.name,
|
|
22682
|
+
kind: entry.kind,
|
|
22683
|
+
action: entry.action,
|
|
22684
|
+
metric_id: entry.metric_id,
|
|
22685
|
+
metric_name: entry.metric_name,
|
|
22686
|
+
origin: entry.origin,
|
|
22687
|
+
definition: entry.definition,
|
|
22688
|
+
note: entry.note,
|
|
22689
|
+
...entry.preview === void 0 ? {} : { preview: entry.preview },
|
|
22690
|
+
...entry.after === void 0 ? {} : { after: entry.after },
|
|
22691
|
+
...entry.code === void 0 ? {} : { code: entry.code, message: entry.message },
|
|
22692
|
+
...entry.action === "edit" ? { old_definition: entry.old_definition, new_definition: entry.new_definition } : {},
|
|
22693
|
+
...entry.status === void 0 ? {} : { status: entry.status }
|
|
22694
|
+
})),
|
|
22695
|
+
not_checked: plan.not_checked,
|
|
22696
|
+
notes: [...plan.notes, ...applied?.notes ?? []],
|
|
22697
|
+
...applied === void 0 ? {} : { created: applied.created, bindings: applied.bindings, ok: applied.ok }
|
|
22698
|
+
};
|
|
22699
|
+
}
|
|
22700
|
+
async function checkMetricSources(t, appId, contract) {
|
|
22701
|
+
const out = [];
|
|
22702
|
+
for (const [source, key] of Object.entries(metricSources(contract))) {
|
|
22703
|
+
let answer;
|
|
22704
|
+
try {
|
|
22705
|
+
answer = await t.call("POST", `${t.appSpace}/preview/apps/${encodeURIComponent(appId)}/${encodeURIComponent(source)}`, { op: "load" });
|
|
22706
|
+
} catch (err) {
|
|
22707
|
+
out.push({ source, key, state: "unchecked", message: `could not be run (${firstLine(err)})` });
|
|
22708
|
+
continue;
|
|
22709
|
+
}
|
|
22710
|
+
if (answer.status < 200 || answer.status >= 300) {
|
|
22711
|
+
out.push({ source, key, ...previewRefusal(answer) });
|
|
22712
|
+
continue;
|
|
22713
|
+
}
|
|
22714
|
+
if (!isObj3(answer.body)) {
|
|
22715
|
+
out.push({ source, key, state: "unchecked", message: `could not be read (the preview answered HTTP ${String(answer.status)} with no source answer)` });
|
|
22716
|
+
continue;
|
|
22717
|
+
}
|
|
22718
|
+
const body = answer.body;
|
|
22719
|
+
const held = isObj3(body["result"]) && !("meta" in body) ? obj2(body["result"]) : body;
|
|
22720
|
+
if (!("error" in held) && !("meta" in held) && !("rows" in held)) {
|
|
22721
|
+
out.push({ source, key, state: "unchecked", message: `could not be read (the preview answered HTTP ${String(answer.status)} with no source answer)` });
|
|
22722
|
+
continue;
|
|
22723
|
+
}
|
|
22724
|
+
const status = obj2(obj2(held["meta"])["source"])["status"];
|
|
22725
|
+
const error = typeof held["error"] === "string" ? held["error"] : "";
|
|
22726
|
+
if (status === "processing") out.push({ source, key, state: "preparing", message: error || "Datarails is preparing the metric" });
|
|
22727
|
+
else if (error !== "" || status === "unbound" || status === "gone" || status === "error") {
|
|
22728
|
+
out.push({ source, key, state: "failed", message: error || `the metric is ${String(status)}` });
|
|
22729
|
+
} else out.push({ source, key, state: "figure", message: "read" });
|
|
22730
|
+
}
|
|
22731
|
+
return out;
|
|
22732
|
+
}
|
|
22733
|
+
var PREVIEW_UNAVAILABLE = /* @__PURE__ */ new Set([401, 403, 407, 408, 429, 502, 503, 504]);
|
|
22734
|
+
function previewRefusal(answer) {
|
|
22735
|
+
const said2 = sentenceOf(answer.body);
|
|
22736
|
+
const http = `HTTP ${String(answer.status)}${said2 === "" ? "" : `: ${said2}`}`;
|
|
22737
|
+
if (PREVIEW_UNAVAILABLE.has(answer.status) || !isObj3(answer.body)) {
|
|
22738
|
+
return { state: "unchecked", message: `could not be run (${http})` };
|
|
22739
|
+
}
|
|
22740
|
+
if (answer.status >= 500) {
|
|
22741
|
+
return { state: "failed", message: `its handler failed once pushed (${http}); \`app-space logs <app-id>\` says why` };
|
|
22742
|
+
}
|
|
22743
|
+
return { state: "failed", message: `the preview refused it once pushed (${http})` };
|
|
22744
|
+
}
|
|
22745
|
+
function sentenceOf(body) {
|
|
22746
|
+
if (typeof body === "string") return /^<(!doctype html|html)/i.test(body.trimStart()) ? "" : body.trim().slice(0, 300);
|
|
22747
|
+
if (!isObj3(body)) return "";
|
|
22748
|
+
const error = body["error"];
|
|
22749
|
+
if (typeof error === "string") return error;
|
|
22750
|
+
if (isObj3(error) && typeof error["message"] === "string") return error["message"];
|
|
22751
|
+
if (typeof body["detail"] === "string") return body["detail"];
|
|
22752
|
+
const parts = [];
|
|
22753
|
+
for (const [field, value] of Object.entries(body)) {
|
|
22754
|
+
if (field === "success") continue;
|
|
22755
|
+
const said2 = Array.isArray(value) ? value.map((one) => typeof one === "string" ? one : JSON.stringify(one)).join(" ") : typeof value === "string" ? value : "";
|
|
22756
|
+
if (said2 !== "") parts.push(`${field}: ${said2}`);
|
|
22757
|
+
}
|
|
22758
|
+
return parts.join("; ").slice(0, 300);
|
|
22759
|
+
}
|
|
22760
|
+
function firstLine(err) {
|
|
22761
|
+
return (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
|
|
22762
|
+
}
|
|
22763
|
+
function isObj3(value) {
|
|
22764
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22765
|
+
}
|
|
22766
|
+
function obj2(value) {
|
|
22767
|
+
return isObj3(value) ? value : {};
|
|
22768
|
+
}
|
|
22769
|
+
|
|
22770
|
+
// src/commands/appspace/push.ts
|
|
21639
22771
|
var KIT_ENV = "DR_APPSPACE_KIT";
|
|
21640
22772
|
function kitForPush(opts, env = process.env) {
|
|
21641
22773
|
const flag = (opts.kit ?? "").trim();
|
|
@@ -21804,12 +22936,39 @@ function describePostCommit(result) {
|
|
|
21804
22936
|
}
|
|
21805
22937
|
return lines.join("");
|
|
21806
22938
|
}
|
|
22939
|
+
async function bindForPush(client, appId, contract, opts) {
|
|
22940
|
+
const transport = metricsTransport(client);
|
|
22941
|
+
const app = contract?.app;
|
|
22942
|
+
const appName = typeof app?.name === "string" ? app.name : appId;
|
|
22943
|
+
const plan = await planMetrics(contract, transport, { appId, appName });
|
|
22944
|
+
process.stderr.write(renderPlan(plan));
|
|
22945
|
+
if (plan.problems.length > 0) {
|
|
22946
|
+
throw new Error(`the Datarails metrics this app declares cannot be bound here; nothing was created or pushed:
|
|
22947
|
+
${formatProblems(plan.problems)}`);
|
|
22948
|
+
}
|
|
22949
|
+
const wait = metricsWaitOf(opts.metricsWait);
|
|
22950
|
+
if (wait.note !== null) process.stderr.write(`note: ${wait.note}
|
|
22951
|
+
`);
|
|
22952
|
+
const applied = await applyPlan(plan, transport, { waitSeconds: wait.seconds });
|
|
22953
|
+
process.stderr.write(renderApplied(plan, applied));
|
|
22954
|
+
if (!applied.ok) {
|
|
22955
|
+
const kept = applied.created.map((one) => `"${one.name}" (id ${String(one.id)})`).join(", ");
|
|
22956
|
+
throw new Error(
|
|
22957
|
+
`a Datarails metric could not be created, so nothing was pushed${kept === "" ? "" : `; kept, and reused by the next push: ${kept}`}:
|
|
22958
|
+
${formatProblems(applied.problems)}`
|
|
22959
|
+
);
|
|
22960
|
+
}
|
|
22961
|
+
return applied.bindings;
|
|
22962
|
+
}
|
|
21807
22963
|
function registerPushCommand(parent) {
|
|
21808
22964
|
withJsonOption(
|
|
21809
22965
|
withServerOptions(
|
|
21810
22966
|
parent.command("push [dir]").description("Push a folder to App Space as a draft.").option("--publish", "Publish it too (needs an admin role).").option("--seed <file>", "Seed rows, as JSON. Only fills empty tables.").option("--force", "Push even if the app changed since you fetched it.").option("--no-contract", "Do not extract and attach the app contract.").option(
|
|
21811
22967
|
"--kit <asset>",
|
|
21812
22968
|
`The builder kit that made this app, e.g. appspace-app-kit@12 (default: $${KIT_ENV}). Recorded as the contract row's kit_version; omitted when neither is set.`
|
|
22969
|
+
).option(
|
|
22970
|
+
"--metrics-wait <seconds>",
|
|
22971
|
+
"When the contract declares Datarails metrics (data.metrics) and this push creates a BASE one, how long to wait for it to extract before pushing anyway (default 60, at most 120)."
|
|
21813
22972
|
)
|
|
21814
22973
|
)
|
|
21815
22974
|
).action(async (dirArg, opts) => {
|
|
@@ -21851,6 +23010,9 @@ async function pushBody(dir, opts) {
|
|
|
21851
23010
|
if (opts.force !== true && typeof state.revision === "number") {
|
|
21852
23011
|
body["expected_revision"] = state.revision;
|
|
21853
23012
|
}
|
|
23013
|
+
if (opts.metricBindings !== void 0 && Object.keys(opts.metricBindings).length > 0) {
|
|
23014
|
+
body["metric_bindings"] = opts.metricBindings;
|
|
23015
|
+
}
|
|
21854
23016
|
return { appId, body, handlers };
|
|
21855
23017
|
}
|
|
21856
23018
|
function chartsWithoutEcharts(files) {
|
|
@@ -21872,6 +23034,9 @@ async function pushFolder(dir, opts) {
|
|
|
21872
23034
|
const { appId, body, handlers } = await pushBody(dir, opts);
|
|
21873
23035
|
const client = await connect(opts);
|
|
21874
23036
|
try {
|
|
23037
|
+
if (opts.metricBindings === void 0 && Object.keys(declaredMetrics(body["contract"])).length > 0) {
|
|
23038
|
+
body["metric_bindings"] = await bindForPush(client, appId, body["contract"], opts);
|
|
23039
|
+
}
|
|
21875
23040
|
let result;
|
|
21876
23041
|
try {
|
|
21877
23042
|
result = await client.request(`/apps/${encodeURIComponent(appId)}/package`, {
|
|
@@ -21888,7 +23053,8 @@ async function pushFolder(dir, opts) {
|
|
|
21888
23053
|
const outcome = {
|
|
21889
23054
|
revision: result.revision,
|
|
21890
23055
|
stage: result.stage,
|
|
21891
|
-
code_version: typeof result.code_version === "number" ? result.code_version : null
|
|
23056
|
+
code_version: typeof result.code_version === "number" ? result.code_version : null,
|
|
23057
|
+
...result.metric_bindings === void 0 ? {} : { metric_bindings: result.metric_bindings }
|
|
21892
23058
|
};
|
|
21893
23059
|
await recordRevision(dir, result.revision, result.stage, handlers, {
|
|
21894
23060
|
id: appId,
|
|
@@ -21904,6 +23070,8 @@ async function pushFolder(dir, opts) {
|
|
|
21904
23070
|
`
|
|
21905
23071
|
);
|
|
21906
23072
|
process.stdout.write(describePostCommit(result));
|
|
23073
|
+
for (const line2 of bindingWarnings(result.metric_bindings)) process.stdout.write(`warning: ${line2}
|
|
23074
|
+
`);
|
|
21907
23075
|
process.stdout.write(describeContract(result.contract));
|
|
21908
23076
|
process.stdout.write(describeIgnoredName(result.name_ignored));
|
|
21909
23077
|
process.stdout.write(describeSeeding(result));
|
|
@@ -21983,9 +23151,9 @@ async function writeReport(dir, report) {
|
|
|
21983
23151
|
}
|
|
21984
23152
|
async function ownedFiles(dir) {
|
|
21985
23153
|
try {
|
|
21986
|
-
const
|
|
21987
|
-
if (!Array.isArray(
|
|
21988
|
-
return new Set(
|
|
23154
|
+
const parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
|
|
23155
|
+
if (!Array.isArray(parsed2.files)) return /* @__PURE__ */ new Set();
|
|
23156
|
+
return new Set(parsed2.files.map(String));
|
|
21989
23157
|
} catch {
|
|
21990
23158
|
return /* @__PURE__ */ new Set();
|
|
21991
23159
|
}
|
|
@@ -21995,8 +23163,8 @@ function digestOf(text3) {
|
|
|
21995
23163
|
}
|
|
21996
23164
|
async function recordedDigests(dir) {
|
|
21997
23165
|
try {
|
|
21998
|
-
const
|
|
21999
|
-
const held =
|
|
23166
|
+
const parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
|
|
23167
|
+
const held = parsed2.digests;
|
|
22000
23168
|
if (typeof held !== "object" || held === null || Array.isArray(held)) return {};
|
|
22001
23169
|
return Object.fromEntries(Object.entries(held).filter((entry) => typeof entry[1] === "string"));
|
|
22002
23170
|
} catch {
|
|
@@ -22004,21 +23172,21 @@ async function recordedDigests(dir) {
|
|
|
22004
23172
|
}
|
|
22005
23173
|
}
|
|
22006
23174
|
async function recordAsInput(dir, path3) {
|
|
22007
|
-
let
|
|
23175
|
+
let parsed2;
|
|
22008
23176
|
try {
|
|
22009
|
-
|
|
23177
|
+
parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
|
|
22010
23178
|
} catch {
|
|
22011
23179
|
return;
|
|
22012
23180
|
}
|
|
22013
|
-
if (typeof
|
|
22014
|
-
const list3 = (key) => Array.isArray(
|
|
22015
|
-
|
|
22016
|
-
|
|
22017
|
-
const digests =
|
|
23181
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return;
|
|
23182
|
+
const list3 = (key) => Array.isArray(parsed2[key]) ? parsed2[key].map(String) : [];
|
|
23183
|
+
parsed2["files"] = list3("files").filter((one) => one !== path3);
|
|
23184
|
+
parsed2["inputs"] = [.../* @__PURE__ */ new Set([...list3("inputs"), path3])].sort();
|
|
23185
|
+
const digests = parsed2["digests"];
|
|
22018
23186
|
if (typeof digests === "object" && digests !== null && !Array.isArray(digests)) {
|
|
22019
23187
|
delete digests[path3];
|
|
22020
23188
|
}
|
|
22021
|
-
await writeFile4(reportPath(dir), JSON.stringify(
|
|
23189
|
+
await writeFile4(reportPath(dir), JSON.stringify(parsed2, null, 2) + "\n");
|
|
22022
23190
|
}
|
|
22023
23191
|
|
|
22024
23192
|
// src/commands/appspace/get.ts
|
|
@@ -22039,8 +23207,8 @@ function requireBundlePath(name) {
|
|
|
22039
23207
|
async function unpushedInputs(dir, pkg) {
|
|
22040
23208
|
let inputs = [];
|
|
22041
23209
|
try {
|
|
22042
|
-
const
|
|
22043
|
-
if (Array.isArray(
|
|
23210
|
+
const parsed2 = JSON.parse(await readFile9(reportPath(dir), "utf8"));
|
|
23211
|
+
if (Array.isArray(parsed2.inputs)) inputs = parsed2.inputs.map(String);
|
|
22044
23212
|
} catch {
|
|
22045
23213
|
return [];
|
|
22046
23214
|
}
|
|
@@ -22305,7 +23473,7 @@ import { cp, mkdir as mkdir5, mkdtemp as mkdtemp2, rm as rm3, writeFile as write
|
|
|
22305
23473
|
import { tmpdir as tmpdir3 } from "os";
|
|
22306
23474
|
import { dirname as dirname4, join as join11, resolve as resolve5, sep as sep2 } from "path";
|
|
22307
23475
|
var WHAT = "package";
|
|
22308
|
-
async function checkPackage(dir, files, contract, opts, online) {
|
|
23476
|
+
async function checkPackage(dir, files, contract, opts, online, pendingMetricKeys = []) {
|
|
22309
23477
|
if (!online) {
|
|
22310
23478
|
return skip("no -s server to run the push's own checks against; the push is the first to run them");
|
|
22311
23479
|
}
|
|
@@ -22317,13 +23485,13 @@ async function checkPackage(dir, files, contract, opts, online) {
|
|
|
22317
23485
|
try {
|
|
22318
23486
|
payload = await pushBody(scratch, { ...opts, contract: true });
|
|
22319
23487
|
} catch (err) {
|
|
22320
|
-
return skip(`the push body could not be built (${
|
|
23488
|
+
return skip(`the push body could not be built (${firstLine2(err)})`);
|
|
22321
23489
|
}
|
|
22322
23490
|
let client;
|
|
22323
23491
|
try {
|
|
22324
23492
|
client = await connect(opts);
|
|
22325
23493
|
} catch (err) {
|
|
22326
|
-
return skip(`could not reach the server (${
|
|
23494
|
+
return skip(`could not reach the server (${firstLine2(err)})`);
|
|
22327
23495
|
}
|
|
22328
23496
|
try {
|
|
22329
23497
|
const path3 = `/apps/${encodeURIComponent(payload.appId)}/package/validate`;
|
|
@@ -22336,9 +23504,9 @@ async function checkPackage(dir, files, contract, opts, online) {
|
|
|
22336
23504
|
body: JSON.stringify(payload.body)
|
|
22337
23505
|
});
|
|
22338
23506
|
} catch (err) {
|
|
22339
|
-
return skip(`the server did not answer (${
|
|
23507
|
+
return skip(`the server did not answer (${firstLine2(err)})`);
|
|
22340
23508
|
}
|
|
22341
|
-
return interpret(res.status, parse(res.body), path3, contract);
|
|
23509
|
+
return setAsidePending(interpret(res.status, parse(res.body), path3, contract), pendingMetricKeys);
|
|
22342
23510
|
} finally {
|
|
22343
23511
|
client.cleanup();
|
|
22344
23512
|
}
|
|
@@ -22346,11 +23514,28 @@ async function checkPackage(dir, files, contract, opts, online) {
|
|
|
22346
23514
|
await rm3(scratch, { recursive: true, force: true });
|
|
22347
23515
|
}
|
|
22348
23516
|
}
|
|
23517
|
+
function setAsidePending(check5, pending) {
|
|
23518
|
+
if (pending.length === 0) return check5;
|
|
23519
|
+
const mentions3 = (message) => pending.some((key) => new RegExp(`metric_bindings\\.${key.replace(/[^a-z0-9_]/g, "")}(?![a-z0-9_])`).test(message));
|
|
23520
|
+
const kept = check5.problems.filter((one) => !mentions3(one.message));
|
|
23521
|
+
if (kept.length === check5.problems.length) return check5;
|
|
23522
|
+
const keys2 = pending.map((key) => `"${key}"`).join(", ");
|
|
23523
|
+
return {
|
|
23524
|
+
problems: kept,
|
|
23525
|
+
skipped: [
|
|
23526
|
+
...check5.skipped,
|
|
23527
|
+
{
|
|
23528
|
+
what: "metric bindings",
|
|
23529
|
+
why: `${keys2} ${pending.length === 1 ? "is" : "are"} created on push and ${pending.length === 1 ? "has" : "have"} no id to bind yet` + (kept.length === 0 ? "; the platform's checks after its metric check (a data-model change, the revision) did not run" : "")
|
|
23530
|
+
}
|
|
23531
|
+
]
|
|
23532
|
+
};
|
|
23533
|
+
}
|
|
22349
23534
|
function interpret(status, body, path3, contract) {
|
|
22350
23535
|
if (typeof body === "string" && /^<(!doctype html|html)/i.test(body.trimStart())) {
|
|
22351
23536
|
return skip(`the request was intercepted before reaching App Space (HTTP ${String(status)}, an HTML page)`);
|
|
22352
23537
|
}
|
|
22353
|
-
if (status === 200 &&
|
|
23538
|
+
if (status === 200 && isObj4(body)) {
|
|
22354
23539
|
const answer = body;
|
|
22355
23540
|
if (answer.ok === true) return { problems: [], skipped: [] };
|
|
22356
23541
|
if (answer.ok === false) {
|
|
@@ -22399,7 +23584,7 @@ function fromRefusal(status, detail, contract) {
|
|
|
22399
23584
|
};
|
|
22400
23585
|
});
|
|
22401
23586
|
}
|
|
22402
|
-
if (
|
|
23587
|
+
if (isObj4(detail) && detail["error"] === "stale_revision") {
|
|
22403
23588
|
return [
|
|
22404
23589
|
{
|
|
22405
23590
|
code: "server:stale-revision",
|
|
@@ -22409,7 +23594,7 @@ function fromRefusal(status, detail, contract) {
|
|
|
22409
23594
|
}
|
|
22410
23595
|
];
|
|
22411
23596
|
}
|
|
22412
|
-
if (
|
|
23597
|
+
if (isObj4(detail) && Array.isArray(detail["seed"])) {
|
|
22413
23598
|
return detail["seed"].map((line2) => ({
|
|
22414
23599
|
code: "server:seed",
|
|
22415
23600
|
pointer: "/",
|
|
@@ -22417,7 +23602,7 @@ function fromRefusal(status, detail, contract) {
|
|
|
22417
23602
|
fix: "Fix the --seed file: every key a declared table, every row an object."
|
|
22418
23603
|
}));
|
|
22419
23604
|
}
|
|
22420
|
-
if (
|
|
23605
|
+
if (isObj4(detail) && Array.isArray(detail["problems"])) {
|
|
22421
23606
|
const hint = typeof detail["hint"] === "string" ? detail["hint"] : null;
|
|
22422
23607
|
return detail["problems"].map((line2) => fromLine(String(line2), refused, tables, hint));
|
|
22423
23608
|
}
|
|
@@ -22468,11 +23653,11 @@ async function stage(dir, scratch, files) {
|
|
|
22468
23653
|
}
|
|
22469
23654
|
function tablesOf2(contract) {
|
|
22470
23655
|
const out = /* @__PURE__ */ new Map();
|
|
22471
|
-
const app =
|
|
22472
|
-
const model =
|
|
22473
|
-
const tables =
|
|
23656
|
+
const app = isObj4(contract) && isObj4(contract["app"]) ? contract["app"] : {};
|
|
23657
|
+
const model = isObj4(app["data_model"]) ? app["data_model"] : {};
|
|
23658
|
+
const tables = isObj4(model["tables"]) ? model["tables"] : {};
|
|
22474
23659
|
for (const [name, table] of Object.entries(tables)) {
|
|
22475
|
-
const columns =
|
|
23660
|
+
const columns = isObj4(table) && isObj4(table["columns"]) ? table["columns"] : {};
|
|
22476
23661
|
out.set(name, new Set(Object.keys(columns)));
|
|
22477
23662
|
}
|
|
22478
23663
|
return out;
|
|
@@ -22490,15 +23675,15 @@ function parse(bytes) {
|
|
|
22490
23675
|
}
|
|
22491
23676
|
}
|
|
22492
23677
|
function detailOf(body) {
|
|
22493
|
-
return
|
|
23678
|
+
return isObj4(body) ? body["detail"] : void 0;
|
|
22494
23679
|
}
|
|
22495
23680
|
function sentence(text3) {
|
|
22496
23681
|
return text3.endsWith(".") ? text3 : `${text3}.`;
|
|
22497
23682
|
}
|
|
22498
|
-
function
|
|
23683
|
+
function firstLine2(err) {
|
|
22499
23684
|
return (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
|
|
22500
23685
|
}
|
|
22501
|
-
function
|
|
23686
|
+
function isObj4(value) {
|
|
22502
23687
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22503
23688
|
}
|
|
22504
23689
|
|
|
@@ -22540,8 +23725,8 @@ function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
|
|
|
22540
23725
|
else delete page.tree;
|
|
22541
23726
|
}
|
|
22542
23727
|
if (out.bindings !== void 0) out.bindings = renameDeep(out.bindings, rename2);
|
|
22543
|
-
for (const
|
|
22544
|
-
if (typeof
|
|
23728
|
+
for (const problem6 of Array.isArray(out.problems) ? out.problems : []) {
|
|
23729
|
+
if (typeof problem6.where === "string") problem6.where = fileOf(problem6.where);
|
|
22545
23730
|
}
|
|
22546
23731
|
return out;
|
|
22547
23732
|
}
|
|
@@ -22946,7 +24131,7 @@ function specSource(data, card) {
|
|
|
22946
24131
|
if (!isPlainObject11(row)) return;
|
|
22947
24132
|
cite(row.source);
|
|
22948
24133
|
if (typeof row.data === "string") readsOf(row.data);
|
|
22949
|
-
for (const next of [...
|
|
24134
|
+
for (const next of [...listOf2(row.depends), ...listOf2(row.about)]) visit(next);
|
|
22950
24135
|
};
|
|
22951
24136
|
visit(card);
|
|
22952
24137
|
return { refs, sentence: parts.join("; ") };
|
|
@@ -22955,7 +24140,7 @@ function lookupOf(step) {
|
|
|
22955
24140
|
if (!isPlainObject11(step)) return [];
|
|
22956
24141
|
return Object.values(step).flatMap((value) => isPlainObject11(value) && typeof value.lookup === "string" ? [value.lookup] : []);
|
|
22957
24142
|
}
|
|
22958
|
-
function
|
|
24143
|
+
function listOf2(value) {
|
|
22959
24144
|
return Array.isArray(value) ? value : [];
|
|
22960
24145
|
}
|
|
22961
24146
|
function named6(value) {
|
|
@@ -23258,7 +24443,7 @@ function checkPage2(app, page, kit, runtime = null) {
|
|
|
23258
24443
|
const missing = runtime === null ? [] : missingLibraries(page).map((one) => one.module);
|
|
23259
24444
|
const library = runtime === null ? loadsModule(page.path, page.text, WIDGET_LIBRARY2) : !missing.includes("dr-app-runtime") && !missing.includes("dr-app-widgets");
|
|
23260
24445
|
violations.push(...checkLibraryDraws(page, library, scripts, runtime === null ? null : missing));
|
|
23261
|
-
if (runtime !== null) for (const
|
|
24446
|
+
if (runtime !== null) for (const problem6 of libraryOrderProblems(page)) violations.push({ rule: "R2.6", where: page.path, message: problem6 });
|
|
23262
24447
|
if (widgets.length > 0 && !library && !/drInsights\.setData\s*\(/.test(scriptText)) {
|
|
23263
24448
|
violations.push({
|
|
23264
24449
|
rule: "R3.4",
|
|
@@ -24050,10 +25235,10 @@ var PAGE_PATH4 = /\.html?$/i;
|
|
|
24050
25235
|
var AppLoadError2 = class extends Error {
|
|
24051
25236
|
};
|
|
24052
25237
|
function loadApp2(dir) {
|
|
24053
|
-
const manifest =
|
|
24054
|
-
const dataModel = existsSync2(join13(dir, DATA_MODEL_PATH2)) ?
|
|
25238
|
+
const manifest = readJson3(dir, "manifest.json", true);
|
|
25239
|
+
const dataModel = existsSync2(join13(dir, DATA_MODEL_PATH2)) ? readJson3(dir, DATA_MODEL_PATH2, false) : null;
|
|
24055
25240
|
const handlersSource = existsSync2(join13(dir, "handlers.json")) ? "handlers.json" : "manifest.json";
|
|
24056
|
-
const handlersIndex = handlersSource === "handlers.json" ?
|
|
25241
|
+
const handlersIndex = handlersSource === "handlers.json" ? readJson3(dir, "handlers.json", false) : manifest.handlers ?? {};
|
|
24057
25242
|
if (handlersIndex === null || typeof handlersIndex !== "object" || Array.isArray(handlersIndex)) {
|
|
24058
25243
|
throw new AppLoadError2(`${handlersSource} must be an object mapping handler names to {http, file}`);
|
|
24059
25244
|
}
|
|
@@ -24082,7 +25267,7 @@ function readHandlerSource2(dir, file) {
|
|
|
24082
25267
|
if (!existsSync2(full) || !statSync2(full).isFile()) return "";
|
|
24083
25268
|
return readFileSync4(full, "utf8");
|
|
24084
25269
|
}
|
|
24085
|
-
function
|
|
25270
|
+
function readJson3(dir, path3, required) {
|
|
24086
25271
|
const full = join13(dir, path3);
|
|
24087
25272
|
if (!existsSync2(full)) {
|
|
24088
25273
|
if (required) throw new AppLoadError2(`${path3} is missing from the app version`);
|
|
@@ -25785,7 +26970,7 @@ function labelFor2(node, text3, tree) {
|
|
|
25785
26970
|
}
|
|
25786
26971
|
function textOf2(text3, node) {
|
|
25787
26972
|
const next = text3.indexOf("<", node.end);
|
|
25788
|
-
return
|
|
26973
|
+
return decodeEntities3(text3.slice(node.end, next === -1 ? void 0 : next).replace(/\s+/g, " ").trim());
|
|
25789
26974
|
}
|
|
25790
26975
|
function slotsOf2(node, tree) {
|
|
25791
26976
|
if (!(WIDGET_ATTR2 in node.attrs)) return {};
|
|
@@ -26252,7 +27437,7 @@ function specTransforms2(handlers, spec, problems) {
|
|
|
26252
27437
|
out.push({ path: path3, text: text3 });
|
|
26253
27438
|
}
|
|
26254
27439
|
}
|
|
26255
|
-
const unreadable = problems.some((
|
|
27440
|
+
const unreadable = problems.some((problem6) => problem6.code === "transform_unreadable");
|
|
26256
27441
|
for (const key of Object.keys(spec.table).filter(isWidgetKey2).sort(natural5)) {
|
|
26257
27442
|
const path3 = isPlainObject12(spec.table[key]) ? spec.table[key].transform : void 0;
|
|
26258
27443
|
if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
|
|
@@ -26548,7 +27733,7 @@ function natural5(a, b) {
|
|
|
26548
27733
|
}
|
|
26549
27734
|
var NAMED_ENTITIES2 = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
26550
27735
|
var ENTITY2 = /&(#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]{1,31});/g;
|
|
26551
|
-
function
|
|
27736
|
+
function decodeEntities3(value) {
|
|
26552
27737
|
const text3 = String(value);
|
|
26553
27738
|
if (!text3.includes("&")) return text3;
|
|
26554
27739
|
return text3.replace(ENTITY2, (whole, body) => {
|
|
@@ -26561,7 +27746,7 @@ function decodeEntities2(value) {
|
|
|
26561
27746
|
function decodedTree2(html) {
|
|
26562
27747
|
const tree = parseTree2(html);
|
|
26563
27748
|
for (const node of tree.nodes) {
|
|
26564
|
-
for (const [name, value] of Object.entries(node.attrs)) node.attrs[name] =
|
|
27749
|
+
for (const [name, value] of Object.entries(node.attrs)) node.attrs[name] = decodeEntities3(value);
|
|
26565
27750
|
}
|
|
26566
27751
|
return tree;
|
|
26567
27752
|
}
|
|
@@ -26598,9 +27783,9 @@ var MAX_HINT3 = 120;
|
|
|
26598
27783
|
function validate3(instance, schema, options = {}) {
|
|
26599
27784
|
const problems = [];
|
|
26600
27785
|
check3(instance, schema, "", schema, problems, { quote: options.quote === true, inBranch: false });
|
|
26601
|
-
return problems.map((
|
|
27786
|
+
return problems.map((problem6) => `${problem6.at}: ${problem6.why}`);
|
|
26602
27787
|
}
|
|
26603
|
-
function
|
|
27788
|
+
function problem5(at, why, kind = "other") {
|
|
26604
27789
|
return { at: at || "/", why, kind };
|
|
26605
27790
|
}
|
|
26606
27791
|
function describe4(node, ctx) {
|
|
@@ -26635,7 +27820,7 @@ function check3(instance, node, at, root, problems, ctx) {
|
|
|
26635
27820
|
if (typeof node.$ref === "string") {
|
|
26636
27821
|
const target = resolve10(node.$ref, root);
|
|
26637
27822
|
if (target === null) {
|
|
26638
|
-
problems.push(
|
|
27823
|
+
problems.push(problem5(at, "the schema names a reference it cannot resolve"));
|
|
26639
27824
|
return;
|
|
26640
27825
|
}
|
|
26641
27826
|
check3(instance, target, at, root, problems, ctx);
|
|
@@ -26643,29 +27828,29 @@ function check3(instance, node, at, root, problems, ctx) {
|
|
|
26643
27828
|
if ("type" in node) {
|
|
26644
27829
|
const names = Array.isArray(node.type) ? node.type : [node.type];
|
|
26645
27830
|
if (!names.some((name) => isType3(instance, name))) {
|
|
26646
|
-
problems.push(
|
|
27831
|
+
problems.push(problem5(at, `not of type ${names.join(" or ")}${hint}`, "type"));
|
|
26647
27832
|
return;
|
|
26648
27833
|
}
|
|
26649
27834
|
}
|
|
26650
|
-
if ("const" in node && !same3(instance, node.const)) problems.push(
|
|
27835
|
+
if ("const" in node && !same3(instance, node.const)) problems.push(problem5(at, `not the constant this contract fixes${hint}`, "value"));
|
|
26651
27836
|
if (Array.isArray(node.enum) && !node.enum.some((one) => same3(instance, one))) {
|
|
26652
|
-
problems.push(
|
|
27837
|
+
problems.push(problem5(at, `not one of the values this contract allows${hint}`, "value"));
|
|
26653
27838
|
}
|
|
26654
27839
|
if (typeof node.minimum === "number" && isType3(instance, "number") && instance < node.minimum) {
|
|
26655
|
-
problems.push(
|
|
27840
|
+
problems.push(problem5(at, `below the minimum this contract sets${hint}`));
|
|
26656
27841
|
}
|
|
26657
27842
|
if (typeof node.pattern === "string" && typeof instance === "string" && !new RegExp(node.pattern, "u").test(instance)) {
|
|
26658
27843
|
if (ctx.inBranch) {
|
|
26659
|
-
problems.push(
|
|
27844
|
+
problems.push(problem5(at, `${ctx.quote ? `${JSON.stringify(instance)} ` : ""}does not match ${node.pattern}${hint}`));
|
|
26660
27845
|
} else {
|
|
26661
|
-
problems.push(
|
|
27846
|
+
problems.push(problem5(at, "not in the form this contract requires"));
|
|
26662
27847
|
}
|
|
26663
27848
|
}
|
|
26664
27849
|
if (typeof node.minLength === "number" && typeof instance === "string" && [...instance].length < node.minLength) {
|
|
26665
|
-
problems.push(
|
|
27850
|
+
problems.push(problem5(at, `shorter than this contract allows${hint}`));
|
|
26666
27851
|
}
|
|
26667
27852
|
if (typeof node.maxLength === "number" && typeof instance === "string" && [...instance].length > node.maxLength) {
|
|
26668
|
-
problems.push(
|
|
27853
|
+
problems.push(problem5(at, `longer than this contract allows${hint}`));
|
|
26669
27854
|
}
|
|
26670
27855
|
if (isObject5(instance)) checkObject3(instance, node, at, root, problems, ctx);
|
|
26671
27856
|
if (Array.isArray(instance)) checkArray3(instance, node, at, root, problems, ctx, hint);
|
|
@@ -26701,16 +27886,16 @@ function checkBranches3(instance, branches, at, root, problems, ctx, wanted, sta
|
|
|
26701
27886
|
if (kept.length > MAX_PER_PLACE3) {
|
|
26702
27887
|
const more = kept.length - MAX_PER_PLACE3;
|
|
26703
27888
|
kept = kept.slice(0, MAX_PER_PLACE3);
|
|
26704
|
-
kept.push(
|
|
27889
|
+
kept.push(problem5(at, `and ${more} more problems here`, "more"));
|
|
26705
27890
|
}
|
|
26706
27891
|
for (const one of kept) problems.push(one);
|
|
26707
27892
|
} else {
|
|
26708
|
-
problems.push(
|
|
27893
|
+
problems.push(problem5(at, `matches none of the shapes this contract has (one of: ${shapes3(branches, root)})`));
|
|
26709
27894
|
}
|
|
26710
27895
|
} else if (matched === 0) {
|
|
26711
|
-
problems.push(
|
|
27896
|
+
problems.push(problem5(at, "matches none of the shapes this contract has"));
|
|
26712
27897
|
} else if (wanted !== null && matched > wanted) {
|
|
26713
|
-
problems.push(
|
|
27898
|
+
problems.push(problem5(at, "matches more than one shape this contract has"));
|
|
26714
27899
|
}
|
|
26715
27900
|
}
|
|
26716
27901
|
function shapeMisses3(found, at) {
|
|
@@ -26752,7 +27937,7 @@ function checkObject3(instance, node, at, root, problems, ctx) {
|
|
|
26752
27937
|
for (const name of Array.isArray(node.required) ? node.required : []) {
|
|
26753
27938
|
if (Object.prototype.hasOwnProperty.call(instance, name)) continue;
|
|
26754
27939
|
const wanted = Object.prototype.hasOwnProperty.call(properties, name) ? properties[name] : null;
|
|
26755
|
-
problems.push(
|
|
27940
|
+
problems.push(problem5(`${at}/${escape3(name)}`, `required and absent${describe4(wanted, ctx)}`, "required"));
|
|
26756
27941
|
}
|
|
26757
27942
|
for (const [name, value] of Object.entries(instance)) {
|
|
26758
27943
|
const where2 = `${at}/${escape3(name)}`;
|
|
@@ -26761,12 +27946,12 @@ function checkObject3(instance, node, at, root, problems, ctx) {
|
|
|
26761
27946
|
continue;
|
|
26762
27947
|
}
|
|
26763
27948
|
const extra = node.additionalProperties ?? true;
|
|
26764
|
-
if (extra === false) problems.push(
|
|
27949
|
+
if (extra === false) problems.push(problem5(where2, "not a key this contract carries"));
|
|
26765
27950
|
else if (isObject5(extra)) check3(value, extra, node["x-tenant-keys"] === true && !ctx.quote ? `${at}/*` : where2, root, problems, ctx);
|
|
26766
27951
|
}
|
|
26767
27952
|
}
|
|
26768
27953
|
function checkArray3(instance, node, at, root, problems, ctx, hint) {
|
|
26769
|
-
if (typeof node.minItems === "number" && instance.length < node.minItems) problems.push(
|
|
27954
|
+
if (typeof node.minItems === "number" && instance.length < node.minItems) problems.push(problem5(at, `shorter than this contract allows${hint}`));
|
|
26770
27955
|
if (isObject5(node.items)) instance.forEach((value, index) => check3(value, node.items, `${at}/${index}`, root, problems, ctx));
|
|
26771
27956
|
}
|
|
26772
27957
|
function isType3(instance, name) {
|
|
@@ -27253,8 +28438,8 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
|
|
|
27253
28438
|
if (produced.includes(key)) continue;
|
|
27254
28439
|
const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
|
|
27255
28440
|
const fix = reads.generated ? `Add a control that sets it, or take "${key}" out of the handler's required in contract.json.` : `Add a control that sets it, or read it with payload.get("${key}", <default>).`;
|
|
27256
|
-
const
|
|
27257
|
-
out.push({ handler: name, key, pages, produced, generated: reads.generated, problem:
|
|
28441
|
+
const problem6 = `handler "${name}" reads payload "${key}", and nothing on ${either4(pages)} that calls it produces that key \u2014 ${there}.`;
|
|
28442
|
+
out.push({ handler: name, key, pages, produced, generated: reads.generated, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
27258
28443
|
}
|
|
27259
28444
|
}
|
|
27260
28445
|
return out;
|
|
@@ -27288,9 +28473,9 @@ function undeclaredCustomCalls2(contract, { moduleSource = () => void 0 } = {})
|
|
|
27288
28473
|
}
|
|
27289
28474
|
function undeclaredCallMessage2(id, module, handler, calls, how) {
|
|
27290
28475
|
const listed = calls.length === 0 ? "lists none" : `lists ${calls.join(", ")}`;
|
|
27291
|
-
const
|
|
28476
|
+
const problem6 = `custom "${id}" calls handler "${handler}" (ui.call ${how}, ui/${module}), and its declares.calls ${listed}.`;
|
|
27292
28477
|
const fix = `Add "${handler}" to declares.calls \u2014 and what it sends to declares.produces \u2014 or stop calling it.`;
|
|
27293
|
-
return { problem:
|
|
28478
|
+
return { problem: problem6, fix, message: `${problem6} ${fix}` };
|
|
27294
28479
|
}
|
|
27295
28480
|
function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
|
|
27296
28481
|
const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
|
|
@@ -27333,9 +28518,9 @@ function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
|
|
|
27333
28518
|
}
|
|
27334
28519
|
if (read) continue;
|
|
27335
28520
|
const id = idOf3(element, uid);
|
|
27336
|
-
const
|
|
28521
|
+
const problem6 = `file_upload "${id}" writes the stored file under "${field}" to handler "${handler}" (${via}), and "${handler}" never reads payload["${field}"] \u2014 the file is stored and nothing uses it.`;
|
|
27337
28522
|
const fix = role === "custom" ? `Read payload["${field}"]["upload"] in "${handler}" and hand it to the parser (or write payload["${field}"] into a file column), or drop the upload.` : `Add "${field}" to "${handler}"'s fields (a file column), or drop the upload.`;
|
|
27338
|
-
out.push({ uid, id, page: element.page ?? null, field, handler, via, problem:
|
|
28523
|
+
out.push({ uid, id, page: element.page ?? null, field, handler, via, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
27339
28524
|
}
|
|
27340
28525
|
}
|
|
27341
28526
|
return out;
|
|
@@ -27351,9 +28536,9 @@ function stepsWithoutAction2(contract) {
|
|
|
27351
28536
|
const id = idOf3(element, uid);
|
|
27352
28537
|
const name = named8(step.id) ? step.id : String(index);
|
|
27353
28538
|
const next = named8(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
|
|
27354
|
-
const
|
|
28539
|
+
const problem6 = `stepper "${id}" step "${name}" names no action, so no handler moves the flow on from it \u2014 a flow that reaches it stays there.`;
|
|
27355
28540
|
const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
|
|
27356
|
-
out.push({ uid, id, page: element.page ?? null, step: name, index, problem:
|
|
28541
|
+
out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
27357
28542
|
});
|
|
27358
28543
|
}
|
|
27359
28544
|
return out;
|
|
@@ -27854,9 +29039,9 @@ function unboundControls(contract, files = {}) {
|
|
|
27854
29039
|
const param = page.params.find((one) => one?.name === name) ?? {};
|
|
27855
29040
|
const kind = named9(param.kind) ? param.kind : null;
|
|
27856
29041
|
const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
|
|
27857
|
-
const
|
|
29042
|
+
const problem6 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
|
|
27858
29043
|
const fix = `Give it a control \u2014 a part in param mode (props.props.param "${name}": a slider, a number_input, a select, a date_picker or a toggle), or a custom whose module calls ui.setParam("${name}", value) \u2014 or drop the param.`;
|
|
27859
|
-
out.push({ page: page.path, param: name, kind, problem:
|
|
29044
|
+
out.push({ page: page.path, param: name, kind, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
27860
29045
|
}
|
|
27861
29046
|
}
|
|
27862
29047
|
return out;
|
|
@@ -27910,9 +29095,9 @@ function unreadParams(contract, { moduleSource = () => void 0, handlerSource = (
|
|
|
27910
29095
|
continue;
|
|
27911
29096
|
}
|
|
27912
29097
|
const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both2(named_.map((one) => `"${one}"`))}`;
|
|
27913
|
-
const
|
|
29098
|
+
const problem6 = `${page.path} declares param "${name}", and no source on the page honours it (${there}) \u2014 moving it changes nothing on the page.`;
|
|
27914
29099
|
const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
|
|
27915
|
-
out.push({ page: page.path, param: name, sources: [...named_], problem:
|
|
29100
|
+
out.push({ page: page.path, param: name, sources: [...named_], problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
27916
29101
|
}
|
|
27917
29102
|
}
|
|
27918
29103
|
return out;
|
|
@@ -29136,9 +30321,9 @@ function unreachablePages2(contract, files = {}) {
|
|
|
29136
30321
|
else if (roles.length > 0) why = `it is for ${both3(quoted2(roles))}, and no page ${either5(quoted2(roles))} reaches links to it`;
|
|
29137
30322
|
else if (gates.length > 0) why = `every nav entry to it is gated to ${either5(quoted2(gates))}, and no role shown that entry reaches a page carrying it`;
|
|
29138
30323
|
else why = "the only pages that link to it are pages no role reaches";
|
|
29139
|
-
const
|
|
30324
|
+
const problem6 = `${page.path} is a page of the app, and no role reaches it from the nav \u2014 ${why} \u2014 so only a typed URL opens it.`;
|
|
29140
30325
|
const fix = roles.length > 0 || gates.length > 0 ? `Gate its nav entry to a role that should open it (pages[].roles on the page, a role's home, or the element's role), or drop the page.` : "Give the pages a nav (a nav element on each page), or drop the page.";
|
|
29141
|
-
out.push({ page: page.path, problem:
|
|
30326
|
+
out.push({ page: page.path, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29142
30327
|
}
|
|
29143
30328
|
return out;
|
|
29144
30329
|
}
|
|
@@ -29284,9 +30469,9 @@ function unconfirmedStateChanges2(contract, files = {}) {
|
|
|
29284
30469
|
named_.push(one.module ? `${one.what} (on ${one.page})` : `${one.what} "${one.id}" (on ${one.page})`);
|
|
29285
30470
|
}
|
|
29286
30471
|
const kind = handlers[handler]?.role === "decide" ? "decides a row" : "changes a record's state";
|
|
29287
|
-
const
|
|
30472
|
+
const problem6 = `handler "${handler}" ${kind}, and ${both3(named_)} ${named_.length === 1 ? "calls" : "call"} it on one click, with no confirm and no dialog in front of it.`;
|
|
29288
30473
|
const fix = 'Put the decision behind a confirm: a dialog part whose on_confirm is the handler (with the reason as one of its inputs), a row action with "confirm", a map-form decide whose decision takes a note, or \u2014 in a custom module \u2014 ui.confirm before ui.call.';
|
|
29289
|
-
out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem:
|
|
30474
|
+
out.push({ handler, callers: bare.map(({ page, uid, what }) => ({ page, uid, what })), problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29290
30475
|
}
|
|
29291
30476
|
return out;
|
|
29292
30477
|
}
|
|
@@ -29364,18 +30549,18 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
|
|
|
29364
30549
|
for (const [key, pages] of [...sent.entries()].sort()) {
|
|
29365
30550
|
if (read.has(key)) continue;
|
|
29366
30551
|
const where2 = pages.size === 0 ? "the page" : either5([...pages].sort());
|
|
29367
|
-
const
|
|
30552
|
+
const problem6 = `${where2} sends handler "${name}" a "${key}", and the handler never reads it \u2014 the reason a person types is thrown away.`;
|
|
29368
30553
|
const fix = `Read it (payload.get("${key}")) and keep it with the decision \u2014 a column, or the audit note \u2014 or stop asking for it.`;
|
|
29369
|
-
out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem:
|
|
30554
|
+
out.push({ handler: name, key, kind: "unread", pages: [...pages].sort(), problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29370
30555
|
}
|
|
29371
30556
|
const lines = pyLines2(source);
|
|
29372
30557
|
if (!changesState2(name, row, source) || !REJECTS2.test(lines.join("\n"))) continue;
|
|
29373
30558
|
for (const key of REASON_KEYS2) {
|
|
29374
30559
|
if (!reads.optional.includes(key) || reads.required.includes(key)) continue;
|
|
29375
30560
|
if (requiresReason2(lines, key, reasonVars2(lines, key))) continue;
|
|
29376
|
-
const
|
|
30561
|
+
const problem6 = `handler "${name}" rejects (or overrides), and reads "${key}" without ever requiring it \u2014 a rejection goes through with no reason given.`;
|
|
29377
30562
|
const fix = `Refuse the reject path without one: if decision == "rejected" and not ${key}: return {"error": "Give a reason to reject."}.`;
|
|
29378
|
-
out.push({ handler: name, key, kind: "optional", pages: [], problem:
|
|
30563
|
+
out.push({ handler: name, key, kind: "optional", pages: [], problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29379
30564
|
}
|
|
29380
30565
|
}
|
|
29381
30566
|
return out;
|
|
@@ -29412,9 +30597,9 @@ function unliftableRefusals2(contract, { handlerSource = () => void 0 } = {}) {
|
|
|
29412
30597
|
}
|
|
29413
30598
|
if (!refuses) continue;
|
|
29414
30599
|
const shown2 = line2.trim().replace(/:$/, "");
|
|
29415
|
-
const
|
|
30600
|
+
const problem6 = `handler "${name}" refuses outright at line ${si + 1} (${shown2}) on the same path where it demands a "${key}" (line ${gi + 1}), and no reason lifts that refusal \u2014 the page asks for a reason to go ahead, and the reason is ignored.`;
|
|
29416
30601
|
const fix = `Let the reason override it (check "${key}" before refusing, and record it), or tell the reader that this state cannot be overridden and stop asking for a reason.`;
|
|
29417
|
-
out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem:
|
|
30602
|
+
out.push({ handler: name, key, line: si + 1, gateLine: gi + 1, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29418
30603
|
}
|
|
29419
30604
|
}
|
|
29420
30605
|
}
|
|
@@ -29430,9 +30615,9 @@ function rolesLandingNowhere2(contract) {
|
|
|
29430
30615
|
const exists = pagesOf2(contract).some((page) => page.path === landing);
|
|
29431
30616
|
if (exists && opens2(contract, model, role, landing)) continue;
|
|
29432
30617
|
const stated2 = model.home.get(role);
|
|
29433
|
-
const
|
|
30618
|
+
const problem6 = !exists ? `role "${role}" lands on ${landing} (its home), which is not a page of the app.` : stated2 ? `role "${role}" lands on its home, ${landing}, which is for ${both3(quoted2(pageRoles2(pagesOf2(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.` : `role "${role}" states no home and lands on ${landing}, which is for ${both3(quoted2(pageRoles2(pagesOf2(contract).find((page) => page.path === landing))))} \u2014 so the role opens the app on a 404.`;
|
|
29434
30619
|
const fix = `Give "${role}" a home it may open (backend.roles[].home: one of its pages), or add it to that page's pages[].roles.`;
|
|
29435
|
-
out.push({ role, page: landing, problem:
|
|
30620
|
+
out.push({ role, page: landing, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29436
30621
|
}
|
|
29437
30622
|
return out;
|
|
29438
30623
|
}
|
|
@@ -29471,8 +30656,8 @@ function openToEveryRole2(contract) {
|
|
|
29471
30656
|
const out = [];
|
|
29472
30657
|
const say = (kind, names, what, fix) => {
|
|
29473
30658
|
if (names.length === 0) return;
|
|
29474
|
-
const
|
|
29475
|
-
out.push({ kind, names, problem:
|
|
30659
|
+
const problem6 = `${names.length === 1 ? `${what.one} ${names[0]} states` : `${names.length} ${what.many} \u2014 ${both3(names)} \u2014 state`} no roles, so every role (${all}) may ${what.verb} ${names.length === 1 ? "it" : "them"}.`;
|
|
30660
|
+
out.push({ kind, names, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29476
30661
|
};
|
|
29477
30662
|
say(
|
|
29478
30663
|
"page",
|
|
@@ -29859,7 +31044,7 @@ async function runCompile(dir, opts) {
|
|
|
29859
31044
|
for (const fill of layoutFills(contract)) report.compose.push(fill);
|
|
29860
31045
|
}
|
|
29861
31046
|
const built = await compileFolder(dir, kind, contract, kit, loaded.overlay);
|
|
29862
|
-
const refused = built.problems.filter((
|
|
31047
|
+
const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
|
|
29863
31048
|
if (Object.keys(built.files).length === 0 || refused.length > 0) {
|
|
29864
31049
|
const lines = (refused.length > 0 ? refused : built.problems).map(asProblem);
|
|
29865
31050
|
report.validate.problems = lines;
|
|
@@ -29869,6 +31054,30 @@ async function runCompile(dir, opts) {
|
|
|
29869
31054
|
else process.stdout.write(problemHeading(lines.length) + formatProblems(lines) + formatSkipped(skipped));
|
|
29870
31055
|
return 1;
|
|
29871
31056
|
}
|
|
31057
|
+
let metricsPlan = null;
|
|
31058
|
+
if (Object.keys(declaredMetrics(contract)).length > 0) {
|
|
31059
|
+
if (kind !== "app") {
|
|
31060
|
+
skipped.push({ what: "metric bindings", why: "data.metrics binds an app's sources; a dashboard's are not bound yet (design 06 \xA77, step 8)" });
|
|
31061
|
+
} else if (!hasServer(opts)) {
|
|
31062
|
+
skipped.push({ what: "metric bindings", why: "no -s server: data.metrics is bound in each company when the app ships" });
|
|
31063
|
+
} else {
|
|
31064
|
+
const planned = await planMetricsFor(contract, opts);
|
|
31065
|
+
metricsPlan = planned.plan;
|
|
31066
|
+
report.metrics = planned.plan === null ? null : planReport(planned.plan);
|
|
31067
|
+
if (planned.problems.length > 0) {
|
|
31068
|
+
report.validate.problems = planned.problems;
|
|
31069
|
+
report.failure = "validation";
|
|
31070
|
+
await writeReport(dir, report);
|
|
31071
|
+
if (opts.json === true) printJson(report);
|
|
31072
|
+
else {
|
|
31073
|
+
process.stdout.write(
|
|
31074
|
+
(planned.plan === null ? "" : renderPlan(planned.plan)) + problemHeading(planned.problems.length) + formatProblems(planned.problems) + formatSkipped(skipped)
|
|
31075
|
+
);
|
|
31076
|
+
}
|
|
31077
|
+
return 1;
|
|
31078
|
+
}
|
|
31079
|
+
}
|
|
31080
|
+
}
|
|
29872
31081
|
const paths = Object.keys(built.files).sort();
|
|
29873
31082
|
const escaping = paths.filter((path3) => !insideFolder(dir, path3));
|
|
29874
31083
|
if (escaping.length > 0) {
|
|
@@ -29915,12 +31124,12 @@ async function runCompile(dir, opts) {
|
|
|
29915
31124
|
report.digests = Object.fromEntries(plan.owned.map((path3) => [path3, digestOf(built.files[path3] ?? "")]));
|
|
29916
31125
|
report.inputs = plan.inputs;
|
|
29917
31126
|
}
|
|
29918
|
-
return await afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts);
|
|
31127
|
+
return await afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts, metricsPlan);
|
|
29919
31128
|
} finally {
|
|
29920
31129
|
if (check5) await rm4(target, { recursive: true, force: true });
|
|
29921
31130
|
}
|
|
29922
31131
|
}
|
|
29923
|
-
async function afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts) {
|
|
31132
|
+
async function afterWrite(dir, target, kind, contract, built, report, kit, skipped, opts, metricsPlan = null) {
|
|
29924
31133
|
const composed = kind === "dashboard" && !isRuntimeDashboard(contract) ? composeOf(contract) : built.contract ?? contract;
|
|
29925
31134
|
const extracted = await extractContract(target);
|
|
29926
31135
|
const diffs = roundTripDiffs(composed, extracted, built.problems, { order: built.order });
|
|
@@ -29957,21 +31166,29 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
|
|
|
29957
31166
|
return 4;
|
|
29958
31167
|
}
|
|
29959
31168
|
if (target !== dir) {
|
|
29960
|
-
const server = await checkPackage(
|
|
31169
|
+
const server = await checkPackage(
|
|
31170
|
+
dir,
|
|
31171
|
+
built.files,
|
|
31172
|
+
contract,
|
|
31173
|
+
metricsPlan === null ? opts : { ...opts, metricBindings: plannedBindings(metricsPlan) },
|
|
31174
|
+
hasServer(opts),
|
|
31175
|
+
metricsPlan === null ? [] : metricsPlan.entries.filter((entry) => entry.action === "create").map((entry) => entry.key)
|
|
31176
|
+
);
|
|
29961
31177
|
const notChecked3 = [...skipped, ...server.skipped];
|
|
29962
31178
|
if ((opts.expect ?? []).length > 0) notChecked3.push({ what: "headline", why: "--check runs no load" });
|
|
29963
31179
|
report.validate.problems = server.problems;
|
|
29964
31180
|
report.validate.checks_skipped = notChecked3;
|
|
29965
31181
|
if (server.problems.length > 0) report.failure = "push-would-be-refused";
|
|
29966
31182
|
await writeReport(dir, report);
|
|
31183
|
+
const planned = metricsPlan === null ? "" : renderPlan(metricsPlan);
|
|
29967
31184
|
if (opts.json === true) printJson(report);
|
|
29968
31185
|
else if (server.problems.length > 0) {
|
|
29969
31186
|
process.stdout.write(
|
|
29970
|
-
problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
|
|
31187
|
+
planned + problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
|
|
29971
31188
|
);
|
|
29972
31189
|
} else {
|
|
29973
31190
|
process.stdout.write(
|
|
29974
|
-
problemHeading(0) + checkedLine(report, Object.keys(built.files).length) + warningLines(report) + formatSkipped(notChecked3)
|
|
31191
|
+
planned + problemHeading(0) + checkedLine(report, Object.keys(built.files).length) + warningLines(report) + formatSkipped(notChecked3)
|
|
29975
31192
|
);
|
|
29976
31193
|
}
|
|
29977
31194
|
return server.problems.length > 0 ? 1 : 0;
|
|
@@ -29980,9 +31197,31 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
|
|
|
29980
31197
|
await liveness(dir, kind, report, extracted, opts);
|
|
29981
31198
|
return finish(dir, report, skipped, opts);
|
|
29982
31199
|
}
|
|
31200
|
+
let metricBindings;
|
|
31201
|
+
if (metricsPlan !== null) {
|
|
31202
|
+
const created = await applyMetricsFor(metricsPlan, opts);
|
|
31203
|
+
report.metrics = planReport(metricsPlan, created.applied ?? void 0);
|
|
31204
|
+
if (opts.json !== true) process.stdout.write(renderPlan(metricsPlan) + (created.applied === null ? "" : renderApplied(metricsPlan, created.applied)));
|
|
31205
|
+
if (created.problems.length > 0) {
|
|
31206
|
+
report.failure = "metrics-create";
|
|
31207
|
+
report.push = { pushed: false, stage: null, revision: null, contract_source: null };
|
|
31208
|
+
await writeReport(dir, report);
|
|
31209
|
+
if (opts.json === true) printJson(report);
|
|
31210
|
+
else {
|
|
31211
|
+
const kept = (created.applied?.created ?? []).map((one) => `"${one.name}" (id ${String(one.id)})`);
|
|
31212
|
+
const drifted = created.problems.some((one) => one.code === "metric_changed");
|
|
31213
|
+
process.stderr.write(
|
|
31214
|
+
(drifted ? "a Datarails metric this ship wrote does not hold what it wrote when read back, so nothing was pushed" : "a Datarails metric could not be created or changed, so nothing was pushed") + (kept.length === 0 ? ".\n" : `; kept, and reused by the next ship: ${kept.join(", ")}.
|
|
31215
|
+
`) + formatProblems(created.problems)
|
|
31216
|
+
);
|
|
31217
|
+
}
|
|
31218
|
+
return 1;
|
|
31219
|
+
}
|
|
31220
|
+
metricBindings = created.applied?.bindings ?? {};
|
|
31221
|
+
}
|
|
29983
31222
|
let pushed = null;
|
|
29984
31223
|
try {
|
|
29985
|
-
pushed = await pushFolder(dir, { ...opts, force: opts.force });
|
|
31224
|
+
pushed = await pushFolder(dir, { ...opts, force: opts.force, ...metricBindings === void 0 ? {} : { metricBindings } });
|
|
29986
31225
|
} catch (err) {
|
|
29987
31226
|
report.push = { pushed: false, stage: null, revision: null, contract_source: null };
|
|
29988
31227
|
report.failure = isStaleRevision(err) ? "stale-revision" : isLostRace(err) ? "concurrent-publish" : "push-rejected";
|
|
@@ -30006,7 +31245,11 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
|
|
|
30006
31245
|
contract_source: "cli",
|
|
30007
31246
|
...typeof pushed?.code_version === "number" ? { code_version: pushed.code_version } : {}
|
|
30008
31247
|
};
|
|
31248
|
+
if (report.metrics !== void 0 && report.metrics !== null && pushed?.metric_bindings !== void 0) {
|
|
31249
|
+
report.metrics["push"] = pushed.metric_bindings;
|
|
31250
|
+
}
|
|
30009
31251
|
await liveness(dir, kind, report, extracted, opts);
|
|
31252
|
+
await metricsLiveness(report, extracted, opts);
|
|
30010
31253
|
return finish(dir, report, skipped, opts);
|
|
30011
31254
|
}
|
|
30012
31255
|
async function finish(dir, report, skipped, opts) {
|
|
@@ -30207,25 +31450,25 @@ async function writePlan(dir, kind, contract, files, paths, force) {
|
|
|
30207
31450
|
}
|
|
30208
31451
|
function ownedPaths(contract) {
|
|
30209
31452
|
const out = customHandlerPaths(contract);
|
|
30210
|
-
const data =
|
|
30211
|
-
for (const row of [...Object.values(
|
|
31453
|
+
const data = obj3(obj3(contract)["data"]);
|
|
31454
|
+
for (const row of [...Object.values(obj3(data["sources"])), ...Object.values(obj3(data["widgets"]))]) {
|
|
30212
31455
|
const transform = isPlain(row) ? row["transform"] : void 0;
|
|
30213
31456
|
if (typeof transform === "string" && TRANSFORM_PATH2.test(transform)) out.add(transform);
|
|
30214
31457
|
}
|
|
30215
|
-
for (const element of Object.values(
|
|
31458
|
+
for (const element of Object.values(obj3(obj3(contract)["elements"]))) {
|
|
30216
31459
|
if (isPlain(element) && element["type"] === "custom") {
|
|
30217
|
-
const module =
|
|
31460
|
+
const module = obj3(element["props"])["module"];
|
|
30218
31461
|
if (typeof module === "string" && MODULE_RE.test(module)) out.add(`ui/${module}`);
|
|
30219
31462
|
}
|
|
30220
31463
|
}
|
|
30221
|
-
const pages =
|
|
31464
|
+
const pages = obj3(contract)["pages"];
|
|
30222
31465
|
for (const page of Array.isArray(pages) ? pages : []) {
|
|
30223
31466
|
if (isPlain(page) && page["owned"] === true && typeof page["path"] === "string") out.add(page["path"]);
|
|
30224
31467
|
}
|
|
30225
31468
|
return out;
|
|
30226
31469
|
}
|
|
30227
31470
|
function customHandlerPaths(contract) {
|
|
30228
|
-
const handlers =
|
|
31471
|
+
const handlers = obj3(obj3(obj3(obj3(contract)["data"])["app"])["handlers"]);
|
|
30229
31472
|
const out = /* @__PURE__ */ new Set();
|
|
30230
31473
|
for (const [name, row] of Object.entries(handlers)) {
|
|
30231
31474
|
if (isPlain(row) && row["role"] === "custom") out.add(`handlers/${name}.py`);
|
|
@@ -30268,7 +31511,7 @@ function roundTripDiffs(input, extracted, compilerProblems = [], { order: order2
|
|
|
30268
31511
|
const read = order2 === void 0 ? {} : { order: "keys" };
|
|
30269
31512
|
const left = stripPositional(normaliseApp(input), stated2);
|
|
30270
31513
|
const right = stripPositional(normaliseApp(withoutUnstatedChrome(input, extracted)), read);
|
|
30271
|
-
const declared = compilerProblems.some((
|
|
31514
|
+
const declared = compilerProblems.some((problem6) => problem6.code === DECLARED_LOSS);
|
|
30272
31515
|
const diffs = [];
|
|
30273
31516
|
for (const section of ["app", "pages", "elements", "data"]) {
|
|
30274
31517
|
if (!(section in left)) continue;
|
|
@@ -30343,7 +31586,7 @@ function normaliseApp(contract) {
|
|
|
30343
31586
|
}
|
|
30344
31587
|
const model = record2["data_model"];
|
|
30345
31588
|
if (isPlain(model)) {
|
|
30346
|
-
for (const table of Object.values(
|
|
31589
|
+
for (const table of Object.values(obj3(model["tables"]))) {
|
|
30347
31590
|
if (!isPlain(table)) continue;
|
|
30348
31591
|
const columns = table["columns"];
|
|
30349
31592
|
if (!isPlain(columns)) continue;
|
|
@@ -30363,7 +31606,7 @@ function normaliseApp(contract) {
|
|
|
30363
31606
|
}
|
|
30364
31607
|
}
|
|
30365
31608
|
}
|
|
30366
|
-
const roles =
|
|
31609
|
+
const roles = obj3(out["backend"])["roles"];
|
|
30367
31610
|
if (Array.isArray(roles)) {
|
|
30368
31611
|
for (const role of roles) {
|
|
30369
31612
|
if (isPlain(role) && Array.isArray(role["sees"]) && role["sees"].length === 0) delete role["sees"];
|
|
@@ -30371,7 +31614,7 @@ function normaliseApp(contract) {
|
|
|
30371
31614
|
}
|
|
30372
31615
|
return out;
|
|
30373
31616
|
}
|
|
30374
|
-
function
|
|
31617
|
+
function obj3(value) {
|
|
30375
31618
|
return isPlain(value) ? value : {};
|
|
30376
31619
|
}
|
|
30377
31620
|
function withoutLibrary(section) {
|
|
@@ -30715,7 +31958,7 @@ async function recordPulled(dir, contractWritten) {
|
|
|
30715
31958
|
} catch {
|
|
30716
31959
|
return forget();
|
|
30717
31960
|
}
|
|
30718
|
-
if (Object.keys(built.files).length === 0 || built.problems.some((
|
|
31961
|
+
if (Object.keys(built.files).length === 0 || built.problems.some((problem6) => REFUSALS.has(problem6.code))) {
|
|
30719
31962
|
return forget();
|
|
30720
31963
|
}
|
|
30721
31964
|
const custom = ownedPaths(contract);
|
|
@@ -30821,21 +32064,21 @@ async function pushedState(dir) {
|
|
|
30821
32064
|
const raw = await readIfPresent(join15(dir, ".appspace.json"));
|
|
30822
32065
|
if (raw === void 0) return { stage: null, revision: null };
|
|
30823
32066
|
try {
|
|
30824
|
-
const
|
|
32067
|
+
const parsed2 = JSON.parse(raw);
|
|
30825
32068
|
return {
|
|
30826
|
-
stage: typeof
|
|
30827
|
-
revision: typeof
|
|
32069
|
+
stage: typeof parsed2.stage === "string" ? parsed2.stage : null,
|
|
32070
|
+
revision: typeof parsed2.revision === "number" ? parsed2.revision : null
|
|
30828
32071
|
};
|
|
30829
32072
|
} catch {
|
|
30830
32073
|
return { stage: null, revision: null };
|
|
30831
32074
|
}
|
|
30832
32075
|
}
|
|
30833
|
-
function asProblem(
|
|
32076
|
+
function asProblem(problem6) {
|
|
30834
32077
|
return {
|
|
30835
|
-
code:
|
|
30836
|
-
pointer:
|
|
30837
|
-
message:
|
|
30838
|
-
fix:
|
|
32078
|
+
code: problem6.code,
|
|
32079
|
+
pointer: problem6.pointer ?? problem6.where ?? "/",
|
|
32080
|
+
message: problem6.message.endsWith(".") ? problem6.message : `${problem6.message}.`,
|
|
32081
|
+
fix: problem6.code in PART_REFUSALS ? `Make the part what its entry in ${CATALOG} says: its props, what it reads, what it may hold.` : problem6.code in PARAM_REFUSALS ? `Fix pages[].params or the source's honors: ${PARAM_REFUSALS[problem6.code] ?? ""}.` : problem6.code in APP_FIXES ? APP_FIXES[problem6.code] ?? "" : problem6.code in TRANSFORM_FIXES ? TRANSFORM_FIXES[problem6.code] ?? "" : "The compiler refuses to guess here: state the fact it names in contract.json."
|
|
30839
32082
|
};
|
|
30840
32083
|
}
|
|
30841
32084
|
function livenessLine(report) {
|
|
@@ -30873,6 +32116,7 @@ function describe5(report, skipped) {
|
|
|
30873
32116
|
`;
|
|
30874
32117
|
out += warningLines(report);
|
|
30875
32118
|
out += livenessLine(report);
|
|
32119
|
+
out += metricsLines(report);
|
|
30876
32120
|
if (report.push !== null && report.push.pushed) {
|
|
30877
32121
|
out += `pushed \u2014 ${report.push.stage ?? "draft"}, revision ${String(report.push.revision ?? 0)}
|
|
30878
32122
|
`;
|
|
@@ -30880,10 +32124,149 @@ function describe5(report, skipped) {
|
|
|
30880
32124
|
out += formatSkipped(skipped);
|
|
30881
32125
|
return out;
|
|
30882
32126
|
}
|
|
32127
|
+
async function planMetricsFor(contract, opts) {
|
|
32128
|
+
const unchecked2 = (why) => ({
|
|
32129
|
+
plan: null,
|
|
32130
|
+
problems: [
|
|
32131
|
+
{
|
|
32132
|
+
code: "metric_unchecked",
|
|
32133
|
+
pointer: "/data/metrics",
|
|
32134
|
+
message: `this company's Datarails metrics could not be checked, so data.metrics cannot be bound: ${why}.`,
|
|
32135
|
+
fix: "Ship again shortly; nothing was created or pushed."
|
|
32136
|
+
}
|
|
32137
|
+
]
|
|
32138
|
+
});
|
|
32139
|
+
let client;
|
|
32140
|
+
try {
|
|
32141
|
+
client = await connect(opts);
|
|
32142
|
+
} catch (err) {
|
|
32143
|
+
return unchecked2(`no session for ${opts.server ?? "this server"} (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
|
|
32144
|
+
}
|
|
32145
|
+
try {
|
|
32146
|
+
const plan = await planMetrics(contract, metricsTransport(client), {
|
|
32147
|
+
appId: appIdOf(contract),
|
|
32148
|
+
appName: isPlain(contract) ? appNameOf(contract) : ""
|
|
32149
|
+
});
|
|
32150
|
+
return { plan, problems: plan.problems };
|
|
32151
|
+
} catch (err) {
|
|
32152
|
+
if (err instanceof MetricsReadError) return unchecked2(err.message);
|
|
32153
|
+
throw err;
|
|
32154
|
+
} finally {
|
|
32155
|
+
client.cleanup();
|
|
32156
|
+
}
|
|
32157
|
+
}
|
|
32158
|
+
async function applyMetricsFor(plan, opts) {
|
|
32159
|
+
let wait;
|
|
32160
|
+
try {
|
|
32161
|
+
wait = metricsWaitOf(opts.metricsWait);
|
|
32162
|
+
} catch (err) {
|
|
32163
|
+
return {
|
|
32164
|
+
applied: null,
|
|
32165
|
+
problems: [{ code: "metric_create", pointer: "/", message: `${err instanceof Error ? err.message : String(err)}.`, fix: "Pass --metrics-wait a number of seconds from 0 to 120." }]
|
|
32166
|
+
};
|
|
32167
|
+
}
|
|
32168
|
+
if (wait.note !== null && opts.json !== true) process.stderr.write(`note: ${wait.note}
|
|
32169
|
+
`);
|
|
32170
|
+
let client;
|
|
32171
|
+
try {
|
|
32172
|
+
client = await connect(opts);
|
|
32173
|
+
} catch (err) {
|
|
32174
|
+
return {
|
|
32175
|
+
applied: null,
|
|
32176
|
+
problems: [
|
|
32177
|
+
{
|
|
32178
|
+
code: "metric_create",
|
|
32179
|
+
pointer: "/data/metrics",
|
|
32180
|
+
message: `no session to create the metrics with (${err instanceof Error ? err.message.split("\n")[0] : String(err)}).`,
|
|
32181
|
+
fix: "Log in again and ship again; nothing was created or pushed."
|
|
32182
|
+
}
|
|
32183
|
+
]
|
|
32184
|
+
};
|
|
32185
|
+
}
|
|
32186
|
+
try {
|
|
32187
|
+
const applied = await applyPlan(plan, metricsTransport(client), { waitSeconds: wait.seconds });
|
|
32188
|
+
return { applied, problems: applied.ok ? [] : applied.problems };
|
|
32189
|
+
} finally {
|
|
32190
|
+
client.cleanup();
|
|
32191
|
+
}
|
|
32192
|
+
}
|
|
32193
|
+
function plannedBindings(plan) {
|
|
32194
|
+
const out = {};
|
|
32195
|
+
for (const entry of plan.entries) {
|
|
32196
|
+
if (entry.metric_id === null || entry.origin === null) continue;
|
|
32197
|
+
out[entry.key] = {
|
|
32198
|
+
metric_id: entry.metric_id,
|
|
32199
|
+
origin: entry.origin,
|
|
32200
|
+
...entry.reuse_key === null ? {} : { reuse_key: entry.reuse_key }
|
|
32201
|
+
};
|
|
32202
|
+
}
|
|
32203
|
+
return out;
|
|
32204
|
+
}
|
|
32205
|
+
async function metricsLiveness(report, contract, opts) {
|
|
32206
|
+
if (report.metrics === void 0 || report.metrics === null || report.app_id === null) return;
|
|
32207
|
+
if (Object.keys(metricSources(contract)).length === 0) return;
|
|
32208
|
+
let checks;
|
|
32209
|
+
try {
|
|
32210
|
+
const client = await connect(opts);
|
|
32211
|
+
try {
|
|
32212
|
+
checks = await checkMetricSources(metricsTransport(client), report.app_id, contract);
|
|
32213
|
+
} finally {
|
|
32214
|
+
client.cleanup();
|
|
32215
|
+
}
|
|
32216
|
+
} catch (err) {
|
|
32217
|
+
checks = Object.entries(metricSources(contract)).map(([source, key]) => ({
|
|
32218
|
+
source,
|
|
32219
|
+
key,
|
|
32220
|
+
state: "unchecked",
|
|
32221
|
+
message: `could not be run (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`
|
|
32222
|
+
}));
|
|
32223
|
+
}
|
|
32224
|
+
report.metrics["live"] = checks;
|
|
32225
|
+
const failed = checks.filter((one) => one.state === "failed");
|
|
32226
|
+
if (failed.length === 0) return;
|
|
32227
|
+
const problems = failed.map((one) => ({
|
|
32228
|
+
code: "metric-live",
|
|
32229
|
+
pointer: `/data/sources/${one.source}`,
|
|
32230
|
+
message: `source "${one.source}" (metric @${one.key}) drew no figure once pushed: ${one.message}`,
|
|
32231
|
+
fix: "Check the metric in Datarails and the binding ship printed; a card over a declared metric never falls back to another figure."
|
|
32232
|
+
}));
|
|
32233
|
+
report.liveness = {
|
|
32234
|
+
problems: [...report.liveness?.problems ?? [], ...problems],
|
|
32235
|
+
skipped: report.liveness?.skipped ?? [],
|
|
32236
|
+
widgets: report.liveness?.widgets ?? null,
|
|
32237
|
+
...report.liveness?.others === void 0 ? {} : { others: report.liveness.others }
|
|
32238
|
+
};
|
|
32239
|
+
report.verify.problems = [...report.verify.problems, ...problems];
|
|
32240
|
+
report.verify.violations = (report.verify.violations ?? 0) + problems.length;
|
|
32241
|
+
if (report.failure === null) report.failure = "liveness";
|
|
32242
|
+
}
|
|
32243
|
+
function metricsLines(report) {
|
|
32244
|
+
const block = report.metrics;
|
|
32245
|
+
if (block === void 0 || block === null) return "";
|
|
32246
|
+
let out = "";
|
|
32247
|
+
for (const line2 of bindingWarnings(block["push"])) out += `warning: ${line2}
|
|
32248
|
+
`;
|
|
32249
|
+
const live = Array.isArray(block["live"]) ? block["live"] : [];
|
|
32250
|
+
for (const one of live) {
|
|
32251
|
+
const said2 = one.state === "figure" ? "read its figure" : one.state === "preparing" ? `preparing \u2014 ${one.message} (neither blank nor a pass: run ship again once Datarails has it)` : one.state === "failed" ? `NO figure \u2014 ${one.message}` : `not checked \u2014 ${one.message}`;
|
|
32252
|
+
out += `metric source "${one.source}" (@${one.key}): ${said2}
|
|
32253
|
+
`;
|
|
32254
|
+
}
|
|
32255
|
+
return out;
|
|
32256
|
+
}
|
|
32257
|
+
function metricHelpLines() {
|
|
32258
|
+
return [
|
|
32259
|
+
"Shipped (with -s, --check or --push), each data.metrics key is bound in that company: reused where a metric computes the same thing,",
|
|
32260
|
+
'bound by name with "reuse": "name", changed in place where App Space made it for this app (and says no one else reads it), else created',
|
|
32261
|
+
'(as you, with metrics:write; filed under "App Space", its description naming the app). --check prints the plan and creates nothing. Refused with:',
|
|
32262
|
+
...Object.entries(METRIC_SHIP_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
|
|
32263
|
+
' metric-live once pushed, a source over a declared metric drew a sentence and no figure (exit 5); "preparing" is said and is neither blank nor a pass'
|
|
32264
|
+
];
|
|
32265
|
+
}
|
|
30883
32266
|
function registerCompileCommand(parent) {
|
|
30884
32267
|
withJsonOption(
|
|
30885
32268
|
withServerOptions(
|
|
30886
|
-
withAssemblyOptions(parent.command("compile [dir]").description("Compile contract.json into the package, and prove it round-trips.").option("--check", "Validate, compile and round-trip into a scratch folder, and verify \u2014 everything a push runs first. Write nothing.").option("--push", "Push the compiled package, contract attached.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--catalog <file>", "Check source ids against this catalog instead of the tenant.").option("--force", "Write over files this compiler did not write (never a custom handler, custom module or owned page).").option("--update-block", "After a push that passed, print the appspace_app_update block a build reply ends with.").addHelpText(
|
|
32269
|
+
withAssemblyOptions(parent.command("compile [dir]").description("Compile contract.json into the package, and prove it round-trips.").option("--check", "Validate, compile and round-trip into a scratch folder, and verify \u2014 everything a push runs first. Write nothing.").option("--push", "Push the compiled package, contract attached.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--catalog <file>", "Check source ids against this catalog instead of the tenant.").option("--force", "Write over files this compiler did not write (never a custom handler, custom module or owned page).").option("--update-block", "After a push that passed, print the appspace_app_update block a build reply ends with.").option("--metrics-wait <seconds>", "With --push: how long to wait for a BASE metric this ship created to extract before pushing anyway (default 60, at most 120).").addHelpText(
|
|
30887
32270
|
"after",
|
|
30888
32271
|
[
|
|
30889
32272
|
"",
|
|
@@ -30905,6 +32288,7 @@ function registerCompileCommand(parent) {
|
|
|
30905
32288
|
'A declared Datarails metric (data.metrics, read by a source as from.financeos.metric "@<key>") is refused with:',
|
|
30906
32289
|
...Object.entries(METRIC_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
|
|
30907
32290
|
" (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)",
|
|
32291
|
+
...metricHelpLines(),
|
|
30908
32292
|
"",
|
|
30909
32293
|
"A transform (a dashboard dataset series row's transforms/<name>.py, or an app source's) is refused with:",
|
|
30910
32294
|
...Object.entries(TRANSFORM_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`)
|
|
@@ -30936,7 +32320,7 @@ function registerShipCommand(parent) {
|
|
|
30936
32320
|
parent.command("ship [dir]").description("Assemble, compile, push and prove a dashboard pass; print the update block the reply ends with.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--force", "Write over files this compiler did not write (never a custom handler). Refused with --onto-live.").option(
|
|
30937
32321
|
"--onto-live",
|
|
30938
32322
|
"Land this pass's cards on the app as it is live now, keeping every card already there: pull it, add them, push against its revision, and start over when another push lands first. Needs --app-id and --skeleton, and a folder of its own."
|
|
30939
|
-
)
|
|
32323
|
+
).option("--metrics-wait <seconds>", "How long to wait for a BASE metric this ship created to extract before pushing anyway (default 60, at most 120).").addHelpText("after", ["", ...metricHelpLines()].join("\n"))
|
|
30940
32324
|
)
|
|
30941
32325
|
)
|
|
30942
32326
|
).action(async (dirArg, opts) => {
|
|
@@ -31115,7 +32499,7 @@ async function runEject(target, opts) {
|
|
|
31115
32499
|
}
|
|
31116
32500
|
}
|
|
31117
32501
|
async function ejectHandler(dir, contract, name) {
|
|
31118
|
-
const handlers =
|
|
32502
|
+
const handlers = obj4(obj4(obj4(contract["data"])["app"])["handlers"]);
|
|
31119
32503
|
const row = handlers[name];
|
|
31120
32504
|
if (!isPlain2(row)) {
|
|
31121
32505
|
const known = Object.keys(handlers).sort();
|
|
@@ -31173,7 +32557,7 @@ function literalEnd2(text3, open2) {
|
|
|
31173
32557
|
return -1;
|
|
31174
32558
|
}
|
|
31175
32559
|
async function ejectSource(dir, contract, name) {
|
|
31176
|
-
const sources =
|
|
32560
|
+
const sources = obj4(obj4(contract["data"])["sources"]);
|
|
31177
32561
|
if (!isPlain2(sources[name])) {
|
|
31178
32562
|
const known = Object.keys(sources).sort();
|
|
31179
32563
|
throw new Refused(
|
|
@@ -31183,7 +32567,7 @@ async function ejectSource(dir, contract, name) {
|
|
|
31183
32567
|
const path3 = `handlers/${name}.py`;
|
|
31184
32568
|
if (!insideFolder(dir, path3)) throw new Refused(`${JSON.stringify(path3)} is not a path inside ${dir}.`);
|
|
31185
32569
|
const dependents = Object.keys(sources).filter((other) => {
|
|
31186
|
-
const from =
|
|
32570
|
+
const from = obj4(sources[other])["from"];
|
|
31187
32571
|
return Array.isArray(from) && from.includes(name);
|
|
31188
32572
|
}).sort();
|
|
31189
32573
|
if (dependents.length > 0) {
|
|
@@ -31209,9 +32593,9 @@ async function ejectSource(dir, contract, name) {
|
|
|
31209
32593
|
throw new Refused(`${path3} as compiled carries no SOURCES = {\u2026} table, so it is not a generated source handler.`);
|
|
31210
32594
|
}
|
|
31211
32595
|
await mayPlace(dir, path3, body, [compiled]);
|
|
31212
|
-
const data =
|
|
31213
|
-
const app =
|
|
31214
|
-
const handlers =
|
|
32596
|
+
const data = obj4(contract["data"]);
|
|
32597
|
+
const app = obj4(data["app"]);
|
|
32598
|
+
const handlers = obj4(app["handlers"]);
|
|
31215
32599
|
handlers[name] = { role: "custom" };
|
|
31216
32600
|
app["handlers"] = handlers;
|
|
31217
32601
|
data["app"] = app;
|
|
@@ -31239,8 +32623,8 @@ async function ejectPage(dir, contract, path3) {
|
|
|
31239
32623
|
throw new Refused(`${path3} as compiled has no <head> to carry ${OWNED_META}.`);
|
|
31240
32624
|
}
|
|
31241
32625
|
await mayPlace(dir, path3, marked2, [compiled]);
|
|
31242
|
-
const elements =
|
|
31243
|
-
const gone = new Set(Object.keys(elements).filter((uid) =>
|
|
32626
|
+
const elements = obj4(contract["elements"]);
|
|
32627
|
+
const gone = new Set(Object.keys(elements).filter((uid) => obj4(elements[uid])["page"] === path3));
|
|
31244
32628
|
for (const uid of gone) delete elements[uid];
|
|
31245
32629
|
for (const element of Object.values(elements)) {
|
|
31246
32630
|
if (!isPlain2(element) || !Array.isArray(element["children"])) continue;
|
|
@@ -31251,11 +32635,11 @@ async function ejectPage(dir, contract, path3) {
|
|
|
31251
32635
|
kept["owned"] = true;
|
|
31252
32636
|
if (gate !== null && kept["roles"] === void 0) kept["role"] = gate;
|
|
31253
32637
|
pages[index] = kept;
|
|
31254
|
-
const app =
|
|
32638
|
+
const app = obj4(obj4(contract["data"])["app"]);
|
|
31255
32639
|
if (Array.isArray(app["screens"])) {
|
|
31256
32640
|
app["screens"] = app["screens"].filter((screen) => !(isPlain2(screen) && screen["page"] === path3));
|
|
31257
32641
|
}
|
|
31258
|
-
const sources = Object.keys(
|
|
32642
|
+
const sources = Object.keys(obj4(obj4(contract["data"])["sources"]));
|
|
31259
32643
|
const served = sources.filter((name) => files[`handlers/${name}.py`] !== void 0);
|
|
31260
32644
|
const wrote = await commit(dir, contract, path3, marked2, (after) => {
|
|
31261
32645
|
const lost = served.filter((name) => after[`handlers/${name}.py`] === void 0).sort();
|
|
@@ -31296,19 +32680,19 @@ ${OWNED_META}${html.slice(at2)}`;
|
|
|
31296
32680
|
${OWNED_META}${html.slice(at)}`;
|
|
31297
32681
|
}
|
|
31298
32682
|
async function ejectElement(dir, contract, uid) {
|
|
31299
|
-
const elements =
|
|
32683
|
+
const elements = obj4(contract["elements"]);
|
|
31300
32684
|
const element = elements[uid];
|
|
31301
32685
|
if (!isPlain2(element)) {
|
|
31302
32686
|
throw new Refused(`the contract has no element ${JSON.stringify(uid)}; an element's uid is its key in contract.elements.`);
|
|
31303
32687
|
}
|
|
31304
32688
|
const type = String(element["type"] ?? "");
|
|
31305
|
-
if (type === "custom") throw new Refused(`${uid} is already a custom element, drawn by ${String(
|
|
32689
|
+
if (type === "custom") throw new Refused(`${uid} is already a custom element, drawn by ${String(obj4(element["props"])["module"] ?? "its module")}.`);
|
|
31306
32690
|
if (!EJECTABLE.has(type)) {
|
|
31307
32691
|
throw new Refused(
|
|
31308
32692
|
`${uid} is a ${type || "typeless element"}, and element: ejects a chart or a stat \u2014 a root that draws one source's answer. To own a ${type || "screen"}'s markup, eject its page: page:${String(element["page"] ?? "<ui/path.html>")}.`
|
|
31309
32693
|
);
|
|
31310
32694
|
}
|
|
31311
|
-
const props =
|
|
32695
|
+
const props = obj4(element["props"]);
|
|
31312
32696
|
const name = freeName(moduleName(props["id"], type, uid), ownedPaths(contract));
|
|
31313
32697
|
const module = `components/${name}.js`;
|
|
31314
32698
|
const path3 = `ui/${module}`;
|
|
@@ -31389,9 +32773,9 @@ ${lines}
|
|
|
31389
32773
|
return built.files;
|
|
31390
32774
|
}
|
|
31391
32775
|
function refusedLines(built) {
|
|
31392
|
-
const refused = built.problems.filter((
|
|
32776
|
+
const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
|
|
31393
32777
|
if (Object.keys(built.files).length > 0 && refused.length === 0) return null;
|
|
31394
|
-
return (refused.length > 0 ? refused : built.problems).map((
|
|
32778
|
+
return (refused.length > 0 ? refused : built.problems).map((problem6) => ` [${problem6.code}] ${problem6.message}`).join("\n");
|
|
31395
32779
|
}
|
|
31396
32780
|
async function mayPlace(dir, path3, text3, same5) {
|
|
31397
32781
|
const existing = await readIfPresent(join16(dir, path3));
|
|
@@ -31436,7 +32820,7 @@ ${lines}
|
|
|
31436
32820
|
function isPlain2(value) {
|
|
31437
32821
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31438
32822
|
}
|
|
31439
|
-
function
|
|
32823
|
+
function obj4(value) {
|
|
31440
32824
|
return isPlain2(value) ? value : {};
|
|
31441
32825
|
}
|
|
31442
32826
|
function registerEjectCommand(parent) {
|
|
@@ -31668,13 +33052,13 @@ function parseFileSpec(spec) {
|
|
|
31668
33052
|
return { field, path: path3 };
|
|
31669
33053
|
}
|
|
31670
33054
|
function parseFileSpecs(specs) {
|
|
31671
|
-
const
|
|
33055
|
+
const parsed2 = specs.map(parseFileSpec);
|
|
31672
33056
|
const seen = /* @__PURE__ */ new Set();
|
|
31673
|
-
for (const one of
|
|
33057
|
+
for (const one of parsed2) {
|
|
31674
33058
|
if (seen.has(one.field)) throw new Error(`--file ${one.field} is given twice; give each field one file.`);
|
|
31675
33059
|
seen.add(one.field);
|
|
31676
33060
|
}
|
|
31677
|
-
return
|
|
33061
|
+
return parsed2;
|
|
31678
33062
|
}
|
|
31679
33063
|
var MIME_TYPES = {
|
|
31680
33064
|
".pdf": "application/pdf",
|
|
@@ -32865,15 +34249,15 @@ function answerOf(res) {
|
|
|
32865
34249
|
if (res.body.length === 0 || res.body.length > ROWS_BODY_LIMIT) return {};
|
|
32866
34250
|
const type = Object.entries(res.headers).find(([key]) => key.toLowerCase() === "content-type")?.[1];
|
|
32867
34251
|
if (type !== void 0 && !/json/i.test(type)) return {};
|
|
32868
|
-
let
|
|
34252
|
+
let parsed2;
|
|
32869
34253
|
try {
|
|
32870
|
-
|
|
34254
|
+
parsed2 = JSON.parse(res.body.toString("utf8"));
|
|
32871
34255
|
} catch {
|
|
32872
34256
|
return {};
|
|
32873
34257
|
}
|
|
32874
|
-
if (typeof
|
|
34258
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return {};
|
|
32875
34259
|
const out = { object: true };
|
|
32876
|
-
const { rows: rows2, error, needs } =
|
|
34260
|
+
const { rows: rows2, error, needs } = parsed2;
|
|
32877
34261
|
if (needs && typeof needs === "object" && !Array.isArray(needs)) {
|
|
32878
34262
|
const reason = needs.reason;
|
|
32879
34263
|
if (reason === true || reason && typeof reason === "object" && !Array.isArray(reason)) {
|
|
@@ -32954,8 +34338,8 @@ function harvestNodes(document) {
|
|
|
32954
34338
|
const filter = el.getAttribute(FILTER);
|
|
32955
34339
|
if (filter !== null && filter !== "") props.filter = readFilter(filter);
|
|
32956
34340
|
const args = el.getAttribute(ARGS);
|
|
32957
|
-
const
|
|
32958
|
-
if (
|
|
34341
|
+
const parsed2 = args !== null && args !== "" ? readArgs(args) : null;
|
|
34342
|
+
if (parsed2 !== null) props.args = parsed2;
|
|
32959
34343
|
const typed = el.getAttribute(PROPS);
|
|
32960
34344
|
const typedProps = typed !== null && typed !== "" ? readProps(typed) : null;
|
|
32961
34345
|
if (typedProps !== null) props.props = typedProps;
|
|
@@ -33251,8 +34635,8 @@ function createCanvas2dStub(canvas) {
|
|
|
33251
34635
|
for (const name of CANVAS_2D_VOID_METHODS) target[name] = () => void 0;
|
|
33252
34636
|
const spares = /* @__PURE__ */ new Map();
|
|
33253
34637
|
return new Proxy(target, {
|
|
33254
|
-
get(
|
|
33255
|
-
if (typeof prop === "symbol" || prop in
|
|
34638
|
+
get(obj5, prop, receiver) {
|
|
34639
|
+
if (typeof prop === "symbol" || prop in obj5) return Reflect.get(obj5, prop, receiver);
|
|
33256
34640
|
if (prop === "then") return void 0;
|
|
33257
34641
|
let spare = spares.get(prop);
|
|
33258
34642
|
if (spare === void 0) {
|
|
@@ -33536,13 +34920,13 @@ async function runUiSmoke(opts) {
|
|
|
33536
34920
|
const graceUntil = Date.now() + graceMs;
|
|
33537
34921
|
while (Date.now() < deadline) {
|
|
33538
34922
|
if (drivePrompt?.()) lastActivity = Date.now();
|
|
33539
|
-
const
|
|
34923
|
+
const parsed2 = dom.window.document.readyState === "complete";
|
|
33540
34924
|
const started = calls.length > since || inFlight > 0;
|
|
33541
34925
|
if (!started && Date.now() < graceUntil) {
|
|
33542
34926
|
await sleep(25);
|
|
33543
34927
|
continue;
|
|
33544
34928
|
}
|
|
33545
|
-
if (
|
|
34929
|
+
if (parsed2 && inFlight === 0 && Date.now() - lastActivity >= quietMs) break;
|
|
33546
34930
|
await sleep(25);
|
|
33547
34931
|
}
|
|
33548
34932
|
}
|
|
@@ -34817,9 +36201,9 @@ function unboundControls2(contract, files = {}) {
|
|
|
34817
36201
|
const param = page.params.find((one) => one?.name === name) ?? {};
|
|
34818
36202
|
const kind = named12(param.kind) ? param.kind : null;
|
|
34819
36203
|
const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
|
|
34820
|
-
const
|
|
36204
|
+
const problem6 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
|
|
34821
36205
|
const fix = `Give it a control \u2014 a part in param mode (props.props.param "${name}": a slider, a number_input, a select, a date_picker or a toggle), or a custom whose module calls ui.setParam("${name}", value) \u2014 or drop the param.`;
|
|
34822
|
-
out.push({ page: page.path, param: name, kind, problem:
|
|
36206
|
+
out.push({ page: page.path, param: name, kind, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
34823
36207
|
}
|
|
34824
36208
|
}
|
|
34825
36209
|
return out;
|
|
@@ -34873,9 +36257,9 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
|
|
|
34873
36257
|
continue;
|
|
34874
36258
|
}
|
|
34875
36259
|
const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both4(named_.map((one) => `"${one}"`))}`;
|
|
34876
|
-
const
|
|
36260
|
+
const problem6 = `${page.path} declares param "${name}", and no source on the page honours it (${there}) \u2014 moving it changes nothing on the page.`;
|
|
34877
36261
|
const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
|
|
34878
|
-
out.push({ page: page.path, param: name, sources: [...named_], problem:
|
|
36262
|
+
out.push({ page: page.path, param: name, sources: [...named_], problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
34879
36263
|
}
|
|
34880
36264
|
}
|
|
34881
36265
|
return out;
|
|
@@ -34917,9 +36301,9 @@ function pushedContract(bundle, pages) {
|
|
|
34917
36301
|
const found = contractBlockText(html);
|
|
34918
36302
|
if (found === null) continue;
|
|
34919
36303
|
try {
|
|
34920
|
-
const
|
|
34921
|
-
if (typeof
|
|
34922
|
-
return
|
|
36304
|
+
const parsed2 = JSON.parse(found);
|
|
36305
|
+
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
36306
|
+
return parsed2;
|
|
34923
36307
|
}
|
|
34924
36308
|
} catch {
|
|
34925
36309
|
}
|
|
@@ -35852,11 +37236,11 @@ function customDeclaresFindings(contract, bundle, runs) {
|
|
|
35852
37236
|
if (contract === null || !isAppContract3(contract)) return [];
|
|
35853
37237
|
const out = [];
|
|
35854
37238
|
const said2 = /* @__PURE__ */ new Set();
|
|
35855
|
-
const undeclared = (uid, page, module, handler,
|
|
37239
|
+
const undeclared = (uid, page, module, handler, problem6, fix) => {
|
|
35856
37240
|
const key = `${uid}|${handler}`;
|
|
35857
37241
|
if (said2.has(key)) return;
|
|
35858
37242
|
said2.add(key);
|
|
35859
|
-
out.push({ code: "custom-undeclared-call", where: `${page ?? "ui/"} \u2192 ${module}`, message:
|
|
37243
|
+
out.push({ code: "custom-undeclared-call", where: `${page ?? "ui/"} \u2192 ${module}`, message: problem6, fix, see: SEE.declares });
|
|
35860
37244
|
};
|
|
35861
37245
|
for (const found of DECLARES.undeclaredCustomCalls(contract, pushedFiles(bundle, {}))) {
|
|
35862
37246
|
undeclared(found.uid, found.page, found.module, found.handler, found.problem, found.fix);
|
|
@@ -35869,8 +37253,8 @@ function customDeclaresFindings(contract, bundle, runs) {
|
|
|
35869
37253
|
if (element?.type !== "custom" || declares === null || typeof module !== "string") continue;
|
|
35870
37254
|
if (declares.calls.includes(entry.handler)) continue;
|
|
35871
37255
|
const id = typeof element.props?.["id"] === "string" ? element.props["id"] : entry.uid;
|
|
35872
|
-
const { problem:
|
|
35873
|
-
undeclared(entry.uid, element.page ?? null, module, entry.handler,
|
|
37256
|
+
const { problem: problem6, fix } = DECLARES.undeclaredCallMessage(id, module, entry.handler, declares.calls, "observed at load");
|
|
37257
|
+
undeclared(entry.uid, element.page ?? null, module, entry.handler, problem6, fix);
|
|
35874
37258
|
}
|
|
35875
37259
|
}
|
|
35876
37260
|
for (const run of runs) {
|
|
@@ -36200,13 +37584,13 @@ async function loadFixtures(dir, explicit) {
|
|
|
36200
37584
|
if (explicit !== void 0) throw new Error(`no fixtures file at ${path3}.`);
|
|
36201
37585
|
return { fixtures: { cases: [], ui: { entry: "index.html" } } };
|
|
36202
37586
|
}
|
|
36203
|
-
let
|
|
37587
|
+
let parsed2;
|
|
36204
37588
|
try {
|
|
36205
|
-
|
|
37589
|
+
parsed2 = JSON.parse(raw);
|
|
36206
37590
|
} catch {
|
|
36207
37591
|
throw new FixtureError(`${path3} is not valid JSON.`);
|
|
36208
37592
|
}
|
|
36209
|
-
return { fixtures: parseFixtures(
|
|
37593
|
+
return { fixtures: parseFixtures(parsed2, path3), source: path3 };
|
|
36210
37594
|
}
|
|
36211
37595
|
async function runHandlerCases(cases, dir, appId, client, roleOf = () => void 0) {
|
|
36212
37596
|
const code = /* @__PURE__ */ new Map();
|
|
@@ -36534,7 +37918,7 @@ ${attached}`;
|
|
|
36534
37918
|
}
|
|
36535
37919
|
out += ` FAIL ${head}
|
|
36536
37920
|
${attached}`;
|
|
36537
|
-
for (const
|
|
37921
|
+
for (const problem6 of outcome.problems) out += ` ${problem6}
|
|
36538
37922
|
`;
|
|
36539
37923
|
}
|
|
36540
37924
|
return out;
|
|
@@ -36599,7 +37983,7 @@ function rederiveComponentBindings(contract) {
|
|
|
36599
37983
|
model
|
|
36600
37984
|
});
|
|
36601
37985
|
contract.problems = [
|
|
36602
|
-
...contract.problems.filter((
|
|
37986
|
+
...contract.problems.filter((problem6) => !COMPONENT_PROBLEM_CODES.has(problem6.code)),
|
|
36603
37987
|
...problems
|
|
36604
37988
|
].sort(byProblem4);
|
|
36605
37989
|
}
|
|
@@ -36655,7 +38039,7 @@ function mergeDomComponents(contract, components, page) {
|
|
|
36655
38039
|
contract.elements[uid] = element;
|
|
36656
38040
|
}
|
|
36657
38041
|
contract.problems = contract.problems.filter(
|
|
36658
|
-
(
|
|
38042
|
+
(problem6) => !(problem6.code === "duplicate_dom_id" && problem6.where.startsWith(`${page}@dom#`))
|
|
36659
38043
|
);
|
|
36660
38044
|
for (const { id, uid } of duplicates) {
|
|
36661
38045
|
contract.problems.push({
|
|
@@ -36768,15 +38152,15 @@ function untouchedHandlers(declared, cases, interactions, runs = []) {
|
|
|
36768
38152
|
return declared.filter((name) => !touched.has(name) && !calls.some((call) => callsHandler(call.url, name))).sort();
|
|
36769
38153
|
}
|
|
36770
38154
|
async function readSeed(dir) {
|
|
36771
|
-
let
|
|
38155
|
+
let parsed2;
|
|
36772
38156
|
try {
|
|
36773
|
-
|
|
38157
|
+
parsed2 = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
|
|
36774
38158
|
} catch {
|
|
36775
38159
|
return null;
|
|
36776
38160
|
}
|
|
36777
|
-
if (typeof
|
|
38161
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return null;
|
|
36778
38162
|
const seed = {};
|
|
36779
|
-
for (const [table, rows2] of Object.entries(
|
|
38163
|
+
for (const [table, rows2] of Object.entries(parsed2)) {
|
|
36780
38164
|
if (!Array.isArray(rows2)) continue;
|
|
36781
38165
|
seed[table] = rows2.filter(
|
|
36782
38166
|
(row) => typeof row === "object" && row !== null && !Array.isArray(row)
|
|
@@ -37270,11 +38654,16 @@ function describeTryRole(one) {
|
|
|
37270
38654
|
function rolesAllowed(handler, view) {
|
|
37271
38655
|
return view.roles.filter((role) => view.allowed[role]?.[handler] === true);
|
|
37272
38656
|
}
|
|
38657
|
+
function unboundNote(result) {
|
|
38658
|
+
const meta = result?.meta;
|
|
38659
|
+
if (meta?.source?.status !== "unbound") return null;
|
|
38660
|
+
return 'this source reads a metric the contract declares (data.metrics), and a dry-run carries no metric bindings \u2014 they belong to a pushed version \u2014 so it always answers "isn\'t set up" here. `app-space ship` binds the metric and runs this source against the draft after its push.';
|
|
38661
|
+
}
|
|
37273
38662
|
function registerTryCommand(parent) {
|
|
37274
38663
|
withJsonOption(
|
|
37275
38664
|
withServerOptions(
|
|
37276
38665
|
parent.command("try <handler>").description(
|
|
37277
|
-
|
|
38666
|
+
`Run one handler against the app's real ctx, using the code on disk, without publishing. A source over a declared Datarails metric (data.metrics) answers "isn't set up" here: a dry-run carries no metric bindings; ship checks it after its push.`
|
|
37278
38667
|
).option("--dir <path>", "The app folder (default: current directory).", ".").option(
|
|
37279
38668
|
"--payload <json>",
|
|
37280
38669
|
"JSON payload for the handler, or @<path> to read it from a JSON file (relative to the working directory).",
|
|
@@ -37354,6 +38743,9 @@ function registerTryCommand(parent) {
|
|
|
37354
38743
|
if (res.ok) {
|
|
37355
38744
|
process.stdout.write(`ok
|
|
37356
38745
|
${JSON.stringify(res.result, null, 2)}
|
|
38746
|
+
`);
|
|
38747
|
+
const note = unboundNote(res.result);
|
|
38748
|
+
if (note !== null) process.stdout.write(`note: ${note}
|
|
37357
38749
|
`);
|
|
37358
38750
|
return;
|
|
37359
38751
|
}
|