@datarails-ai/dr-appspace-cli 0.12.37 → 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/README.md +5 -2
- package/dist/cli.js +1745 -324
- 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-
|
|
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,13 +9622,13 @@ 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
|
}
|
|
9621
|
-
function sourceRoles(contract, name) {
|
|
9631
|
+
function sourceRoles(contract, name, known = /* @__PURE__ */ new Set()) {
|
|
9622
9632
|
const source = contract?.data?.sources?.[name];
|
|
9623
9633
|
const stated2 = gateRoles(source?.requiresRole);
|
|
9624
9634
|
const elements = isPlainObject3(contract?.elements) ? contract.elements : {};
|
|
@@ -9636,7 +9646,7 @@ function sourceRoles(contract, name) {
|
|
|
9636
9646
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
9637
9647
|
}
|
|
9638
9648
|
for (const page of pagesOf(contract)) {
|
|
9639
|
-
if (page.owned !== true || readers === 0) continue;
|
|
9649
|
+
if (page.owned !== true || readers === 0 || known.has(page.path)) continue;
|
|
9640
9650
|
const roles = pageRoles(page);
|
|
9641
9651
|
if (roles.length === 0) open2 = true;
|
|
9642
9652
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
@@ -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"];
|
|
@@ -12593,6 +12624,34 @@ function servedSources(elements, sources) {
|
|
|
12593
12624
|
}
|
|
12594
12625
|
return [...out].sort(natural3);
|
|
12595
12626
|
}
|
|
12627
|
+
function ownedReaders(pages, extra) {
|
|
12628
|
+
const readers = {};
|
|
12629
|
+
const known = /* @__PURE__ */ new Set();
|
|
12630
|
+
for (const page of Array.isArray(pages) ? pages : []) {
|
|
12631
|
+
if (page?.owned !== true || !named5(page.path)) continue;
|
|
12632
|
+
const text3 = extra?.[page.path];
|
|
12633
|
+
if (typeof text3 !== "string" || !isOwnedPage(text3)) continue;
|
|
12634
|
+
const embedded = embeddedContractOf({ text: text3 });
|
|
12635
|
+
if (embedded === null) continue;
|
|
12636
|
+
known.add(page.path);
|
|
12637
|
+
const elements = isPlainObject8(embedded.elements) ? embedded.elements : {};
|
|
12638
|
+
for (const uid of Object.keys(elements).sort(natural3)) {
|
|
12639
|
+
const element = elements[uid];
|
|
12640
|
+
if (!isPlainObject8(element) || element.page !== page.path) continue;
|
|
12641
|
+
const props = isPlainObject8(element.props) ? element.props : {};
|
|
12642
|
+
const kept = {};
|
|
12643
|
+
if (named5(props.source)) kept.source = props.source;
|
|
12644
|
+
if (Array.isArray(props.declares?.reads)) kept.declares = { reads: props.declares.reads.filter(named5) };
|
|
12645
|
+
if (Object.keys(kept).length > 0) readers[`owned:${uid}`] = { page: page.path, props: kept };
|
|
12646
|
+
}
|
|
12647
|
+
}
|
|
12648
|
+
return { readers, known };
|
|
12649
|
+
}
|
|
12650
|
+
function withOwnedReaders(contract, readers) {
|
|
12651
|
+
if (Object.keys(readers).length === 0) return contract;
|
|
12652
|
+
const elements = isPlainObject8(contract?.elements) ? contract.elements : {};
|
|
12653
|
+
return { ...contract, elements: { ...elements, ...readers } };
|
|
12654
|
+
}
|
|
12596
12655
|
function sourceProblems(sources, app, handlers, extra) {
|
|
12597
12656
|
const problems = [];
|
|
12598
12657
|
const names = Object.keys(sources).sort(natural3);
|
|
@@ -12814,11 +12873,11 @@ function metricFilterProblem(filter) {
|
|
|
12814
12873
|
if (filter.exclude !== void 0 && typeof filter.exclude !== "boolean") return `states exclude ${JSON.stringify(filter.exclude)}; exclude is true or false`;
|
|
12815
12874
|
return null;
|
|
12816
12875
|
}
|
|
12817
|
-
function metricReaches(
|
|
12876
|
+
function metricReaches(operandsOf2, start, target, seen) {
|
|
12818
12877
|
if (start === target) return true;
|
|
12819
12878
|
if (seen.has(start)) return false;
|
|
12820
12879
|
seen.add(start);
|
|
12821
|
-
return (
|
|
12880
|
+
return (operandsOf2.get(start) ?? []).some((one) => metricReaches(operandsOf2, one, target, seen));
|
|
12822
12881
|
}
|
|
12823
12882
|
function calcShaped(node) {
|
|
12824
12883
|
if (node[0] === "num" || node[0] === "name") return true;
|
|
@@ -12947,12 +13006,12 @@ function metricProblems(metrics, sources) {
|
|
|
12947
13006
|
}
|
|
12948
13007
|
}
|
|
12949
13008
|
}
|
|
12950
|
-
const
|
|
13009
|
+
const operandsOf2 = /* @__PURE__ */ new Map();
|
|
12951
13010
|
for (const key of [...formulas.keys()].sort(natural3)) {
|
|
12952
13011
|
const { problems: wrong, operands, windows } = metricFormulaProblems(formulas.get(key), declaredKeys);
|
|
12953
13012
|
for (const one of wrong) say("metric_formula", at(key, "formula"), `metric "${key}"'s formula ${one}`);
|
|
12954
13013
|
if (wrong.length > 0) continue;
|
|
12955
|
-
|
|
13014
|
+
operandsOf2.set(key, operands);
|
|
12956
13015
|
if (windows.length > 0) {
|
|
12957
13016
|
advise(
|
|
12958
13017
|
"metric_window",
|
|
@@ -12961,9 +13020,9 @@ function metricProblems(metrics, sources) {
|
|
|
12961
13020
|
);
|
|
12962
13021
|
}
|
|
12963
13022
|
}
|
|
12964
|
-
for (const key of [...
|
|
12965
|
-
for (const dep of
|
|
12966
|
-
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())) {
|
|
12967
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`);
|
|
12968
13027
|
break;
|
|
12969
13028
|
}
|
|
@@ -13006,7 +13065,7 @@ function metricProblems(metrics, sources) {
|
|
|
13006
13065
|
const visit = (key) => {
|
|
13007
13066
|
if (used.has(key)) return;
|
|
13008
13067
|
used.add(key);
|
|
13009
|
-
for (const one of
|
|
13068
|
+
for (const one of operandsOf2.get(key) ?? []) visit(one);
|
|
13010
13069
|
};
|
|
13011
13070
|
for (const key of read) visit(key);
|
|
13012
13071
|
for (const key of declaredKeys) {
|
|
@@ -13421,8 +13480,8 @@ function paramProblems(contract, sources) {
|
|
|
13421
13480
|
}
|
|
13422
13481
|
function numberFix(value, integer = false) {
|
|
13423
13482
|
const text3 = typeof value === "string" ? value.trim() : null;
|
|
13424
|
-
const
|
|
13425
|
-
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`;
|
|
13426
13485
|
return `write ${integer ? "a whole number" : "a number"}, or leave the key out`;
|
|
13427
13486
|
}
|
|
13428
13487
|
function uidHint(value, why, elements) {
|
|
@@ -13624,7 +13683,8 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
|
|
|
13624
13683
|
problems.push(...exprProblems(stated2));
|
|
13625
13684
|
const metricFindings = metricProblems(contract?.data?.metrics, stated2);
|
|
13626
13685
|
problems.push(...metricFindings.filter((one) => !one.advisory));
|
|
13627
|
-
|
|
13686
|
+
const owned = ownedReaders(contract.pages, extra);
|
|
13687
|
+
problems.push(...paramProblems(withOwnedReaders(contract, owned.readers), stated2));
|
|
13628
13688
|
problems.push(...themeProblems(app));
|
|
13629
13689
|
problems.push(...shellProblems(contract, typed, handlers, stated2));
|
|
13630
13690
|
const sources = fillFilterable(stated2);
|
|
@@ -13743,10 +13803,10 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
|
|
|
13743
13803
|
problems.push(...placementProblems(elements, pages, tree, order2));
|
|
13744
13804
|
if (problems.length > 0) return { files: {}, problems, contract, warnings };
|
|
13745
13805
|
const whole = restate(contract, elements, pages, typed, order2);
|
|
13746
|
-
const served = servedSources(elements, sources);
|
|
13806
|
+
const served = servedSources({ ...elements, ...owned.readers }, sources);
|
|
13747
13807
|
const transforms = transformTexts2(sources, extra);
|
|
13748
13808
|
const files = {};
|
|
13749
|
-
files["manifest.json"] = json(manifestOf(contract, manifestApp, handlers, names, model, served));
|
|
13809
|
+
files["manifest.json"] = json(manifestOf(contract, manifestApp, handlers, names, model, served, withOwnedReaders(contract, owned.readers), owned.known));
|
|
13750
13810
|
if (app.data_model) files["data_model.json"] = json(dataModelOf(app.data_model));
|
|
13751
13811
|
for (const name of names) {
|
|
13752
13812
|
const path3 = `handlers/${name}.py`;
|
|
@@ -16437,7 +16497,7 @@ function measureProblems(name, row, model, handlers = {}) {
|
|
|
16437
16497
|
const found = whereProblems(spec.where, EVENTS_SHAPE, at, who, {
|
|
16438
16498
|
noColumn: (column) => `filters on "${column}", which is no column of the events; filter on at, action, handler, actor or note`
|
|
16439
16499
|
});
|
|
16440
|
-
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))));
|
|
16441
16501
|
} else problems.push(...whereProblems(spec.where, table, at, who, { platform: true }));
|
|
16442
16502
|
}
|
|
16443
16503
|
}
|
|
@@ -16559,7 +16619,7 @@ function statusColumn(model, row) {
|
|
|
16559
16619
|
const status = table.columns.status;
|
|
16560
16620
|
return status !== void 0 && status.enum === null ? "status" : null;
|
|
16561
16621
|
}
|
|
16562
|
-
function manifestOf(contract, app, handlers, names, model, served = []) {
|
|
16622
|
+
function manifestOf(contract, app, handlers, names, model, served = [], readers = contract, known = /* @__PURE__ */ new Set()) {
|
|
16563
16623
|
const manifest = {
|
|
16564
16624
|
apiVersion: app.api_version ?? DEFAULT_API_VERSION,
|
|
16565
16625
|
id: app.id ?? null,
|
|
@@ -16585,7 +16645,7 @@ function manifestOf(contract, app, handlers, names, model, served = []) {
|
|
|
16585
16645
|
}
|
|
16586
16646
|
for (const name of served) {
|
|
16587
16647
|
const entry = { http: "POST", entrypoint: `handlers/${name}.py` };
|
|
16588
|
-
const { roles: gate } = sourceRoles(
|
|
16648
|
+
const { roles: gate } = sourceRoles(readers, name, known);
|
|
16589
16649
|
if (gate.length > 0) entry.requiresRole = gate;
|
|
16590
16650
|
manifest.handlers[name] = entry;
|
|
16591
16651
|
}
|
|
@@ -17693,6 +17753,7 @@ function validateContract(contract, catalog) {
|
|
|
17693
17753
|
checkKind(app, problems);
|
|
17694
17754
|
checkPreset(app, problems);
|
|
17695
17755
|
if (kind === "dashboard") checkSources(doc, catalog, problems, skipped);
|
|
17756
|
+
checkSourceMetricIds(doc, catalog, problems, skipped);
|
|
17696
17757
|
checkVocabulary(doc, kind, problems, renderer);
|
|
17697
17758
|
checkPages(doc, kind, problems);
|
|
17698
17759
|
if (kind === "dashboard") {
|
|
@@ -17715,22 +17776,22 @@ function validateContract(contract, catalog) {
|
|
|
17715
17776
|
return { problems: dedupe(problems), skipped: dedupeSkipped(skipped), warnings: dedupe(warnings) };
|
|
17716
17777
|
}
|
|
17717
17778
|
function checkCustomDeclares(doc, problems) {
|
|
17718
|
-
for (const
|
|
17779
|
+
for (const problem6 of declaresProblems2(doc)) {
|
|
17719
17780
|
problems.push({
|
|
17720
|
-
code:
|
|
17721
|
-
pointer:
|
|
17722
|
-
message: `${
|
|
17781
|
+
code: problem6.code,
|
|
17782
|
+
pointer: problem6.pointer,
|
|
17783
|
+
message: `${problem6.message}.`,
|
|
17723
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).'
|
|
17724
17785
|
});
|
|
17725
17786
|
}
|
|
17726
17787
|
}
|
|
17727
17788
|
function checkTransforms(doc, problems) {
|
|
17728
|
-
for (const
|
|
17729
|
-
const row =
|
|
17789
|
+
for (const problem6 of transformRowProblems2(obj(doc["data"]))) {
|
|
17790
|
+
const row = problem6.code === "transform_row";
|
|
17730
17791
|
problems.push({
|
|
17731
17792
|
code: row ? "transform-row" : "transform-missing",
|
|
17732
|
-
pointer:
|
|
17733
|
-
message: `${
|
|
17793
|
+
pointer: problem6.where,
|
|
17794
|
+
message: `${problem6.message}.`,
|
|
17734
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)."
|
|
17735
17796
|
});
|
|
17736
17797
|
}
|
|
@@ -18030,6 +18091,30 @@ function checkPreset(app, problems) {
|
|
|
18030
18091
|
fix: `Name one of ${PRESETS2.join(", ")}, or drop the key and take the default.`
|
|
18031
18092
|
});
|
|
18032
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
|
+
}
|
|
18033
18118
|
function checkSources(doc, catalog, problems, skipped) {
|
|
18034
18119
|
const widgets = obj(obj(doc["data"])["widgets"]);
|
|
18035
18120
|
if (Object.keys(widgets).length === 0) return;
|
|
@@ -18271,9 +18356,9 @@ function calendarDateIn(filter) {
|
|
|
18271
18356
|
}
|
|
18272
18357
|
function epochOf(date) {
|
|
18273
18358
|
const day = date.slice(0, 10);
|
|
18274
|
-
const
|
|
18275
|
-
if (Number.isNaN(
|
|
18276
|
-
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));
|
|
18277
18362
|
}
|
|
18278
18363
|
function addressOf(source, hasAlias, hasId) {
|
|
18279
18364
|
const declared = source["address"];
|
|
@@ -19235,11 +19320,11 @@ function walk3(node, path3, visit) {
|
|
|
19235
19320
|
function dedupe(problems) {
|
|
19236
19321
|
const seen = /* @__PURE__ */ new Set();
|
|
19237
19322
|
const out = [];
|
|
19238
|
-
for (const
|
|
19239
|
-
const key = `${
|
|
19323
|
+
for (const problem6 of problems) {
|
|
19324
|
+
const key = `${problem6.code}\0${problem6.pointer}\0${problem6.message}`;
|
|
19240
19325
|
if (seen.has(key)) continue;
|
|
19241
19326
|
seen.add(key);
|
|
19242
|
-
out.push(
|
|
19327
|
+
out.push(problem6);
|
|
19243
19328
|
}
|
|
19244
19329
|
return out;
|
|
19245
19330
|
}
|
|
@@ -20551,16 +20636,16 @@ async function readManifest(dir) {
|
|
|
20551
20636
|
if (raw === void 0) {
|
|
20552
20637
|
throw new Error(`no manifest.json in ${dir} \u2014 is this an app folder?`);
|
|
20553
20638
|
}
|
|
20554
|
-
let
|
|
20639
|
+
let parsed2;
|
|
20555
20640
|
try {
|
|
20556
|
-
|
|
20641
|
+
parsed2 = JSON.parse(raw);
|
|
20557
20642
|
} catch {
|
|
20558
20643
|
throw new Error(`manifest.json in ${dir} is not valid JSON.`);
|
|
20559
20644
|
}
|
|
20560
|
-
if (typeof
|
|
20645
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) {
|
|
20561
20646
|
throw new Error("manifest.json is not a JSON object.");
|
|
20562
20647
|
}
|
|
20563
|
-
const manifest =
|
|
20648
|
+
const manifest = parsed2;
|
|
20564
20649
|
const id = manifest["id"];
|
|
20565
20650
|
if (typeof id !== "string" || id === "") {
|
|
20566
20651
|
throw new Error('manifest.json has no "id".');
|
|
@@ -20574,9 +20659,9 @@ async function readState(dir) {
|
|
|
20574
20659
|
const raw = await readIfPresent(join5(dir, ".appspace.json"));
|
|
20575
20660
|
if (raw === void 0) return {};
|
|
20576
20661
|
try {
|
|
20577
|
-
const
|
|
20578
|
-
if (typeof
|
|
20579
|
-
return
|
|
20662
|
+
const parsed2 = JSON.parse(raw);
|
|
20663
|
+
if (typeof parsed2 === "object" && parsed2 !== null) {
|
|
20664
|
+
return parsed2;
|
|
20580
20665
|
}
|
|
20581
20666
|
} catch {
|
|
20582
20667
|
}
|
|
@@ -20693,8 +20778,8 @@ function summarise(contract) {
|
|
|
20693
20778
|
}
|
|
20694
20779
|
function describeFindings(contract) {
|
|
20695
20780
|
let out = "";
|
|
20696
|
-
for (const
|
|
20697
|
-
out += ` ${
|
|
20781
|
+
for (const problem6 of contract.problems) {
|
|
20782
|
+
out += ` ${problem6.lossy ? "lossy" : "ok "} ${problem6.code} at ${problem6.where}: ${problem6.message}
|
|
20698
20783
|
`;
|
|
20699
20784
|
}
|
|
20700
20785
|
for (const entry of contract.unresolved) {
|
|
@@ -21607,6 +21692,1082 @@ async function previewOne(document, file, opts, shared = {}) {
|
|
|
21607
21692
|
import { readFile as readFile7 } from "fs/promises";
|
|
21608
21693
|
import { join as join8 } from "path";
|
|
21609
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
|
|
21610
22771
|
var KIT_ENV = "DR_APPSPACE_KIT";
|
|
21611
22772
|
function kitForPush(opts, env = process.env) {
|
|
21612
22773
|
const flag = (opts.kit ?? "").trim();
|
|
@@ -21775,12 +22936,39 @@ function describePostCommit(result) {
|
|
|
21775
22936
|
}
|
|
21776
22937
|
return lines.join("");
|
|
21777
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
|
+
}
|
|
21778
22963
|
function registerPushCommand(parent) {
|
|
21779
22964
|
withJsonOption(
|
|
21780
22965
|
withServerOptions(
|
|
21781
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(
|
|
21782
22967
|
"--kit <asset>",
|
|
21783
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)."
|
|
21784
22972
|
)
|
|
21785
22973
|
)
|
|
21786
22974
|
).action(async (dirArg, opts) => {
|
|
@@ -21822,6 +23010,9 @@ async function pushBody(dir, opts) {
|
|
|
21822
23010
|
if (opts.force !== true && typeof state.revision === "number") {
|
|
21823
23011
|
body["expected_revision"] = state.revision;
|
|
21824
23012
|
}
|
|
23013
|
+
if (opts.metricBindings !== void 0 && Object.keys(opts.metricBindings).length > 0) {
|
|
23014
|
+
body["metric_bindings"] = opts.metricBindings;
|
|
23015
|
+
}
|
|
21825
23016
|
return { appId, body, handlers };
|
|
21826
23017
|
}
|
|
21827
23018
|
function chartsWithoutEcharts(files) {
|
|
@@ -21843,6 +23034,9 @@ async function pushFolder(dir, opts) {
|
|
|
21843
23034
|
const { appId, body, handlers } = await pushBody(dir, opts);
|
|
21844
23035
|
const client = await connect(opts);
|
|
21845
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
|
+
}
|
|
21846
23040
|
let result;
|
|
21847
23041
|
try {
|
|
21848
23042
|
result = await client.request(`/apps/${encodeURIComponent(appId)}/package`, {
|
|
@@ -21859,7 +23053,8 @@ async function pushFolder(dir, opts) {
|
|
|
21859
23053
|
const outcome = {
|
|
21860
23054
|
revision: result.revision,
|
|
21861
23055
|
stage: result.stage,
|
|
21862
|
-
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 }
|
|
21863
23058
|
};
|
|
21864
23059
|
await recordRevision(dir, result.revision, result.stage, handlers, {
|
|
21865
23060
|
id: appId,
|
|
@@ -21875,6 +23070,8 @@ async function pushFolder(dir, opts) {
|
|
|
21875
23070
|
`
|
|
21876
23071
|
);
|
|
21877
23072
|
process.stdout.write(describePostCommit(result));
|
|
23073
|
+
for (const line2 of bindingWarnings(result.metric_bindings)) process.stdout.write(`warning: ${line2}
|
|
23074
|
+
`);
|
|
21878
23075
|
process.stdout.write(describeContract(result.contract));
|
|
21879
23076
|
process.stdout.write(describeIgnoredName(result.name_ignored));
|
|
21880
23077
|
process.stdout.write(describeSeeding(result));
|
|
@@ -21954,9 +23151,9 @@ async function writeReport(dir, report) {
|
|
|
21954
23151
|
}
|
|
21955
23152
|
async function ownedFiles(dir) {
|
|
21956
23153
|
try {
|
|
21957
|
-
const
|
|
21958
|
-
if (!Array.isArray(
|
|
21959
|
-
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));
|
|
21960
23157
|
} catch {
|
|
21961
23158
|
return /* @__PURE__ */ new Set();
|
|
21962
23159
|
}
|
|
@@ -21966,8 +23163,8 @@ function digestOf(text3) {
|
|
|
21966
23163
|
}
|
|
21967
23164
|
async function recordedDigests(dir) {
|
|
21968
23165
|
try {
|
|
21969
|
-
const
|
|
21970
|
-
const held =
|
|
23166
|
+
const parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
|
|
23167
|
+
const held = parsed2.digests;
|
|
21971
23168
|
if (typeof held !== "object" || held === null || Array.isArray(held)) return {};
|
|
21972
23169
|
return Object.fromEntries(Object.entries(held).filter((entry) => typeof entry[1] === "string"));
|
|
21973
23170
|
} catch {
|
|
@@ -21975,21 +23172,21 @@ async function recordedDigests(dir) {
|
|
|
21975
23172
|
}
|
|
21976
23173
|
}
|
|
21977
23174
|
async function recordAsInput(dir, path3) {
|
|
21978
|
-
let
|
|
23175
|
+
let parsed2;
|
|
21979
23176
|
try {
|
|
21980
|
-
|
|
23177
|
+
parsed2 = JSON.parse(await readFile8(reportPath(dir), "utf8"));
|
|
21981
23178
|
} catch {
|
|
21982
23179
|
return;
|
|
21983
23180
|
}
|
|
21984
|
-
if (typeof
|
|
21985
|
-
const list3 = (key) => Array.isArray(
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
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"];
|
|
21989
23186
|
if (typeof digests === "object" && digests !== null && !Array.isArray(digests)) {
|
|
21990
23187
|
delete digests[path3];
|
|
21991
23188
|
}
|
|
21992
|
-
await writeFile4(reportPath(dir), JSON.stringify(
|
|
23189
|
+
await writeFile4(reportPath(dir), JSON.stringify(parsed2, null, 2) + "\n");
|
|
21993
23190
|
}
|
|
21994
23191
|
|
|
21995
23192
|
// src/commands/appspace/get.ts
|
|
@@ -22010,8 +23207,8 @@ function requireBundlePath(name) {
|
|
|
22010
23207
|
async function unpushedInputs(dir, pkg) {
|
|
22011
23208
|
let inputs = [];
|
|
22012
23209
|
try {
|
|
22013
|
-
const
|
|
22014
|
-
if (Array.isArray(
|
|
23210
|
+
const parsed2 = JSON.parse(await readFile9(reportPath(dir), "utf8"));
|
|
23211
|
+
if (Array.isArray(parsed2.inputs)) inputs = parsed2.inputs.map(String);
|
|
22015
23212
|
} catch {
|
|
22016
23213
|
return [];
|
|
22017
23214
|
}
|
|
@@ -22276,7 +23473,7 @@ import { cp, mkdir as mkdir5, mkdtemp as mkdtemp2, rm as rm3, writeFile as write
|
|
|
22276
23473
|
import { tmpdir as tmpdir3 } from "os";
|
|
22277
23474
|
import { dirname as dirname4, join as join11, resolve as resolve5, sep as sep2 } from "path";
|
|
22278
23475
|
var WHAT = "package";
|
|
22279
|
-
async function checkPackage(dir, files, contract, opts, online) {
|
|
23476
|
+
async function checkPackage(dir, files, contract, opts, online, pendingMetricKeys = []) {
|
|
22280
23477
|
if (!online) {
|
|
22281
23478
|
return skip("no -s server to run the push's own checks against; the push is the first to run them");
|
|
22282
23479
|
}
|
|
@@ -22288,13 +23485,13 @@ async function checkPackage(dir, files, contract, opts, online) {
|
|
|
22288
23485
|
try {
|
|
22289
23486
|
payload = await pushBody(scratch, { ...opts, contract: true });
|
|
22290
23487
|
} catch (err) {
|
|
22291
|
-
return skip(`the push body could not be built (${
|
|
23488
|
+
return skip(`the push body could not be built (${firstLine2(err)})`);
|
|
22292
23489
|
}
|
|
22293
23490
|
let client;
|
|
22294
23491
|
try {
|
|
22295
23492
|
client = await connect(opts);
|
|
22296
23493
|
} catch (err) {
|
|
22297
|
-
return skip(`could not reach the server (${
|
|
23494
|
+
return skip(`could not reach the server (${firstLine2(err)})`);
|
|
22298
23495
|
}
|
|
22299
23496
|
try {
|
|
22300
23497
|
const path3 = `/apps/${encodeURIComponent(payload.appId)}/package/validate`;
|
|
@@ -22307,9 +23504,9 @@ async function checkPackage(dir, files, contract, opts, online) {
|
|
|
22307
23504
|
body: JSON.stringify(payload.body)
|
|
22308
23505
|
});
|
|
22309
23506
|
} catch (err) {
|
|
22310
|
-
return skip(`the server did not answer (${
|
|
23507
|
+
return skip(`the server did not answer (${firstLine2(err)})`);
|
|
22311
23508
|
}
|
|
22312
|
-
return interpret(res.status, parse(res.body), path3, contract);
|
|
23509
|
+
return setAsidePending(interpret(res.status, parse(res.body), path3, contract), pendingMetricKeys);
|
|
22313
23510
|
} finally {
|
|
22314
23511
|
client.cleanup();
|
|
22315
23512
|
}
|
|
@@ -22317,11 +23514,28 @@ async function checkPackage(dir, files, contract, opts, online) {
|
|
|
22317
23514
|
await rm3(scratch, { recursive: true, force: true });
|
|
22318
23515
|
}
|
|
22319
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
|
+
}
|
|
22320
23534
|
function interpret(status, body, path3, contract) {
|
|
22321
23535
|
if (typeof body === "string" && /^<(!doctype html|html)/i.test(body.trimStart())) {
|
|
22322
23536
|
return skip(`the request was intercepted before reaching App Space (HTTP ${String(status)}, an HTML page)`);
|
|
22323
23537
|
}
|
|
22324
|
-
if (status === 200 &&
|
|
23538
|
+
if (status === 200 && isObj4(body)) {
|
|
22325
23539
|
const answer = body;
|
|
22326
23540
|
if (answer.ok === true) return { problems: [], skipped: [] };
|
|
22327
23541
|
if (answer.ok === false) {
|
|
@@ -22370,7 +23584,7 @@ function fromRefusal(status, detail, contract) {
|
|
|
22370
23584
|
};
|
|
22371
23585
|
});
|
|
22372
23586
|
}
|
|
22373
|
-
if (
|
|
23587
|
+
if (isObj4(detail) && detail["error"] === "stale_revision") {
|
|
22374
23588
|
return [
|
|
22375
23589
|
{
|
|
22376
23590
|
code: "server:stale-revision",
|
|
@@ -22380,7 +23594,7 @@ function fromRefusal(status, detail, contract) {
|
|
|
22380
23594
|
}
|
|
22381
23595
|
];
|
|
22382
23596
|
}
|
|
22383
|
-
if (
|
|
23597
|
+
if (isObj4(detail) && Array.isArray(detail["seed"])) {
|
|
22384
23598
|
return detail["seed"].map((line2) => ({
|
|
22385
23599
|
code: "server:seed",
|
|
22386
23600
|
pointer: "/",
|
|
@@ -22388,7 +23602,7 @@ function fromRefusal(status, detail, contract) {
|
|
|
22388
23602
|
fix: "Fix the --seed file: every key a declared table, every row an object."
|
|
22389
23603
|
}));
|
|
22390
23604
|
}
|
|
22391
|
-
if (
|
|
23605
|
+
if (isObj4(detail) && Array.isArray(detail["problems"])) {
|
|
22392
23606
|
const hint = typeof detail["hint"] === "string" ? detail["hint"] : null;
|
|
22393
23607
|
return detail["problems"].map((line2) => fromLine(String(line2), refused, tables, hint));
|
|
22394
23608
|
}
|
|
@@ -22439,11 +23653,11 @@ async function stage(dir, scratch, files) {
|
|
|
22439
23653
|
}
|
|
22440
23654
|
function tablesOf2(contract) {
|
|
22441
23655
|
const out = /* @__PURE__ */ new Map();
|
|
22442
|
-
const app =
|
|
22443
|
-
const model =
|
|
22444
|
-
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"] : {};
|
|
22445
23659
|
for (const [name, table] of Object.entries(tables)) {
|
|
22446
|
-
const columns =
|
|
23660
|
+
const columns = isObj4(table) && isObj4(table["columns"]) ? table["columns"] : {};
|
|
22447
23661
|
out.set(name, new Set(Object.keys(columns)));
|
|
22448
23662
|
}
|
|
22449
23663
|
return out;
|
|
@@ -22461,15 +23675,15 @@ function parse(bytes) {
|
|
|
22461
23675
|
}
|
|
22462
23676
|
}
|
|
22463
23677
|
function detailOf(body) {
|
|
22464
|
-
return
|
|
23678
|
+
return isObj4(body) ? body["detail"] : void 0;
|
|
22465
23679
|
}
|
|
22466
23680
|
function sentence(text3) {
|
|
22467
23681
|
return text3.endsWith(".") ? text3 : `${text3}.`;
|
|
22468
23682
|
}
|
|
22469
|
-
function
|
|
23683
|
+
function firstLine2(err) {
|
|
22470
23684
|
return (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
|
|
22471
23685
|
}
|
|
22472
|
-
function
|
|
23686
|
+
function isObj4(value) {
|
|
22473
23687
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22474
23688
|
}
|
|
22475
23689
|
|
|
@@ -22511,8 +23725,8 @@ function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
|
|
|
22511
23725
|
else delete page.tree;
|
|
22512
23726
|
}
|
|
22513
23727
|
if (out.bindings !== void 0) out.bindings = renameDeep(out.bindings, rename2);
|
|
22514
|
-
for (const
|
|
22515
|
-
if (typeof
|
|
23728
|
+
for (const problem6 of Array.isArray(out.problems) ? out.problems : []) {
|
|
23729
|
+
if (typeof problem6.where === "string") problem6.where = fileOf(problem6.where);
|
|
22516
23730
|
}
|
|
22517
23731
|
return out;
|
|
22518
23732
|
}
|
|
@@ -22917,7 +24131,7 @@ function specSource(data, card) {
|
|
|
22917
24131
|
if (!isPlainObject11(row)) return;
|
|
22918
24132
|
cite(row.source);
|
|
22919
24133
|
if (typeof row.data === "string") readsOf(row.data);
|
|
22920
|
-
for (const next of [...
|
|
24134
|
+
for (const next of [...listOf2(row.depends), ...listOf2(row.about)]) visit(next);
|
|
22921
24135
|
};
|
|
22922
24136
|
visit(card);
|
|
22923
24137
|
return { refs, sentence: parts.join("; ") };
|
|
@@ -22926,7 +24140,7 @@ function lookupOf(step) {
|
|
|
22926
24140
|
if (!isPlainObject11(step)) return [];
|
|
22927
24141
|
return Object.values(step).flatMap((value) => isPlainObject11(value) && typeof value.lookup === "string" ? [value.lookup] : []);
|
|
22928
24142
|
}
|
|
22929
|
-
function
|
|
24143
|
+
function listOf2(value) {
|
|
22930
24144
|
return Array.isArray(value) ? value : [];
|
|
22931
24145
|
}
|
|
22932
24146
|
function named6(value) {
|
|
@@ -23229,7 +24443,7 @@ function checkPage2(app, page, kit, runtime = null) {
|
|
|
23229
24443
|
const missing = runtime === null ? [] : missingLibraries(page).map((one) => one.module);
|
|
23230
24444
|
const library = runtime === null ? loadsModule(page.path, page.text, WIDGET_LIBRARY2) : !missing.includes("dr-app-runtime") && !missing.includes("dr-app-widgets");
|
|
23231
24445
|
violations.push(...checkLibraryDraws(page, library, scripts, runtime === null ? null : missing));
|
|
23232
|
-
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 });
|
|
23233
24447
|
if (widgets.length > 0 && !library && !/drInsights\.setData\s*\(/.test(scriptText)) {
|
|
23234
24448
|
violations.push({
|
|
23235
24449
|
rule: "R3.4",
|
|
@@ -24021,10 +25235,10 @@ var PAGE_PATH4 = /\.html?$/i;
|
|
|
24021
25235
|
var AppLoadError2 = class extends Error {
|
|
24022
25236
|
};
|
|
24023
25237
|
function loadApp2(dir) {
|
|
24024
|
-
const manifest =
|
|
24025
|
-
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;
|
|
24026
25240
|
const handlersSource = existsSync2(join13(dir, "handlers.json")) ? "handlers.json" : "manifest.json";
|
|
24027
|
-
const handlersIndex = handlersSource === "handlers.json" ?
|
|
25241
|
+
const handlersIndex = handlersSource === "handlers.json" ? readJson3(dir, "handlers.json", false) : manifest.handlers ?? {};
|
|
24028
25242
|
if (handlersIndex === null || typeof handlersIndex !== "object" || Array.isArray(handlersIndex)) {
|
|
24029
25243
|
throw new AppLoadError2(`${handlersSource} must be an object mapping handler names to {http, file}`);
|
|
24030
25244
|
}
|
|
@@ -24053,7 +25267,7 @@ function readHandlerSource2(dir, file) {
|
|
|
24053
25267
|
if (!existsSync2(full) || !statSync2(full).isFile()) return "";
|
|
24054
25268
|
return readFileSync4(full, "utf8");
|
|
24055
25269
|
}
|
|
24056
|
-
function
|
|
25270
|
+
function readJson3(dir, path3, required) {
|
|
24057
25271
|
const full = join13(dir, path3);
|
|
24058
25272
|
if (!existsSync2(full)) {
|
|
24059
25273
|
if (required) throw new AppLoadError2(`${path3} is missing from the app version`);
|
|
@@ -25756,7 +26970,7 @@ function labelFor2(node, text3, tree) {
|
|
|
25756
26970
|
}
|
|
25757
26971
|
function textOf2(text3, node) {
|
|
25758
26972
|
const next = text3.indexOf("<", node.end);
|
|
25759
|
-
return
|
|
26973
|
+
return decodeEntities3(text3.slice(node.end, next === -1 ? void 0 : next).replace(/\s+/g, " ").trim());
|
|
25760
26974
|
}
|
|
25761
26975
|
function slotsOf2(node, tree) {
|
|
25762
26976
|
if (!(WIDGET_ATTR2 in node.attrs)) return {};
|
|
@@ -26223,7 +27437,7 @@ function specTransforms2(handlers, spec, problems) {
|
|
|
26223
27437
|
out.push({ path: path3, text: text3 });
|
|
26224
27438
|
}
|
|
26225
27439
|
}
|
|
26226
|
-
const unreadable = problems.some((
|
|
27440
|
+
const unreadable = problems.some((problem6) => problem6.code === "transform_unreadable");
|
|
26227
27441
|
for (const key of Object.keys(spec.table).filter(isWidgetKey2).sort(natural5)) {
|
|
26228
27442
|
const path3 = isPlainObject12(spec.table[key]) ? spec.table[key].transform : void 0;
|
|
26229
27443
|
if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
|
|
@@ -26519,7 +27733,7 @@ function natural5(a, b) {
|
|
|
26519
27733
|
}
|
|
26520
27734
|
var NAMED_ENTITIES2 = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
26521
27735
|
var ENTITY2 = /&(#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]{1,31});/g;
|
|
26522
|
-
function
|
|
27736
|
+
function decodeEntities3(value) {
|
|
26523
27737
|
const text3 = String(value);
|
|
26524
27738
|
if (!text3.includes("&")) return text3;
|
|
26525
27739
|
return text3.replace(ENTITY2, (whole, body) => {
|
|
@@ -26532,7 +27746,7 @@ function decodeEntities2(value) {
|
|
|
26532
27746
|
function decodedTree2(html) {
|
|
26533
27747
|
const tree = parseTree2(html);
|
|
26534
27748
|
for (const node of tree.nodes) {
|
|
26535
|
-
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);
|
|
26536
27750
|
}
|
|
26537
27751
|
return tree;
|
|
26538
27752
|
}
|
|
@@ -26569,9 +27783,9 @@ var MAX_HINT3 = 120;
|
|
|
26569
27783
|
function validate3(instance, schema, options = {}) {
|
|
26570
27784
|
const problems = [];
|
|
26571
27785
|
check3(instance, schema, "", schema, problems, { quote: options.quote === true, inBranch: false });
|
|
26572
|
-
return problems.map((
|
|
27786
|
+
return problems.map((problem6) => `${problem6.at}: ${problem6.why}`);
|
|
26573
27787
|
}
|
|
26574
|
-
function
|
|
27788
|
+
function problem5(at, why, kind = "other") {
|
|
26575
27789
|
return { at: at || "/", why, kind };
|
|
26576
27790
|
}
|
|
26577
27791
|
function describe4(node, ctx) {
|
|
@@ -26606,7 +27820,7 @@ function check3(instance, node, at, root, problems, ctx) {
|
|
|
26606
27820
|
if (typeof node.$ref === "string") {
|
|
26607
27821
|
const target = resolve10(node.$ref, root);
|
|
26608
27822
|
if (target === null) {
|
|
26609
|
-
problems.push(
|
|
27823
|
+
problems.push(problem5(at, "the schema names a reference it cannot resolve"));
|
|
26610
27824
|
return;
|
|
26611
27825
|
}
|
|
26612
27826
|
check3(instance, target, at, root, problems, ctx);
|
|
@@ -26614,29 +27828,29 @@ function check3(instance, node, at, root, problems, ctx) {
|
|
|
26614
27828
|
if ("type" in node) {
|
|
26615
27829
|
const names = Array.isArray(node.type) ? node.type : [node.type];
|
|
26616
27830
|
if (!names.some((name) => isType3(instance, name))) {
|
|
26617
|
-
problems.push(
|
|
27831
|
+
problems.push(problem5(at, `not of type ${names.join(" or ")}${hint}`, "type"));
|
|
26618
27832
|
return;
|
|
26619
27833
|
}
|
|
26620
27834
|
}
|
|
26621
|
-
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"));
|
|
26622
27836
|
if (Array.isArray(node.enum) && !node.enum.some((one) => same3(instance, one))) {
|
|
26623
|
-
problems.push(
|
|
27837
|
+
problems.push(problem5(at, `not one of the values this contract allows${hint}`, "value"));
|
|
26624
27838
|
}
|
|
26625
27839
|
if (typeof node.minimum === "number" && isType3(instance, "number") && instance < node.minimum) {
|
|
26626
|
-
problems.push(
|
|
27840
|
+
problems.push(problem5(at, `below the minimum this contract sets${hint}`));
|
|
26627
27841
|
}
|
|
26628
27842
|
if (typeof node.pattern === "string" && typeof instance === "string" && !new RegExp(node.pattern, "u").test(instance)) {
|
|
26629
27843
|
if (ctx.inBranch) {
|
|
26630
|
-
problems.push(
|
|
27844
|
+
problems.push(problem5(at, `${ctx.quote ? `${JSON.stringify(instance)} ` : ""}does not match ${node.pattern}${hint}`));
|
|
26631
27845
|
} else {
|
|
26632
|
-
problems.push(
|
|
27846
|
+
problems.push(problem5(at, "not in the form this contract requires"));
|
|
26633
27847
|
}
|
|
26634
27848
|
}
|
|
26635
27849
|
if (typeof node.minLength === "number" && typeof instance === "string" && [...instance].length < node.minLength) {
|
|
26636
|
-
problems.push(
|
|
27850
|
+
problems.push(problem5(at, `shorter than this contract allows${hint}`));
|
|
26637
27851
|
}
|
|
26638
27852
|
if (typeof node.maxLength === "number" && typeof instance === "string" && [...instance].length > node.maxLength) {
|
|
26639
|
-
problems.push(
|
|
27853
|
+
problems.push(problem5(at, `longer than this contract allows${hint}`));
|
|
26640
27854
|
}
|
|
26641
27855
|
if (isObject5(instance)) checkObject3(instance, node, at, root, problems, ctx);
|
|
26642
27856
|
if (Array.isArray(instance)) checkArray3(instance, node, at, root, problems, ctx, hint);
|
|
@@ -26672,16 +27886,16 @@ function checkBranches3(instance, branches, at, root, problems, ctx, wanted, sta
|
|
|
26672
27886
|
if (kept.length > MAX_PER_PLACE3) {
|
|
26673
27887
|
const more = kept.length - MAX_PER_PLACE3;
|
|
26674
27888
|
kept = kept.slice(0, MAX_PER_PLACE3);
|
|
26675
|
-
kept.push(
|
|
27889
|
+
kept.push(problem5(at, `and ${more} more problems here`, "more"));
|
|
26676
27890
|
}
|
|
26677
27891
|
for (const one of kept) problems.push(one);
|
|
26678
27892
|
} else {
|
|
26679
|
-
problems.push(
|
|
27893
|
+
problems.push(problem5(at, `matches none of the shapes this contract has (one of: ${shapes3(branches, root)})`));
|
|
26680
27894
|
}
|
|
26681
27895
|
} else if (matched === 0) {
|
|
26682
|
-
problems.push(
|
|
27896
|
+
problems.push(problem5(at, "matches none of the shapes this contract has"));
|
|
26683
27897
|
} else if (wanted !== null && matched > wanted) {
|
|
26684
|
-
problems.push(
|
|
27898
|
+
problems.push(problem5(at, "matches more than one shape this contract has"));
|
|
26685
27899
|
}
|
|
26686
27900
|
}
|
|
26687
27901
|
function shapeMisses3(found, at) {
|
|
@@ -26723,7 +27937,7 @@ function checkObject3(instance, node, at, root, problems, ctx) {
|
|
|
26723
27937
|
for (const name of Array.isArray(node.required) ? node.required : []) {
|
|
26724
27938
|
if (Object.prototype.hasOwnProperty.call(instance, name)) continue;
|
|
26725
27939
|
const wanted = Object.prototype.hasOwnProperty.call(properties, name) ? properties[name] : null;
|
|
26726
|
-
problems.push(
|
|
27940
|
+
problems.push(problem5(`${at}/${escape3(name)}`, `required and absent${describe4(wanted, ctx)}`, "required"));
|
|
26727
27941
|
}
|
|
26728
27942
|
for (const [name, value] of Object.entries(instance)) {
|
|
26729
27943
|
const where2 = `${at}/${escape3(name)}`;
|
|
@@ -26732,12 +27946,12 @@ function checkObject3(instance, node, at, root, problems, ctx) {
|
|
|
26732
27946
|
continue;
|
|
26733
27947
|
}
|
|
26734
27948
|
const extra = node.additionalProperties ?? true;
|
|
26735
|
-
if (extra === false) problems.push(
|
|
27949
|
+
if (extra === false) problems.push(problem5(where2, "not a key this contract carries"));
|
|
26736
27950
|
else if (isObject5(extra)) check3(value, extra, node["x-tenant-keys"] === true && !ctx.quote ? `${at}/*` : where2, root, problems, ctx);
|
|
26737
27951
|
}
|
|
26738
27952
|
}
|
|
26739
27953
|
function checkArray3(instance, node, at, root, problems, ctx, hint) {
|
|
26740
|
-
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}`));
|
|
26741
27955
|
if (isObject5(node.items)) instance.forEach((value, index) => check3(value, node.items, `${at}/${index}`, root, problems, ctx));
|
|
26742
27956
|
}
|
|
26743
27957
|
function isType3(instance, name) {
|
|
@@ -27224,8 +28438,8 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
|
|
|
27224
28438
|
if (produced.includes(key)) continue;
|
|
27225
28439
|
const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
|
|
27226
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>).`;
|
|
27227
|
-
const
|
|
27228
|
-
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}` });
|
|
27229
28443
|
}
|
|
27230
28444
|
}
|
|
27231
28445
|
return out;
|
|
@@ -27259,9 +28473,9 @@ function undeclaredCustomCalls2(contract, { moduleSource = () => void 0 } = {})
|
|
|
27259
28473
|
}
|
|
27260
28474
|
function undeclaredCallMessage2(id, module, handler, calls, how) {
|
|
27261
28475
|
const listed = calls.length === 0 ? "lists none" : `lists ${calls.join(", ")}`;
|
|
27262
|
-
const
|
|
28476
|
+
const problem6 = `custom "${id}" calls handler "${handler}" (ui.call ${how}, ui/${module}), and its declares.calls ${listed}.`;
|
|
27263
28477
|
const fix = `Add "${handler}" to declares.calls \u2014 and what it sends to declares.produces \u2014 or stop calling it.`;
|
|
27264
|
-
return { problem:
|
|
28478
|
+
return { problem: problem6, fix, message: `${problem6} ${fix}` };
|
|
27265
28479
|
}
|
|
27266
28480
|
function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
|
|
27267
28481
|
const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
|
|
@@ -27304,9 +28518,9 @@ function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
|
|
|
27304
28518
|
}
|
|
27305
28519
|
if (read) continue;
|
|
27306
28520
|
const id = idOf3(element, uid);
|
|
27307
|
-
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.`;
|
|
27308
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.`;
|
|
27309
|
-
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}` });
|
|
27310
28524
|
}
|
|
27311
28525
|
}
|
|
27312
28526
|
return out;
|
|
@@ -27322,9 +28536,9 @@ function stepsWithoutAction2(contract) {
|
|
|
27322
28536
|
const id = idOf3(element, uid);
|
|
27323
28537
|
const name = named8(step.id) ? step.id : String(index);
|
|
27324
28538
|
const next = named8(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
|
|
27325
|
-
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.`;
|
|
27326
28540
|
const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
|
|
27327
|
-
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}` });
|
|
27328
28542
|
});
|
|
27329
28543
|
}
|
|
27330
28544
|
return out;
|
|
@@ -27825,9 +29039,9 @@ function unboundControls(contract, files = {}) {
|
|
|
27825
29039
|
const param = page.params.find((one) => one?.name === name) ?? {};
|
|
27826
29040
|
const kind = named9(param.kind) ? param.kind : null;
|
|
27827
29041
|
const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
|
|
27828
|
-
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}.`;
|
|
27829
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.`;
|
|
27830
|
-
out.push({ page: page.path, param: name, kind, problem:
|
|
29044
|
+
out.push({ page: page.path, param: name, kind, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
27831
29045
|
}
|
|
27832
29046
|
}
|
|
27833
29047
|
return out;
|
|
@@ -27881,9 +29095,9 @@ function unreadParams(contract, { moduleSource = () => void 0, handlerSource = (
|
|
|
27881
29095
|
continue;
|
|
27882
29096
|
}
|
|
27883
29097
|
const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both2(named_.map((one) => `"${one}"`))}`;
|
|
27884
|
-
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.`;
|
|
27885
29099
|
const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
|
|
27886
|
-
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}` });
|
|
27887
29101
|
}
|
|
27888
29102
|
}
|
|
27889
29103
|
return out;
|
|
@@ -29107,9 +30321,9 @@ function unreachablePages2(contract, files = {}) {
|
|
|
29107
30321
|
else if (roles.length > 0) why = `it is for ${both3(quoted2(roles))}, and no page ${either5(quoted2(roles))} reaches links to it`;
|
|
29108
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`;
|
|
29109
30323
|
else why = "the only pages that link to it are pages no role reaches";
|
|
29110
|
-
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.`;
|
|
29111
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.";
|
|
29112
|
-
out.push({ page: page.path, problem:
|
|
30326
|
+
out.push({ page: page.path, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29113
30327
|
}
|
|
29114
30328
|
return out;
|
|
29115
30329
|
}
|
|
@@ -29255,9 +30469,9 @@ function unconfirmedStateChanges2(contract, files = {}) {
|
|
|
29255
30469
|
named_.push(one.module ? `${one.what} (on ${one.page})` : `${one.what} "${one.id}" (on ${one.page})`);
|
|
29256
30470
|
}
|
|
29257
30471
|
const kind = handlers[handler]?.role === "decide" ? "decides a row" : "changes a record's state";
|
|
29258
|
-
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.`;
|
|
29259
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.';
|
|
29260
|
-
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}` });
|
|
29261
30475
|
}
|
|
29262
30476
|
return out;
|
|
29263
30477
|
}
|
|
@@ -29335,18 +30549,18 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
|
|
|
29335
30549
|
for (const [key, pages] of [...sent.entries()].sort()) {
|
|
29336
30550
|
if (read.has(key)) continue;
|
|
29337
30551
|
const where2 = pages.size === 0 ? "the page" : either5([...pages].sort());
|
|
29338
|
-
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.`;
|
|
29339
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.`;
|
|
29340
|
-
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}` });
|
|
29341
30555
|
}
|
|
29342
30556
|
const lines = pyLines2(source);
|
|
29343
30557
|
if (!changesState2(name, row, source) || !REJECTS2.test(lines.join("\n"))) continue;
|
|
29344
30558
|
for (const key of REASON_KEYS2) {
|
|
29345
30559
|
if (!reads.optional.includes(key) || reads.required.includes(key)) continue;
|
|
29346
30560
|
if (requiresReason2(lines, key, reasonVars2(lines, key))) continue;
|
|
29347
|
-
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.`;
|
|
29348
30562
|
const fix = `Refuse the reject path without one: if decision == "rejected" and not ${key}: return {"error": "Give a reason to reject."}.`;
|
|
29349
|
-
out.push({ handler: name, key, kind: "optional", pages: [], problem:
|
|
30563
|
+
out.push({ handler: name, key, kind: "optional", pages: [], problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29350
30564
|
}
|
|
29351
30565
|
}
|
|
29352
30566
|
return out;
|
|
@@ -29383,9 +30597,9 @@ function unliftableRefusals2(contract, { handlerSource = () => void 0 } = {}) {
|
|
|
29383
30597
|
}
|
|
29384
30598
|
if (!refuses) continue;
|
|
29385
30599
|
const shown2 = line2.trim().replace(/:$/, "");
|
|
29386
|
-
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.`;
|
|
29387
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.`;
|
|
29388
|
-
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}` });
|
|
29389
30603
|
}
|
|
29390
30604
|
}
|
|
29391
30605
|
}
|
|
@@ -29401,13 +30615,13 @@ function rolesLandingNowhere2(contract) {
|
|
|
29401
30615
|
const exists = pagesOf2(contract).some((page) => page.path === landing);
|
|
29402
30616
|
if (exists && opens2(contract, model, role, landing)) continue;
|
|
29403
30617
|
const stated2 = model.home.get(role);
|
|
29404
|
-
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.`;
|
|
29405
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.`;
|
|
29406
|
-
out.push({ role, page: landing, problem:
|
|
30620
|
+
out.push({ role, page: landing, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
29407
30621
|
}
|
|
29408
30622
|
return out;
|
|
29409
30623
|
}
|
|
29410
|
-
function sourceRoles2(contract, name) {
|
|
30624
|
+
function sourceRoles2(contract, name, known = /* @__PURE__ */ new Set()) {
|
|
29411
30625
|
const source = contract?.data?.sources?.[name];
|
|
29412
30626
|
const stated2 = gateRoles2(source?.requiresRole);
|
|
29413
30627
|
const elements = isPlainObject17(contract?.elements) ? contract.elements : {};
|
|
@@ -29425,7 +30639,7 @@ function sourceRoles2(contract, name) {
|
|
|
29425
30639
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
29426
30640
|
}
|
|
29427
30641
|
for (const page of pagesOf2(contract)) {
|
|
29428
|
-
if (page.owned !== true || readers === 0) continue;
|
|
30642
|
+
if (page.owned !== true || readers === 0 || known.has(page.path)) continue;
|
|
29429
30643
|
const roles = pageRoles2(page);
|
|
29430
30644
|
if (roles.length === 0) open2 = true;
|
|
29431
30645
|
for (const role of roles) if (!union.includes(role)) union.push(role);
|
|
@@ -29442,8 +30656,8 @@ function openToEveryRole2(contract) {
|
|
|
29442
30656
|
const out = [];
|
|
29443
30657
|
const say = (kind, names, what, fix) => {
|
|
29444
30658
|
if (names.length === 0) return;
|
|
29445
|
-
const
|
|
29446
|
-
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}` });
|
|
29447
30661
|
};
|
|
29448
30662
|
say(
|
|
29449
30663
|
"page",
|
|
@@ -29830,7 +31044,7 @@ async function runCompile(dir, opts) {
|
|
|
29830
31044
|
for (const fill of layoutFills(contract)) report.compose.push(fill);
|
|
29831
31045
|
}
|
|
29832
31046
|
const built = await compileFolder(dir, kind, contract, kit, loaded.overlay);
|
|
29833
|
-
const refused = built.problems.filter((
|
|
31047
|
+
const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
|
|
29834
31048
|
if (Object.keys(built.files).length === 0 || refused.length > 0) {
|
|
29835
31049
|
const lines = (refused.length > 0 ? refused : built.problems).map(asProblem);
|
|
29836
31050
|
report.validate.problems = lines;
|
|
@@ -29840,6 +31054,30 @@ async function runCompile(dir, opts) {
|
|
|
29840
31054
|
else process.stdout.write(problemHeading(lines.length) + formatProblems(lines) + formatSkipped(skipped));
|
|
29841
31055
|
return 1;
|
|
29842
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
|
+
}
|
|
29843
31081
|
const paths = Object.keys(built.files).sort();
|
|
29844
31082
|
const escaping = paths.filter((path3) => !insideFolder(dir, path3));
|
|
29845
31083
|
if (escaping.length > 0) {
|
|
@@ -29886,12 +31124,12 @@ async function runCompile(dir, opts) {
|
|
|
29886
31124
|
report.digests = Object.fromEntries(plan.owned.map((path3) => [path3, digestOf(built.files[path3] ?? "")]));
|
|
29887
31125
|
report.inputs = plan.inputs;
|
|
29888
31126
|
}
|
|
29889
|
-
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);
|
|
29890
31128
|
} finally {
|
|
29891
31129
|
if (check5) await rm4(target, { recursive: true, force: true });
|
|
29892
31130
|
}
|
|
29893
31131
|
}
|
|
29894
|
-
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) {
|
|
29895
31133
|
const composed = kind === "dashboard" && !isRuntimeDashboard(contract) ? composeOf(contract) : built.contract ?? contract;
|
|
29896
31134
|
const extracted = await extractContract(target);
|
|
29897
31135
|
const diffs = roundTripDiffs(composed, extracted, built.problems, { order: built.order });
|
|
@@ -29928,21 +31166,29 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
|
|
|
29928
31166
|
return 4;
|
|
29929
31167
|
}
|
|
29930
31168
|
if (target !== dir) {
|
|
29931
|
-
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
|
+
);
|
|
29932
31177
|
const notChecked3 = [...skipped, ...server.skipped];
|
|
29933
31178
|
if ((opts.expect ?? []).length > 0) notChecked3.push({ what: "headline", why: "--check runs no load" });
|
|
29934
31179
|
report.validate.problems = server.problems;
|
|
29935
31180
|
report.validate.checks_skipped = notChecked3;
|
|
29936
31181
|
if (server.problems.length > 0) report.failure = "push-would-be-refused";
|
|
29937
31182
|
await writeReport(dir, report);
|
|
31183
|
+
const planned = metricsPlan === null ? "" : renderPlan(metricsPlan);
|
|
29938
31184
|
if (opts.json === true) printJson(report);
|
|
29939
31185
|
else if (server.problems.length > 0) {
|
|
29940
31186
|
process.stdout.write(
|
|
29941
|
-
problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
|
|
31187
|
+
planned + problemHeading(server.problems.length) + formatProblems(server.problems) + formatSkipped(notChecked3)
|
|
29942
31188
|
);
|
|
29943
31189
|
} else {
|
|
29944
31190
|
process.stdout.write(
|
|
29945
|
-
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)
|
|
29946
31192
|
);
|
|
29947
31193
|
}
|
|
29948
31194
|
return server.problems.length > 0 ? 1 : 0;
|
|
@@ -29951,9 +31197,31 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
|
|
|
29951
31197
|
await liveness(dir, kind, report, extracted, opts);
|
|
29952
31198
|
return finish(dir, report, skipped, opts);
|
|
29953
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
|
+
}
|
|
29954
31222
|
let pushed = null;
|
|
29955
31223
|
try {
|
|
29956
|
-
pushed = await pushFolder(dir, { ...opts, force: opts.force });
|
|
31224
|
+
pushed = await pushFolder(dir, { ...opts, force: opts.force, ...metricBindings === void 0 ? {} : { metricBindings } });
|
|
29957
31225
|
} catch (err) {
|
|
29958
31226
|
report.push = { pushed: false, stage: null, revision: null, contract_source: null };
|
|
29959
31227
|
report.failure = isStaleRevision(err) ? "stale-revision" : isLostRace(err) ? "concurrent-publish" : "push-rejected";
|
|
@@ -29977,7 +31245,11 @@ async function afterWrite(dir, target, kind, contract, built, report, kit, skipp
|
|
|
29977
31245
|
contract_source: "cli",
|
|
29978
31246
|
...typeof pushed?.code_version === "number" ? { code_version: pushed.code_version } : {}
|
|
29979
31247
|
};
|
|
31248
|
+
if (report.metrics !== void 0 && report.metrics !== null && pushed?.metric_bindings !== void 0) {
|
|
31249
|
+
report.metrics["push"] = pushed.metric_bindings;
|
|
31250
|
+
}
|
|
29980
31251
|
await liveness(dir, kind, report, extracted, opts);
|
|
31252
|
+
await metricsLiveness(report, extracted, opts);
|
|
29981
31253
|
return finish(dir, report, skipped, opts);
|
|
29982
31254
|
}
|
|
29983
31255
|
async function finish(dir, report, skipped, opts) {
|
|
@@ -30178,25 +31450,25 @@ async function writePlan(dir, kind, contract, files, paths, force) {
|
|
|
30178
31450
|
}
|
|
30179
31451
|
function ownedPaths(contract) {
|
|
30180
31452
|
const out = customHandlerPaths(contract);
|
|
30181
|
-
const data =
|
|
30182
|
-
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"]))]) {
|
|
30183
31455
|
const transform = isPlain(row) ? row["transform"] : void 0;
|
|
30184
31456
|
if (typeof transform === "string" && TRANSFORM_PATH2.test(transform)) out.add(transform);
|
|
30185
31457
|
}
|
|
30186
|
-
for (const element of Object.values(
|
|
31458
|
+
for (const element of Object.values(obj3(obj3(contract)["elements"]))) {
|
|
30187
31459
|
if (isPlain(element) && element["type"] === "custom") {
|
|
30188
|
-
const module =
|
|
31460
|
+
const module = obj3(element["props"])["module"];
|
|
30189
31461
|
if (typeof module === "string" && MODULE_RE.test(module)) out.add(`ui/${module}`);
|
|
30190
31462
|
}
|
|
30191
31463
|
}
|
|
30192
|
-
const pages =
|
|
31464
|
+
const pages = obj3(contract)["pages"];
|
|
30193
31465
|
for (const page of Array.isArray(pages) ? pages : []) {
|
|
30194
31466
|
if (isPlain(page) && page["owned"] === true && typeof page["path"] === "string") out.add(page["path"]);
|
|
30195
31467
|
}
|
|
30196
31468
|
return out;
|
|
30197
31469
|
}
|
|
30198
31470
|
function customHandlerPaths(contract) {
|
|
30199
|
-
const handlers =
|
|
31471
|
+
const handlers = obj3(obj3(obj3(obj3(contract)["data"])["app"])["handlers"]);
|
|
30200
31472
|
const out = /* @__PURE__ */ new Set();
|
|
30201
31473
|
for (const [name, row] of Object.entries(handlers)) {
|
|
30202
31474
|
if (isPlain(row) && row["role"] === "custom") out.add(`handlers/${name}.py`);
|
|
@@ -30239,7 +31511,7 @@ function roundTripDiffs(input, extracted, compilerProblems = [], { order: order2
|
|
|
30239
31511
|
const read = order2 === void 0 ? {} : { order: "keys" };
|
|
30240
31512
|
const left = stripPositional(normaliseApp(input), stated2);
|
|
30241
31513
|
const right = stripPositional(normaliseApp(withoutUnstatedChrome(input, extracted)), read);
|
|
30242
|
-
const declared = compilerProblems.some((
|
|
31514
|
+
const declared = compilerProblems.some((problem6) => problem6.code === DECLARED_LOSS);
|
|
30243
31515
|
const diffs = [];
|
|
30244
31516
|
for (const section of ["app", "pages", "elements", "data"]) {
|
|
30245
31517
|
if (!(section in left)) continue;
|
|
@@ -30314,7 +31586,7 @@ function normaliseApp(contract) {
|
|
|
30314
31586
|
}
|
|
30315
31587
|
const model = record2["data_model"];
|
|
30316
31588
|
if (isPlain(model)) {
|
|
30317
|
-
for (const table of Object.values(
|
|
31589
|
+
for (const table of Object.values(obj3(model["tables"]))) {
|
|
30318
31590
|
if (!isPlain(table)) continue;
|
|
30319
31591
|
const columns = table["columns"];
|
|
30320
31592
|
if (!isPlain(columns)) continue;
|
|
@@ -30334,7 +31606,7 @@ function normaliseApp(contract) {
|
|
|
30334
31606
|
}
|
|
30335
31607
|
}
|
|
30336
31608
|
}
|
|
30337
|
-
const roles =
|
|
31609
|
+
const roles = obj3(out["backend"])["roles"];
|
|
30338
31610
|
if (Array.isArray(roles)) {
|
|
30339
31611
|
for (const role of roles) {
|
|
30340
31612
|
if (isPlain(role) && Array.isArray(role["sees"]) && role["sees"].length === 0) delete role["sees"];
|
|
@@ -30342,7 +31614,7 @@ function normaliseApp(contract) {
|
|
|
30342
31614
|
}
|
|
30343
31615
|
return out;
|
|
30344
31616
|
}
|
|
30345
|
-
function
|
|
31617
|
+
function obj3(value) {
|
|
30346
31618
|
return isPlain(value) ? value : {};
|
|
30347
31619
|
}
|
|
30348
31620
|
function withoutLibrary(section) {
|
|
@@ -30686,7 +31958,7 @@ async function recordPulled(dir, contractWritten) {
|
|
|
30686
31958
|
} catch {
|
|
30687
31959
|
return forget();
|
|
30688
31960
|
}
|
|
30689
|
-
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))) {
|
|
30690
31962
|
return forget();
|
|
30691
31963
|
}
|
|
30692
31964
|
const custom = ownedPaths(contract);
|
|
@@ -30792,21 +32064,21 @@ async function pushedState(dir) {
|
|
|
30792
32064
|
const raw = await readIfPresent(join15(dir, ".appspace.json"));
|
|
30793
32065
|
if (raw === void 0) return { stage: null, revision: null };
|
|
30794
32066
|
try {
|
|
30795
|
-
const
|
|
32067
|
+
const parsed2 = JSON.parse(raw);
|
|
30796
32068
|
return {
|
|
30797
|
-
stage: typeof
|
|
30798
|
-
revision: typeof
|
|
32069
|
+
stage: typeof parsed2.stage === "string" ? parsed2.stage : null,
|
|
32070
|
+
revision: typeof parsed2.revision === "number" ? parsed2.revision : null
|
|
30799
32071
|
};
|
|
30800
32072
|
} catch {
|
|
30801
32073
|
return { stage: null, revision: null };
|
|
30802
32074
|
}
|
|
30803
32075
|
}
|
|
30804
|
-
function asProblem(
|
|
32076
|
+
function asProblem(problem6) {
|
|
30805
32077
|
return {
|
|
30806
|
-
code:
|
|
30807
|
-
pointer:
|
|
30808
|
-
message:
|
|
30809
|
-
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."
|
|
30810
32082
|
};
|
|
30811
32083
|
}
|
|
30812
32084
|
function livenessLine(report) {
|
|
@@ -30844,6 +32116,7 @@ function describe5(report, skipped) {
|
|
|
30844
32116
|
`;
|
|
30845
32117
|
out += warningLines(report);
|
|
30846
32118
|
out += livenessLine(report);
|
|
32119
|
+
out += metricsLines(report);
|
|
30847
32120
|
if (report.push !== null && report.push.pushed) {
|
|
30848
32121
|
out += `pushed \u2014 ${report.push.stage ?? "draft"}, revision ${String(report.push.revision ?? 0)}
|
|
30849
32122
|
`;
|
|
@@ -30851,10 +32124,149 @@ function describe5(report, skipped) {
|
|
|
30851
32124
|
out += formatSkipped(skipped);
|
|
30852
32125
|
return out;
|
|
30853
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
|
+
}
|
|
30854
32266
|
function registerCompileCommand(parent) {
|
|
30855
32267
|
withJsonOption(
|
|
30856
32268
|
withServerOptions(
|
|
30857
|
-
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(
|
|
30858
32270
|
"after",
|
|
30859
32271
|
[
|
|
30860
32272
|
"",
|
|
@@ -30876,6 +32288,7 @@ function registerCompileCommand(parent) {
|
|
|
30876
32288
|
'A declared Datarails metric (data.metrics, read by a source as from.financeos.metric "@<key>") is refused with:',
|
|
30877
32289
|
...Object.entries(METRIC_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
|
|
30878
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(),
|
|
30879
32292
|
"",
|
|
30880
32293
|
"A transform (a dashboard dataset series row's transforms/<name>.py, or an app source's) is refused with:",
|
|
30881
32294
|
...Object.entries(TRANSFORM_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`)
|
|
@@ -30907,7 +32320,7 @@ function registerShipCommand(parent) {
|
|
|
30907
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(
|
|
30908
32321
|
"--onto-live",
|
|
30909
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."
|
|
30910
|
-
)
|
|
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"))
|
|
30911
32324
|
)
|
|
30912
32325
|
)
|
|
30913
32326
|
).action(async (dirArg, opts) => {
|
|
@@ -31086,7 +32499,7 @@ async function runEject(target, opts) {
|
|
|
31086
32499
|
}
|
|
31087
32500
|
}
|
|
31088
32501
|
async function ejectHandler(dir, contract, name) {
|
|
31089
|
-
const handlers =
|
|
32502
|
+
const handlers = obj4(obj4(obj4(contract["data"])["app"])["handlers"]);
|
|
31090
32503
|
const row = handlers[name];
|
|
31091
32504
|
if (!isPlain2(row)) {
|
|
31092
32505
|
const known = Object.keys(handlers).sort();
|
|
@@ -31144,7 +32557,7 @@ function literalEnd2(text3, open2) {
|
|
|
31144
32557
|
return -1;
|
|
31145
32558
|
}
|
|
31146
32559
|
async function ejectSource(dir, contract, name) {
|
|
31147
|
-
const sources =
|
|
32560
|
+
const sources = obj4(obj4(contract["data"])["sources"]);
|
|
31148
32561
|
if (!isPlain2(sources[name])) {
|
|
31149
32562
|
const known = Object.keys(sources).sort();
|
|
31150
32563
|
throw new Refused(
|
|
@@ -31154,7 +32567,7 @@ async function ejectSource(dir, contract, name) {
|
|
|
31154
32567
|
const path3 = `handlers/${name}.py`;
|
|
31155
32568
|
if (!insideFolder(dir, path3)) throw new Refused(`${JSON.stringify(path3)} is not a path inside ${dir}.`);
|
|
31156
32569
|
const dependents = Object.keys(sources).filter((other) => {
|
|
31157
|
-
const from =
|
|
32570
|
+
const from = obj4(sources[other])["from"];
|
|
31158
32571
|
return Array.isArray(from) && from.includes(name);
|
|
31159
32572
|
}).sort();
|
|
31160
32573
|
if (dependents.length > 0) {
|
|
@@ -31180,9 +32593,9 @@ async function ejectSource(dir, contract, name) {
|
|
|
31180
32593
|
throw new Refused(`${path3} as compiled carries no SOURCES = {\u2026} table, so it is not a generated source handler.`);
|
|
31181
32594
|
}
|
|
31182
32595
|
await mayPlace(dir, path3, body, [compiled]);
|
|
31183
|
-
const data =
|
|
31184
|
-
const app =
|
|
31185
|
-
const handlers =
|
|
32596
|
+
const data = obj4(contract["data"]);
|
|
32597
|
+
const app = obj4(data["app"]);
|
|
32598
|
+
const handlers = obj4(app["handlers"]);
|
|
31186
32599
|
handlers[name] = { role: "custom" };
|
|
31187
32600
|
app["handlers"] = handlers;
|
|
31188
32601
|
data["app"] = app;
|
|
@@ -31210,8 +32623,8 @@ async function ejectPage(dir, contract, path3) {
|
|
|
31210
32623
|
throw new Refused(`${path3} as compiled has no <head> to carry ${OWNED_META}.`);
|
|
31211
32624
|
}
|
|
31212
32625
|
await mayPlace(dir, path3, marked2, [compiled]);
|
|
31213
|
-
const elements =
|
|
31214
|
-
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));
|
|
31215
32628
|
for (const uid of gone) delete elements[uid];
|
|
31216
32629
|
for (const element of Object.values(elements)) {
|
|
31217
32630
|
if (!isPlain2(element) || !Array.isArray(element["children"])) continue;
|
|
@@ -31222,11 +32635,11 @@ async function ejectPage(dir, contract, path3) {
|
|
|
31222
32635
|
kept["owned"] = true;
|
|
31223
32636
|
if (gate !== null && kept["roles"] === void 0) kept["role"] = gate;
|
|
31224
32637
|
pages[index] = kept;
|
|
31225
|
-
const app =
|
|
32638
|
+
const app = obj4(obj4(contract["data"])["app"]);
|
|
31226
32639
|
if (Array.isArray(app["screens"])) {
|
|
31227
32640
|
app["screens"] = app["screens"].filter((screen) => !(isPlain2(screen) && screen["page"] === path3));
|
|
31228
32641
|
}
|
|
31229
|
-
const sources = Object.keys(
|
|
32642
|
+
const sources = Object.keys(obj4(obj4(contract["data"])["sources"]));
|
|
31230
32643
|
const served = sources.filter((name) => files[`handlers/${name}.py`] !== void 0);
|
|
31231
32644
|
const wrote = await commit(dir, contract, path3, marked2, (after) => {
|
|
31232
32645
|
const lost = served.filter((name) => after[`handlers/${name}.py`] === void 0).sort();
|
|
@@ -31242,7 +32655,7 @@ function unservedAfter(path3, lost) {
|
|
|
31242
32655
|
const one = lost.length === 1;
|
|
31243
32656
|
const names = lost.map((name) => JSON.stringify(name)).join(", ");
|
|
31244
32657
|
const files = lost.map((name) => `handlers/${name}.py`).join(", ");
|
|
31245
|
-
return
|
|
32658
|
+
return `after this eject the compiler would write no handler for source${one ? "" : "s"} ${names}, which ${path3} reads, so ${files} would go and the page would call ${one ? "a handler" : "handlers"} that ${one ? "is" : "are"} not there. The compiler counts an owned page as a reader of what the contract it embeds reads, and eject keeps that contract on the page, so this is a bug in the compiler or in eject, not in your contract. Meanwhile, keep a card that reads ${one ? "it" : "each"} on a compiled page, or eject ${one ? "the source" : "each source"} first (app-space eject ${lost.map((name) => `source:${name}`).join(", ")}), which makes it a custom handler the compiler always carries, where that eject is allowed: eject source: says when it is not.`;
|
|
31246
32659
|
}
|
|
31247
32660
|
function navGateOf(files, path3) {
|
|
31248
32661
|
const gates = navGates;
|
|
@@ -31267,19 +32680,19 @@ ${OWNED_META}${html.slice(at2)}`;
|
|
|
31267
32680
|
${OWNED_META}${html.slice(at)}`;
|
|
31268
32681
|
}
|
|
31269
32682
|
async function ejectElement(dir, contract, uid) {
|
|
31270
|
-
const elements =
|
|
32683
|
+
const elements = obj4(contract["elements"]);
|
|
31271
32684
|
const element = elements[uid];
|
|
31272
32685
|
if (!isPlain2(element)) {
|
|
31273
32686
|
throw new Refused(`the contract has no element ${JSON.stringify(uid)}; an element's uid is its key in contract.elements.`);
|
|
31274
32687
|
}
|
|
31275
32688
|
const type = String(element["type"] ?? "");
|
|
31276
|
-
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")}.`);
|
|
31277
32690
|
if (!EJECTABLE.has(type)) {
|
|
31278
32691
|
throw new Refused(
|
|
31279
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>")}.`
|
|
31280
32693
|
);
|
|
31281
32694
|
}
|
|
31282
|
-
const props =
|
|
32695
|
+
const props = obj4(element["props"]);
|
|
31283
32696
|
const name = freeName(moduleName(props["id"], type, uid), ownedPaths(contract));
|
|
31284
32697
|
const module = `components/${name}.js`;
|
|
31285
32698
|
const path3 = `ui/${module}`;
|
|
@@ -31360,9 +32773,9 @@ ${lines}
|
|
|
31360
32773
|
return built.files;
|
|
31361
32774
|
}
|
|
31362
32775
|
function refusedLines(built) {
|
|
31363
|
-
const refused = built.problems.filter((
|
|
32776
|
+
const refused = built.problems.filter((problem6) => REFUSALS.has(problem6.code));
|
|
31364
32777
|
if (Object.keys(built.files).length > 0 && refused.length === 0) return null;
|
|
31365
|
-
return (refused.length > 0 ? refused : built.problems).map((
|
|
32778
|
+
return (refused.length > 0 ? refused : built.problems).map((problem6) => ` [${problem6.code}] ${problem6.message}`).join("\n");
|
|
31366
32779
|
}
|
|
31367
32780
|
async function mayPlace(dir, path3, text3, same5) {
|
|
31368
32781
|
const existing = await readIfPresent(join16(dir, path3));
|
|
@@ -31407,7 +32820,7 @@ ${lines}
|
|
|
31407
32820
|
function isPlain2(value) {
|
|
31408
32821
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31409
32822
|
}
|
|
31410
|
-
function
|
|
32823
|
+
function obj4(value) {
|
|
31411
32824
|
return isPlain2(value) ? value : {};
|
|
31412
32825
|
}
|
|
31413
32826
|
function registerEjectCommand(parent) {
|
|
@@ -31639,13 +33052,13 @@ function parseFileSpec(spec) {
|
|
|
31639
33052
|
return { field, path: path3 };
|
|
31640
33053
|
}
|
|
31641
33054
|
function parseFileSpecs(specs) {
|
|
31642
|
-
const
|
|
33055
|
+
const parsed2 = specs.map(parseFileSpec);
|
|
31643
33056
|
const seen = /* @__PURE__ */ new Set();
|
|
31644
|
-
for (const one of
|
|
33057
|
+
for (const one of parsed2) {
|
|
31645
33058
|
if (seen.has(one.field)) throw new Error(`--file ${one.field} is given twice; give each field one file.`);
|
|
31646
33059
|
seen.add(one.field);
|
|
31647
33060
|
}
|
|
31648
|
-
return
|
|
33061
|
+
return parsed2;
|
|
31649
33062
|
}
|
|
31650
33063
|
var MIME_TYPES = {
|
|
31651
33064
|
".pdf": "application/pdf",
|
|
@@ -32836,15 +34249,15 @@ function answerOf(res) {
|
|
|
32836
34249
|
if (res.body.length === 0 || res.body.length > ROWS_BODY_LIMIT) return {};
|
|
32837
34250
|
const type = Object.entries(res.headers).find(([key]) => key.toLowerCase() === "content-type")?.[1];
|
|
32838
34251
|
if (type !== void 0 && !/json/i.test(type)) return {};
|
|
32839
|
-
let
|
|
34252
|
+
let parsed2;
|
|
32840
34253
|
try {
|
|
32841
|
-
|
|
34254
|
+
parsed2 = JSON.parse(res.body.toString("utf8"));
|
|
32842
34255
|
} catch {
|
|
32843
34256
|
return {};
|
|
32844
34257
|
}
|
|
32845
|
-
if (typeof
|
|
34258
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return {};
|
|
32846
34259
|
const out = { object: true };
|
|
32847
|
-
const { rows: rows2, error, needs } =
|
|
34260
|
+
const { rows: rows2, error, needs } = parsed2;
|
|
32848
34261
|
if (needs && typeof needs === "object" && !Array.isArray(needs)) {
|
|
32849
34262
|
const reason = needs.reason;
|
|
32850
34263
|
if (reason === true || reason && typeof reason === "object" && !Array.isArray(reason)) {
|
|
@@ -32925,8 +34338,8 @@ function harvestNodes(document) {
|
|
|
32925
34338
|
const filter = el.getAttribute(FILTER);
|
|
32926
34339
|
if (filter !== null && filter !== "") props.filter = readFilter(filter);
|
|
32927
34340
|
const args = el.getAttribute(ARGS);
|
|
32928
|
-
const
|
|
32929
|
-
if (
|
|
34341
|
+
const parsed2 = args !== null && args !== "" ? readArgs(args) : null;
|
|
34342
|
+
if (parsed2 !== null) props.args = parsed2;
|
|
32930
34343
|
const typed = el.getAttribute(PROPS);
|
|
32931
34344
|
const typedProps = typed !== null && typed !== "" ? readProps(typed) : null;
|
|
32932
34345
|
if (typedProps !== null) props.props = typedProps;
|
|
@@ -33222,8 +34635,8 @@ function createCanvas2dStub(canvas) {
|
|
|
33222
34635
|
for (const name of CANVAS_2D_VOID_METHODS) target[name] = () => void 0;
|
|
33223
34636
|
const spares = /* @__PURE__ */ new Map();
|
|
33224
34637
|
return new Proxy(target, {
|
|
33225
|
-
get(
|
|
33226
|
-
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);
|
|
33227
34640
|
if (prop === "then") return void 0;
|
|
33228
34641
|
let spare = spares.get(prop);
|
|
33229
34642
|
if (spare === void 0) {
|
|
@@ -33507,13 +34920,13 @@ async function runUiSmoke(opts) {
|
|
|
33507
34920
|
const graceUntil = Date.now() + graceMs;
|
|
33508
34921
|
while (Date.now() < deadline) {
|
|
33509
34922
|
if (drivePrompt?.()) lastActivity = Date.now();
|
|
33510
|
-
const
|
|
34923
|
+
const parsed2 = dom.window.document.readyState === "complete";
|
|
33511
34924
|
const started = calls.length > since || inFlight > 0;
|
|
33512
34925
|
if (!started && Date.now() < graceUntil) {
|
|
33513
34926
|
await sleep(25);
|
|
33514
34927
|
continue;
|
|
33515
34928
|
}
|
|
33516
|
-
if (
|
|
34929
|
+
if (parsed2 && inFlight === 0 && Date.now() - lastActivity >= quietMs) break;
|
|
33517
34930
|
await sleep(25);
|
|
33518
34931
|
}
|
|
33519
34932
|
}
|
|
@@ -34788,9 +36201,9 @@ function unboundControls2(contract, files = {}) {
|
|
|
34788
36201
|
const param = page.params.find((one) => one?.name === name) ?? {};
|
|
34789
36202
|
const kind = named12(param.kind) ? param.kind : null;
|
|
34790
36203
|
const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
|
|
34791
|
-
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}.`;
|
|
34792
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.`;
|
|
34793
|
-
out.push({ page: page.path, param: name, kind, problem:
|
|
36206
|
+
out.push({ page: page.path, param: name, kind, problem: problem6, fix, message: `${problem6} ${fix}` });
|
|
34794
36207
|
}
|
|
34795
36208
|
}
|
|
34796
36209
|
return out;
|
|
@@ -34844,9 +36257,9 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
|
|
|
34844
36257
|
continue;
|
|
34845
36258
|
}
|
|
34846
36259
|
const there = named_.length === 0 ? "no component there reads a source" : `its sources are ${both4(named_.map((one) => `"${one}"`))}`;
|
|
34847
|
-
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.`;
|
|
34848
36261
|
const fix = `Add "${name}" to the honors of the source it should move${named_.length === 1 ? ` ("${named_[0]}")` : ""}, or drop the param.`;
|
|
34849
|
-
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}` });
|
|
34850
36263
|
}
|
|
34851
36264
|
}
|
|
34852
36265
|
return out;
|
|
@@ -34888,9 +36301,9 @@ function pushedContract(bundle, pages) {
|
|
|
34888
36301
|
const found = contractBlockText(html);
|
|
34889
36302
|
if (found === null) continue;
|
|
34890
36303
|
try {
|
|
34891
|
-
const
|
|
34892
|
-
if (typeof
|
|
34893
|
-
return
|
|
36304
|
+
const parsed2 = JSON.parse(found);
|
|
36305
|
+
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
|
|
36306
|
+
return parsed2;
|
|
34894
36307
|
}
|
|
34895
36308
|
} catch {
|
|
34896
36309
|
}
|
|
@@ -35823,11 +37236,11 @@ function customDeclaresFindings(contract, bundle, runs) {
|
|
|
35823
37236
|
if (contract === null || !isAppContract3(contract)) return [];
|
|
35824
37237
|
const out = [];
|
|
35825
37238
|
const said2 = /* @__PURE__ */ new Set();
|
|
35826
|
-
const undeclared = (uid, page, module, handler,
|
|
37239
|
+
const undeclared = (uid, page, module, handler, problem6, fix) => {
|
|
35827
37240
|
const key = `${uid}|${handler}`;
|
|
35828
37241
|
if (said2.has(key)) return;
|
|
35829
37242
|
said2.add(key);
|
|
35830
|
-
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 });
|
|
35831
37244
|
};
|
|
35832
37245
|
for (const found of DECLARES.undeclaredCustomCalls(contract, pushedFiles(bundle, {}))) {
|
|
35833
37246
|
undeclared(found.uid, found.page, found.module, found.handler, found.problem, found.fix);
|
|
@@ -35840,8 +37253,8 @@ function customDeclaresFindings(contract, bundle, runs) {
|
|
|
35840
37253
|
if (element?.type !== "custom" || declares === null || typeof module !== "string") continue;
|
|
35841
37254
|
if (declares.calls.includes(entry.handler)) continue;
|
|
35842
37255
|
const id = typeof element.props?.["id"] === "string" ? element.props["id"] : entry.uid;
|
|
35843
|
-
const { problem:
|
|
35844
|
-
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);
|
|
35845
37258
|
}
|
|
35846
37259
|
}
|
|
35847
37260
|
for (const run of runs) {
|
|
@@ -36171,13 +37584,13 @@ async function loadFixtures(dir, explicit) {
|
|
|
36171
37584
|
if (explicit !== void 0) throw new Error(`no fixtures file at ${path3}.`);
|
|
36172
37585
|
return { fixtures: { cases: [], ui: { entry: "index.html" } } };
|
|
36173
37586
|
}
|
|
36174
|
-
let
|
|
37587
|
+
let parsed2;
|
|
36175
37588
|
try {
|
|
36176
|
-
|
|
37589
|
+
parsed2 = JSON.parse(raw);
|
|
36177
37590
|
} catch {
|
|
36178
37591
|
throw new FixtureError(`${path3} is not valid JSON.`);
|
|
36179
37592
|
}
|
|
36180
|
-
return { fixtures: parseFixtures(
|
|
37593
|
+
return { fixtures: parseFixtures(parsed2, path3), source: path3 };
|
|
36181
37594
|
}
|
|
36182
37595
|
async function runHandlerCases(cases, dir, appId, client, roleOf = () => void 0) {
|
|
36183
37596
|
const code = /* @__PURE__ */ new Map();
|
|
@@ -36505,7 +37918,7 @@ ${attached}`;
|
|
|
36505
37918
|
}
|
|
36506
37919
|
out += ` FAIL ${head}
|
|
36507
37920
|
${attached}`;
|
|
36508
|
-
for (const
|
|
37921
|
+
for (const problem6 of outcome.problems) out += ` ${problem6}
|
|
36509
37922
|
`;
|
|
36510
37923
|
}
|
|
36511
37924
|
return out;
|
|
@@ -36570,7 +37983,7 @@ function rederiveComponentBindings(contract) {
|
|
|
36570
37983
|
model
|
|
36571
37984
|
});
|
|
36572
37985
|
contract.problems = [
|
|
36573
|
-
...contract.problems.filter((
|
|
37986
|
+
...contract.problems.filter((problem6) => !COMPONENT_PROBLEM_CODES.has(problem6.code)),
|
|
36574
37987
|
...problems
|
|
36575
37988
|
].sort(byProblem4);
|
|
36576
37989
|
}
|
|
@@ -36626,7 +38039,7 @@ function mergeDomComponents(contract, components, page) {
|
|
|
36626
38039
|
contract.elements[uid] = element;
|
|
36627
38040
|
}
|
|
36628
38041
|
contract.problems = contract.problems.filter(
|
|
36629
|
-
(
|
|
38042
|
+
(problem6) => !(problem6.code === "duplicate_dom_id" && problem6.where.startsWith(`${page}@dom#`))
|
|
36630
38043
|
);
|
|
36631
38044
|
for (const { id, uid } of duplicates) {
|
|
36632
38045
|
contract.problems.push({
|
|
@@ -36739,15 +38152,15 @@ function untouchedHandlers(declared, cases, interactions, runs = []) {
|
|
|
36739
38152
|
return declared.filter((name) => !touched.has(name) && !calls.some((call) => callsHandler(call.url, name))).sort();
|
|
36740
38153
|
}
|
|
36741
38154
|
async function readSeed(dir) {
|
|
36742
|
-
let
|
|
38155
|
+
let parsed2;
|
|
36743
38156
|
try {
|
|
36744
|
-
|
|
38157
|
+
parsed2 = JSON.parse(await readFile13(join18(dir, "seed.json"), "utf8"));
|
|
36745
38158
|
} catch {
|
|
36746
38159
|
return null;
|
|
36747
38160
|
}
|
|
36748
|
-
if (typeof
|
|
38161
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) return null;
|
|
36749
38162
|
const seed = {};
|
|
36750
|
-
for (const [table, rows2] of Object.entries(
|
|
38163
|
+
for (const [table, rows2] of Object.entries(parsed2)) {
|
|
36751
38164
|
if (!Array.isArray(rows2)) continue;
|
|
36752
38165
|
seed[table] = rows2.filter(
|
|
36753
38166
|
(row) => typeof row === "object" && row !== null && !Array.isArray(row)
|
|
@@ -37241,11 +38654,16 @@ function describeTryRole(one) {
|
|
|
37241
38654
|
function rolesAllowed(handler, view) {
|
|
37242
38655
|
return view.roles.filter((role) => view.allowed[role]?.[handler] === true);
|
|
37243
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
|
+
}
|
|
37244
38662
|
function registerTryCommand(parent) {
|
|
37245
38663
|
withJsonOption(
|
|
37246
38664
|
withServerOptions(
|
|
37247
38665
|
parent.command("try <handler>").description(
|
|
37248
|
-
|
|
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.`
|
|
37249
38667
|
).option("--dir <path>", "The app folder (default: current directory).", ".").option(
|
|
37250
38668
|
"--payload <json>",
|
|
37251
38669
|
"JSON payload for the handler, or @<path> to read it from a JSON file (relative to the working directory).",
|
|
@@ -37325,6 +38743,9 @@ function registerTryCommand(parent) {
|
|
|
37325
38743
|
if (res.ok) {
|
|
37326
38744
|
process.stdout.write(`ok
|
|
37327
38745
|
${JSON.stringify(res.result, null, 2)}
|
|
38746
|
+
`);
|
|
38747
|
+
const note = unboundNote(res.result);
|
|
38748
|
+
if (note !== null) process.stdout.write(`note: ${note}
|
|
37328
38749
|
`);
|
|
37329
38750
|
return;
|
|
37330
38751
|
}
|