@mondaydotcomorg/z2h-cli 0.33.0 → 0.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/__tests__/format-sandbox-run.test.d.ts +2 -0
- package/dist/backend/__tests__/format-sandbox-run.test.d.ts.map +1 -0
- package/dist/backend/client.d.ts +32 -2
- package/dist/backend/client.d.ts.map +1 -1
- package/dist/backend/client.js +8 -5
- package/dist/backend/format-sandbox-run.d.ts +4 -0
- package/dist/backend/format-sandbox-run.d.ts.map +1 -0
- package/dist/backend/format-sandbox-run.js +49 -0
- package/dist/commands/__tests__/backend-invoke.test.d.ts +2 -0
- package/dist/commands/__tests__/backend-invoke.test.d.ts.map +1 -0
- package/dist/commands/backend.d.ts +12 -6
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +41 -15
- package/dist/commands/deploy.js +29 -16
- package/dist/esm/backend/__tests__/format-sandbox-run.test.d.ts +2 -0
- package/dist/esm/backend/__tests__/format-sandbox-run.test.d.ts.map +1 -0
- package/dist/esm/backend/client.d.ts +32 -2
- package/dist/esm/backend/client.d.ts.map +1 -1
- package/dist/esm/backend/client.mjs +8 -5
- package/dist/esm/backend/format-sandbox-run.d.ts +4 -0
- package/dist/esm/backend/format-sandbox-run.d.ts.map +1 -0
- package/dist/esm/backend/format-sandbox-run.mjs +47 -0
- package/dist/esm/commands/__tests__/backend-invoke.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/backend-invoke.test.d.ts.map +1 -0
- package/dist/esm/commands/backend.d.ts +12 -6
- package/dist/esm/commands/backend.d.ts.map +1 -1
- package/dist/esm/commands/backend.mjs +42 -16
- package/dist/esm/commands/deploy.mjs +29 -16
- package/dist/esm/index.mjs +9 -4
- package/dist/esm/util/broker/app.d.ts +2 -0
- package/dist/esm/util/broker/app.d.ts.map +1 -1
- package/dist/esm/util/broker/app.mjs +20 -7
- package/dist/index.js +9 -4
- package/dist/util/broker/app.d.ts +2 -0
- package/dist/util/broker/app.d.ts.map +1 -1
- package/dist/util/broker/app.js +20 -7
- package/handler-api/backend-runner/backend-runner.errors.ts +12 -0
- package/handler-api/backend-runner/backend-runner.types.ts +3 -0
- package/handler-api/backend-runner/channels/channel-map.ts +12 -3
- package/handler-api/backend-runner/channels/channel-telemetry.ts +13 -7
- package/handler-api/backend-runner/channels/channel-trace.ts +52 -0
- package/package.json +2 -2
- package/src/backend/__tests__/client.test.ts +17 -13
- package/src/backend/__tests__/format-sandbox-run.test.ts +42 -0
- package/src/backend/client.ts +27 -10
- package/src/backend/format-sandbox-run.ts +51 -0
- package/src/commands/__tests__/backend-deploy.test.ts +42 -21
- package/src/commands/__tests__/backend-invoke.test.ts +125 -0
- package/src/commands/__tests__/deploy.test.ts +53 -0
- package/src/commands/backend.ts +57 -18
- package/src/commands/deploy.ts +31 -17
- package/src/index.ts +13 -5
- package/src/util/broker/app.ts +59 -50
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
4
|
import { readJson, writeJson } from 'fs-extra';
|
|
4
5
|
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
5
6
|
import { output, info } from '../util/logger.mjs';
|
|
@@ -9,7 +10,9 @@ import { readHandlers } from '../backend/handlers.mjs';
|
|
|
9
10
|
import { detectIntegrations } from '../backend/detect-integrations.mjs';
|
|
10
11
|
import { getS3Client } from '../util/s3/client.mjs';
|
|
11
12
|
import { BUCKET_WRITE_NAME } from '../constants.mjs';
|
|
12
|
-
import {
|
|
13
|
+
import { runSandbox } from '../backend/client.mjs';
|
|
14
|
+
import { formatSandboxRun } from '../backend/format-sandbox-run.mjs';
|
|
15
|
+
import { validateHandlerSource } from '../backend/validate.mjs';
|
|
13
16
|
import { tagDeployError } from '../util/errors.mjs';
|
|
14
17
|
|
|
15
18
|
function currentAppName() {
|
|
@@ -39,21 +42,19 @@ async function writeUsageJson(appDir, patch) {
|
|
|
39
42
|
const existing = await readExistingUsage(appDir);
|
|
40
43
|
await writeJson(path.join(appDir, 'usage.json'), { ...existing, ...patch }, { spaces: 2 });
|
|
41
44
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Returns `[]` when there is no `backend/handlers/` dir (or it is empty of sources)
|
|
44
|
-
* so callers can always pass the result to `registerOrUpdateApp` without a separate gate.
|
|
45
|
-
*/
|
|
46
45
|
async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy, logLabel) {
|
|
47
46
|
if (!hasHandlers(appDir)) {
|
|
48
|
-
|
|
47
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages: {} });
|
|
48
|
+
return { integrations: [], handlerNames: [], apiUsages: {} };
|
|
49
49
|
}
|
|
50
50
|
info(`reading backend handlers ${logLabel}`);
|
|
51
51
|
const { handlers, apiDomains, apiUsages } = await readHandlers(appDir);
|
|
52
52
|
const integrations = detectIntegrations(apiDomains);
|
|
53
|
+
const handlerNames = Object.keys(handlers);
|
|
54
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
53
55
|
if (Object.keys(handlers).length === 0) {
|
|
54
|
-
return integrations;
|
|
56
|
+
return { integrations, handlerNames, apiUsages };
|
|
55
57
|
}
|
|
56
|
-
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
57
58
|
const s3 = getS3Client();
|
|
58
59
|
const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
|
|
59
60
|
const put = (key, body, contentType) => s3.send(new PutObjectCommand({ Bucket: BUCKET_WRITE_NAME, Key: key, Body: body, ContentType: contentType }));
|
|
@@ -61,13 +62,9 @@ async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy,
|
|
|
61
62
|
...Object.entries(handlers).map(([name, code]) => put(`${prefix}/${name}.js`, code, 'application/javascript')),
|
|
62
63
|
put(`${prefix}/meta.json`, meta, 'application/json'),
|
|
63
64
|
]);
|
|
64
|
-
return integrations;
|
|
65
|
+
return { integrations, handlerNames, apiUsages };
|
|
65
66
|
}
|
|
66
67
|
/**
|
|
67
|
-
* Deploy the app's backend handlers to S3 and return the integrations detected
|
|
68
|
-
* from their sources. Each `.js` handler is uploaded as its own flat file — no
|
|
69
|
-
* compilation, no bundling, no npm deps.
|
|
70
|
-
*
|
|
71
68
|
* Integration sync note: the returned set is re-derived from the CURRENT backend
|
|
72
69
|
* sources on every deploy, and the platform set is reconciled to exactly match
|
|
73
70
|
* it (there is no stored declaration to fall back on). A deploy therefore always
|
|
@@ -105,7 +102,29 @@ async function backendValidateCommand(appDir = process.cwd()) {
|
|
|
105
102
|
integrations,
|
|
106
103
|
});
|
|
107
104
|
}
|
|
108
|
-
async function
|
|
105
|
+
async function readHandlerSource(appDir, handlerName) {
|
|
106
|
+
const dir = path.join(appDir, 'backend', 'handlers');
|
|
107
|
+
try {
|
|
108
|
+
return await readFile(path.join(dir, `${handlerName}.js`), 'utf8');
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err.code !== 'ENOENT') {
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
const files = hasHandlers(appDir) ? await readdir(dir) : [];
|
|
115
|
+
const available = files
|
|
116
|
+
.filter(f => f.endsWith('.js'))
|
|
117
|
+
.map(f => path.parse(f).name)
|
|
118
|
+
.sort();
|
|
119
|
+
throw new Error(`no handler "${handlerName}" in backend/handlers/` +
|
|
120
|
+
(available.length ? ` (available: ${available.join(', ')})` : ' (no handlers found)'));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Runs a handler from the working tree (or `--code`) on the runner's sandbox and
|
|
125
|
+
* prints the trace. Validated locally first, like deploy; exits 1 when the handler fails.
|
|
126
|
+
*/
|
|
127
|
+
async function backendInvokeCommand(handlerName, opts = {}, appDir = process.cwd()) {
|
|
109
128
|
const appName = currentAppName();
|
|
110
129
|
let input = null;
|
|
111
130
|
if (opts.input) {
|
|
@@ -116,8 +135,15 @@ async function backendInvokeCommand(handlerName, opts = {}) {
|
|
|
116
135
|
throw new Error(`--input must be valid JSON (got: ${opts.input})`);
|
|
117
136
|
}
|
|
118
137
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
138
|
+
const file = `${handlerName}.js`;
|
|
139
|
+
const code = opts.code ?? (await readHandlerSource(appDir, handlerName));
|
|
140
|
+
validateHandlerSource(file, code);
|
|
141
|
+
info(`validated ${opts.code ? '--code' : file}`);
|
|
142
|
+
const res = await runSandbox(appName, { code, input, handlerName });
|
|
143
|
+
output(formatSandboxRun(res), res);
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
121
147
|
}
|
|
122
148
|
|
|
123
149
|
export { backendInvokeCommand, backendValidateCommand, deployBackend, hasHandlers };
|
|
@@ -79,12 +79,9 @@ async function buildAndStamp(opts) {
|
|
|
79
79
|
*/
|
|
80
80
|
async function uploadAndRegister(opts) {
|
|
81
81
|
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// reconciled to exactly match on register — see `deployBackend` for why this
|
|
86
|
-
// depends on deploying from the app's latest merged state.
|
|
87
|
-
const integrations = await deployBackend({
|
|
82
|
+
// Runs before the frontend upload so a `deployBackend` failure can't leave a
|
|
83
|
+
// preview's unversioned `MF` dir overwritten with no rollback.
|
|
84
|
+
const { integrations, handlerNames, apiUsages } = await deployBackend({
|
|
88
85
|
appDir: process.cwd(),
|
|
89
86
|
appName,
|
|
90
87
|
mfKeyPrefix: keyPrefix,
|
|
@@ -92,16 +89,32 @@ async function uploadAndRegister(opts) {
|
|
|
92
89
|
deployedBy: whoAmI(),
|
|
93
90
|
isPreview,
|
|
94
91
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
try {
|
|
93
|
+
info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
94
|
+
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
95
|
+
await registerOrUpdateApp({
|
|
96
|
+
appName,
|
|
97
|
+
token,
|
|
98
|
+
nextVersion,
|
|
99
|
+
entry,
|
|
100
|
+
gitRemote,
|
|
101
|
+
integrations,
|
|
102
|
+
handlerNames,
|
|
103
|
+
apiUsages,
|
|
104
|
+
description,
|
|
105
|
+
masterAppName,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// A live deploy is safe either way — RDS's version gates what's actually served, so
|
|
110
|
+
// an orphaned S3 object is harmless — but a preview is served straight from its
|
|
111
|
+
// unversioned prefix with no such gate, so only it needs the warning below.
|
|
112
|
+
if (isPreview) {
|
|
113
|
+
warn(`preview backend was already deployed before this failed — re-run "z2h-cli deploy --preview" to retry ` +
|
|
114
|
+
`(every step overwrites, so a retry fully repairs it)`);
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
105
118
|
}
|
|
106
119
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
107
120
|
// A preview inherits from the live app it previews, resolved by name server-side —
|
package/dist/esm/index.mjs
CHANGED
|
@@ -180,15 +180,18 @@ async function main() {
|
|
|
180
180
|
.action(runCommand({ data: cwdApp }, (opts) => deleteCommand(opts)));
|
|
181
181
|
const backend = program
|
|
182
182
|
.command('backend')
|
|
183
|
-
.description("Inspect the app's backend: validate handler sources or
|
|
183
|
+
.description("Inspect the app's backend: validate handler sources or run a handler from the working tree");
|
|
184
184
|
backend
|
|
185
185
|
.command('validate')
|
|
186
186
|
.description('Validate backend/handlers/*.js exactly as deploy would (syntax, isolate rules, integrations) without uploading')
|
|
187
187
|
.action(runCommand({ data: cwdApp }, () => backendValidateCommand()));
|
|
188
188
|
backend
|
|
189
189
|
.command('invoke <handlerName>')
|
|
190
|
-
.description(
|
|
191
|
-
|
|
190
|
+
.description("Run backend/handlers/<handlerName>.js from the working tree — not the deployed handler — against the app's real " +
|
|
191
|
+
'channels (real db data, real monday calls), and print a trace of every channel call and the ' +
|
|
192
|
+
'result. Nothing is deployed. Exits 1 when the handler fails.')
|
|
193
|
+
.option('--input <json>', 'JSON passed to the handler as ctx.input (what the frontend would send)')
|
|
194
|
+
.option('--code <source>', 'run this source instead of backend/handlers/<handlerName>.js — <handlerName> is then just a label')
|
|
192
195
|
.action(runCommand({
|
|
193
196
|
data: ([handlerName]) => ({ app_name: getCwdAppName(), handler: handlerName }),
|
|
194
197
|
}, (handlerName, opts) => backendInvokeCommand(handlerName, opts)));
|
|
@@ -215,7 +218,9 @@ void main()
|
|
|
215
218
|
if (getOutputMode() === 'human') {
|
|
216
219
|
selfUpdate();
|
|
217
220
|
}
|
|
218
|
-
|
|
221
|
+
// A command can finish normally and still report a non-zero code (e.g.
|
|
222
|
+
// `backend invoke` when the handler failed) via process.exitCode.
|
|
223
|
+
process.exit(process.exitCode ?? 0);
|
|
219
224
|
})
|
|
220
225
|
.catch(async (err) => {
|
|
221
226
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -29,6 +29,8 @@ export interface RegisterOrUpdateOptions {
|
|
|
29
29
|
entry: ManifestEntry;
|
|
30
30
|
gitRemote: string;
|
|
31
31
|
integrations?: string[];
|
|
32
|
+
handlerNames?: string[];
|
|
33
|
+
apiUsages?: Record<string, unknown>;
|
|
32
34
|
description?: string;
|
|
33
35
|
/**
|
|
34
36
|
* The base app a preview is for, sent only on first registration — the link
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8DD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
|
|
@@ -47,11 +47,23 @@ async function claimApp(appName, gitRemote) {
|
|
|
47
47
|
throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
async function registerApp(
|
|
50
|
+
async function registerApp(params) {
|
|
51
|
+
const { appName, token, nextVersion: version, entry, visibility, gitRemote, integrations, handlerNames, apiUsages, description, masterAppName, } = params;
|
|
51
52
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
52
53
|
method: 'POST',
|
|
53
54
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
appName,
|
|
57
|
+
version,
|
|
58
|
+
entry,
|
|
59
|
+
visibility,
|
|
60
|
+
gitRemote,
|
|
61
|
+
integrations,
|
|
62
|
+
handlerNames,
|
|
63
|
+
apiUsages,
|
|
64
|
+
description,
|
|
65
|
+
masterAppName,
|
|
66
|
+
}),
|
|
55
67
|
});
|
|
56
68
|
if (!res.ok) {
|
|
57
69
|
const text = await res.text();
|
|
@@ -59,11 +71,12 @@ async function registerApp(appName, token, version, entry, visibility, gitRemote
|
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
/** Releases a new version onto an app that already exists. */
|
|
62
|
-
async function updateApp(
|
|
74
|
+
async function updateApp(params) {
|
|
75
|
+
const { appName, token, nextVersion: version, entry, gitRemote, integrations, handlerNames, apiUsages, description, } = params;
|
|
63
76
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/update`, {
|
|
64
77
|
method: 'POST',
|
|
65
78
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
66
|
-
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, description }),
|
|
79
|
+
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, handlerNames, apiUsages, description }),
|
|
67
80
|
});
|
|
68
81
|
if (!res.ok) {
|
|
69
82
|
const text = await res.text();
|
|
@@ -77,16 +90,16 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
|
|
|
77
90
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
78
91
|
*/
|
|
79
92
|
async function registerOrUpdateApp(opts) {
|
|
80
|
-
const { appName,
|
|
93
|
+
const { appName, nextVersion } = opts;
|
|
81
94
|
try {
|
|
82
95
|
if (nextVersion === 1) {
|
|
83
96
|
const visibility = await resolveAppVisibility(appName);
|
|
84
97
|
info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
85
|
-
await registerApp(
|
|
98
|
+
await registerApp({ ...opts, visibility });
|
|
86
99
|
}
|
|
87
100
|
else {
|
|
88
101
|
info(`updating "${appName}" → v${nextVersion}`);
|
|
89
|
-
await updateApp(
|
|
102
|
+
await updateApp(opts);
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
105
|
catch (err) {
|
package/dist/index.js
CHANGED
|
@@ -186,15 +186,18 @@ async function main() {
|
|
|
186
186
|
.action(util_runCommand.runCommand({ data: cwdApp }, (opts) => commands_delete.deleteCommand(opts)));
|
|
187
187
|
const backend = program
|
|
188
188
|
.command('backend')
|
|
189
|
-
.description("Inspect the app's backend: validate handler sources or
|
|
189
|
+
.description("Inspect the app's backend: validate handler sources or run a handler from the working tree");
|
|
190
190
|
backend
|
|
191
191
|
.command('validate')
|
|
192
192
|
.description('Validate backend/handlers/*.js exactly as deploy would (syntax, isolate rules, integrations) without uploading')
|
|
193
193
|
.action(util_runCommand.runCommand({ data: cwdApp }, () => commands_backend.backendValidateCommand()));
|
|
194
194
|
backend
|
|
195
195
|
.command('invoke <handlerName>')
|
|
196
|
-
.description(
|
|
197
|
-
|
|
196
|
+
.description("Run backend/handlers/<handlerName>.js from the working tree — not the deployed handler — against the app's real " +
|
|
197
|
+
'channels (real db data, real monday calls), and print a trace of every channel call and the ' +
|
|
198
|
+
'result. Nothing is deployed. Exits 1 when the handler fails.')
|
|
199
|
+
.option('--input <json>', 'JSON passed to the handler as ctx.input (what the frontend would send)')
|
|
200
|
+
.option('--code <source>', 'run this source instead of backend/handlers/<handlerName>.js — <handlerName> is then just a label')
|
|
198
201
|
.action(util_runCommand.runCommand({
|
|
199
202
|
data: ([handlerName]) => ({ app_name: util_appName.getCwdAppName(), handler: handlerName }),
|
|
200
203
|
}, (handlerName, opts) => commands_backend.backendInvokeCommand(handlerName, opts)));
|
|
@@ -221,7 +224,9 @@ void main()
|
|
|
221
224
|
if (util_logger.getOutputMode() === 'human') {
|
|
222
225
|
selfUpdate();
|
|
223
226
|
}
|
|
224
|
-
|
|
227
|
+
// A command can finish normally and still report a non-zero code (e.g.
|
|
228
|
+
// `backend invoke` when the handler failed) via process.exitCode.
|
|
229
|
+
process.exit(process.exitCode ?? 0);
|
|
225
230
|
})
|
|
226
231
|
.catch(async (err) => {
|
|
227
232
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -29,6 +29,8 @@ export interface RegisterOrUpdateOptions {
|
|
|
29
29
|
entry: ManifestEntry;
|
|
30
30
|
gitRemote: string;
|
|
31
31
|
integrations?: string[];
|
|
32
|
+
handlerNames?: string[];
|
|
33
|
+
apiUsages?: Record<string, unknown>;
|
|
32
34
|
description?: string;
|
|
33
35
|
/**
|
|
34
36
|
* The base app a preview is for, sent only on first registration — the link
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8DD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
|
package/dist/util/broker/app.js
CHANGED
|
@@ -49,11 +49,23 @@ async function claimApp(appName, gitRemote) {
|
|
|
49
49
|
throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
async function registerApp(
|
|
52
|
+
async function registerApp(params) {
|
|
53
|
+
const { appName, token, nextVersion: version, entry, visibility, gitRemote, integrations, handlerNames, apiUsages, description, masterAppName, } = params;
|
|
53
54
|
const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
54
55
|
method: 'POST',
|
|
55
56
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
appName,
|
|
59
|
+
version,
|
|
60
|
+
entry,
|
|
61
|
+
visibility,
|
|
62
|
+
gitRemote,
|
|
63
|
+
integrations,
|
|
64
|
+
handlerNames,
|
|
65
|
+
apiUsages,
|
|
66
|
+
description,
|
|
67
|
+
masterAppName,
|
|
68
|
+
}),
|
|
57
69
|
});
|
|
58
70
|
if (!res.ok) {
|
|
59
71
|
const text = await res.text();
|
|
@@ -61,11 +73,12 @@ async function registerApp(appName, token, version, entry, visibility, gitRemote
|
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
/** Releases a new version onto an app that already exists. */
|
|
64
|
-
async function updateApp(
|
|
76
|
+
async function updateApp(params) {
|
|
77
|
+
const { appName, token, nextVersion: version, entry, gitRemote, integrations, handlerNames, apiUsages, description, } = params;
|
|
65
78
|
const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/update`, {
|
|
66
79
|
method: 'POST',
|
|
67
80
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
|
|
68
|
-
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, description }),
|
|
81
|
+
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, handlerNames, apiUsages, description }),
|
|
69
82
|
});
|
|
70
83
|
if (!res.ok) {
|
|
71
84
|
const text = await res.text();
|
|
@@ -79,16 +92,16 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
|
|
|
79
92
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
80
93
|
*/
|
|
81
94
|
async function registerOrUpdateApp(opts) {
|
|
82
|
-
const { appName,
|
|
95
|
+
const { appName, nextVersion } = opts;
|
|
83
96
|
try {
|
|
84
97
|
if (nextVersion === 1) {
|
|
85
98
|
const visibility = await util_promptVisibility.resolveAppVisibility(appName);
|
|
86
99
|
util_logger.info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
87
|
-
await registerApp(
|
|
100
|
+
await registerApp({ ...opts, visibility });
|
|
88
101
|
}
|
|
89
102
|
else {
|
|
90
103
|
util_logger.info(`updating "${appName}" → v${nextVersion}`);
|
|
91
|
-
await updateApp(
|
|
104
|
+
await updateApp(opts);
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
catch (err) {
|
|
@@ -54,3 +54,15 @@ export class ChannelError extends BackendRunnerError {
|
|
|
54
54
|
this.name = 'ChannelError';
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A sandbox run request the runner refuses before any code runs — the code is
|
|
60
|
+
* unusable or the app doesn't exist. Carries a `code` so
|
|
61
|
+
* the CLI can say exactly which, the same way a channel error does.
|
|
62
|
+
*/
|
|
63
|
+
export class SandboxRequestError extends BackendRunnerError {
|
|
64
|
+
constructor(statusCode: number, message: string, public readonly code: string) {
|
|
65
|
+
super(statusCode, message);
|
|
66
|
+
this.name = 'SandboxRequestError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -34,6 +34,9 @@ export type ChannelHandler = (args: unknown[]) => Promise<unknown> | unknown;
|
|
|
34
34
|
*/
|
|
35
35
|
export type ChannelMap = Record<string, ChannelHandler>;
|
|
36
36
|
|
|
37
|
+
/** Which path built the channels — a deployed handler (`invoke`) or source sent to the sandbox. Tags channel telemetry. */
|
|
38
|
+
export type RunMode = 'invoke' | 'sandbox';
|
|
39
|
+
|
|
37
40
|
/**
|
|
38
41
|
* A breakdown of where the isolate wall-clock went, in ms. Lets us see whether a
|
|
39
42
|
* slow invoke is the V8 setup (create + compile) or the guest's own work + its
|
|
@@ -6,13 +6,19 @@ import { makeDbChannel } from './db-channel/db.channel';
|
|
|
6
6
|
import { makeLlmChannel } from './llm-channel/llm.channel';
|
|
7
7
|
import { makeMondayChannel } from './monday-channel/monday.channel';
|
|
8
8
|
import { makeSnowflakeChannel } from './snowflake-channel/snowflake.channel';
|
|
9
|
-
import type { Caller, ChannelMap } from '../backend-runner.types';
|
|
9
|
+
import type { Caller, ChannelMap, RunMode } from '../backend-runner.types';
|
|
10
10
|
import type { RunnerCustomIntegration } from '../runner-data.store';
|
|
11
11
|
|
|
12
12
|
const OAUTH_CHANNELS: Record<string, (caller: Caller) => ChannelMap> = {
|
|
13
13
|
monday: (caller) => makeMondayChannel(caller),
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Every integration with a registered channel. Sandbox runs build all of them —
|
|
18
|
+
* safe because each fetches the caller's token only when a method is called.
|
|
19
|
+
*/
|
|
20
|
+
export const OAUTH_INTEGRATIONS = Object.keys(OAUTH_CHANNELS);
|
|
21
|
+
|
|
16
22
|
/**
|
|
17
23
|
* Builds the complete channel surface for one invocation — the only way out of
|
|
18
24
|
* the isolate.
|
|
@@ -63,7 +69,7 @@ const OAUTH_CHANNELS: Record<string, (caller: Caller) => ChannelMap> = {
|
|
|
63
69
|
* setup) is wrong and retrying won't help; `502` means the upstream failed.
|
|
64
70
|
*
|
|
65
71
|
* Each key emits a unified call counter, duration distribution, and BI event tagged
|
|
66
|
-
* by app, channel, method, and outcome. Uncaught failures also reach BI at the
|
|
72
|
+
* by app, mode (`invoke` or `sandbox`), channel, method, and outcome. Uncaught failures also reach BI at the
|
|
67
73
|
* invocation level — see `runner.service.ts`.
|
|
68
74
|
*/
|
|
69
75
|
export function buildChannels(
|
|
@@ -71,6 +77,7 @@ export function buildChannels(
|
|
|
71
77
|
caller: Caller,
|
|
72
78
|
declaredIntegrations: string[],
|
|
73
79
|
customIntegrations: RunnerCustomIntegration[],
|
|
80
|
+
mode: RunMode,
|
|
74
81
|
): ChannelMap {
|
|
75
82
|
const channels: ChannelMap = {
|
|
76
83
|
...makeDbChannel(appName),
|
|
@@ -91,5 +98,7 @@ export function buildChannels(
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
return Object.fromEntries(
|
|
101
|
+
return Object.fromEntries(
|
|
102
|
+
Object.entries(channels).map(([key, fn]) => [key, withChannelTelemetry(appName, mode, key, fn)]),
|
|
103
|
+
);
|
|
95
104
|
}
|
|
@@ -4,7 +4,7 @@ import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trid
|
|
|
4
4
|
|
|
5
5
|
import { ChannelError } from '../backend-runner.errors';
|
|
6
6
|
import { BI_SOURCE } from '../constants';
|
|
7
|
-
import type { ChannelHandler } from '../backend-runner.types';
|
|
7
|
+
import type { ChannelHandler, RunMode } from '../backend-runner.types';
|
|
8
8
|
|
|
9
9
|
type ChannelOutcome = 'success' | 'failure';
|
|
10
10
|
|
|
@@ -26,6 +26,7 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
|
26
26
|
|
|
27
27
|
function recordChannelCall(
|
|
28
28
|
appName: string,
|
|
29
|
+
mode: RunMode,
|
|
29
30
|
channel: string,
|
|
30
31
|
method: string,
|
|
31
32
|
outcome: ChannelOutcome,
|
|
@@ -33,7 +34,7 @@ function recordChannelCall(
|
|
|
33
34
|
error?: unknown,
|
|
34
35
|
): void {
|
|
35
36
|
const durationMs = performance.now() - startedAt;
|
|
36
|
-
const dimensions = { appName, channel, method };
|
|
37
|
+
const dimensions = { appName, mode, channel, method };
|
|
37
38
|
|
|
38
39
|
metric.increment(BackendRunnerEvents.channel.callMetric, { ...dimensions, outcome });
|
|
39
40
|
metric.distribution(BackendRunnerEvents.channel.durationMetric, durationMs, dimensions);
|
|
@@ -49,7 +50,12 @@ function recordChannelCall(
|
|
|
49
50
|
);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export function withChannelTelemetry(
|
|
53
|
+
export function withChannelTelemetry(
|
|
54
|
+
appName: string,
|
|
55
|
+
mode: RunMode,
|
|
56
|
+
key: string,
|
|
57
|
+
handler: ChannelHandler,
|
|
58
|
+
): ChannelHandler {
|
|
53
59
|
const { channel, method } = parseChannelKey(key);
|
|
54
60
|
|
|
55
61
|
return (args) => {
|
|
@@ -59,20 +65,20 @@ export function withChannelTelemetry(appName: string, key: string, handler: Chan
|
|
|
59
65
|
if (isPromiseLike(result)) {
|
|
60
66
|
return Promise.resolve(result).then(
|
|
61
67
|
(value) => {
|
|
62
|
-
recordChannelCall(appName, channel, method, 'success', startedAt);
|
|
68
|
+
recordChannelCall(appName, mode, channel, method, 'success', startedAt);
|
|
63
69
|
return value;
|
|
64
70
|
},
|
|
65
71
|
(error: unknown) => {
|
|
66
|
-
recordChannelCall(appName, channel, method, 'failure', startedAt, error);
|
|
72
|
+
recordChannelCall(appName, mode, channel, method, 'failure', startedAt, error);
|
|
67
73
|
throw error;
|
|
68
74
|
},
|
|
69
75
|
);
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
recordChannelCall(appName, channel, method, 'success', startedAt);
|
|
78
|
+
recordChannelCall(appName, mode, channel, method, 'success', startedAt);
|
|
73
79
|
return result;
|
|
74
80
|
} catch (error) {
|
|
75
|
-
recordChannelCall(appName, channel, method, 'failure', startedAt, error);
|
|
81
|
+
recordChannelCall(appName, mode, channel, method, 'failure', startedAt, error);
|
|
76
82
|
throw error;
|
|
77
83
|
}
|
|
78
84
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ChannelError } from '../backend-runner.errors';
|
|
2
|
+
import type { ChannelHandler, ChannelMap } from '../backend-runner.types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One channel call as the sandbox reports it back to the author. Raw args are
|
|
6
|
+
* recorded — for `snowflake.query` that is the SQL text and its params, not the
|
|
7
|
+
* bound SQL the channel builds from them.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChannelCallTrace {
|
|
10
|
+
method: string;
|
|
11
|
+
args: unknown[];
|
|
12
|
+
durationMs: number;
|
|
13
|
+
ok: boolean;
|
|
14
|
+
/** Why the call failed — same shape as the run's `error`. A non-`ChannelError` is a 500 with no code. */
|
|
15
|
+
error?: { status: number; code?: string; message: string };
|
|
16
|
+
/** What the channel returned to the handler, as-is — e.g. Snowflake rows or monday's GraphQL `data`. */
|
|
17
|
+
result?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type CallOutcome = Pick<ChannelCallTrace, 'ok' | 'error' | 'result'>;
|
|
21
|
+
|
|
22
|
+
function withChannelTrace(calls: ChannelCallTrace[], method: string, handler: ChannelHandler): ChannelHandler {
|
|
23
|
+
return async (args) => {
|
|
24
|
+
const startedAt = performance.now();
|
|
25
|
+
const record = (outcome: CallOutcome): void => {
|
|
26
|
+
calls.push({ method, args, durationMs: performance.now() - startedAt, ...outcome });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await handler(args);
|
|
31
|
+
record({ ok: true, result });
|
|
32
|
+
return result;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
record({
|
|
35
|
+
ok: false,
|
|
36
|
+
error:
|
|
37
|
+
err instanceof ChannelError
|
|
38
|
+
? { status: err.statusCode, code: err.code, message: err.message }
|
|
39
|
+
: { status: 500, message: err instanceof Error ? err.message : String(err) },
|
|
40
|
+
});
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wraps every channel so each call is appended to `calls` in the order it
|
|
48
|
+
* finished. Sandbox-only: the deployed invoke path never records a trace.
|
|
49
|
+
*/
|
|
50
|
+
export function traceChannels(channels: ChannelMap, calls: ChannelCallTrace[]): ChannelMap {
|
|
51
|
+
return Object.fromEntries(Object.entries(channels).map(([key, fn]) => [key, withChannelTrace(calls, key, fn)]));
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mondaydotcomorg/z2h-cli",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"bin": "./bin/z2h-cli.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@aws-sdk/client-s3": "^3.706.0",
|
|
54
54
|
"@mondaydotcomorg/z2h-dashboard-templates": "^0.4.0",
|
|
55
|
-
"@mondaydotcomorg/z2h-shared-utils": "^0.7.
|
|
55
|
+
"@mondaydotcomorg/z2h-shared-utils": "^0.7.1",
|
|
56
56
|
"acorn": "^8.14.0",
|
|
57
57
|
"acorn-walk": "^8.3.4",
|
|
58
58
|
"chokidar": "^3.6.0",
|