nango 0.72.1 → 0.72.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/index.js +14 -7
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/zeroYaml/compile.unit.cli-test.js +9 -0
- package/dist/zeroYaml/compile.unit.cli-test.js.map +1 -1
- package/dist/zeroYaml/definitions.d.ts +7 -0
- package/dist/zeroYaml/definitions.js +12 -1
- package/dist/zeroYaml/definitions.js.map +1 -1
- package/dist/zeroYaml/deploy.d.ts +8 -2
- package/dist/zeroYaml/deploy.js +263 -245
- package/dist/zeroYaml/deploy.js.map +1 -1
- package/dist/zeroYaml/deploy.unit.cli-test.js +579 -71
- package/dist/zeroYaml/deploy.unit.cli-test.js.map +1 -1
- package/package.json +6 -6
package/dist/zeroYaml/deploy.js
CHANGED
|
@@ -3,19 +3,32 @@ import path from 'node:path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import columnify from 'columnify';
|
|
5
5
|
import promptly from 'promptly';
|
|
6
|
+
import * as z from 'zod';
|
|
6
7
|
import { cliFetch } from '../tls.js';
|
|
7
|
-
import { getCliHeaders,
|
|
8
|
+
import { getCliHeaders, parseSecretKey, printDebug, resolveHostport } from '../utils.js';
|
|
8
9
|
import { Err, Ok } from '../utils/result.js';
|
|
9
10
|
import { Spinner } from '../utils/spinner.js';
|
|
10
11
|
import { NANGO_VERSION } from '../version.js';
|
|
11
12
|
import { tsToJsPath } from './compile.js';
|
|
12
13
|
import { parseIntegrationDefinitions } from './definitions.js';
|
|
13
|
-
import { resolveProjectPath } from './project-path.js';
|
|
14
|
+
import { normalizeProjectRelativePath, resolveProjectPath } from './project-path.js';
|
|
14
15
|
import { ReadableError } from './utils.js';
|
|
15
|
-
export async function deploy({ fullPath, options, environmentName, interactive
|
|
16
|
+
export async function deploy({ fullPath, options, environmentName, file, interactive }) {
|
|
16
17
|
const { env, version, debug } = options;
|
|
17
18
|
const spinnerFactory = new Spinner({ interactive });
|
|
18
|
-
|
|
19
|
+
if (options.sync && options.action) {
|
|
20
|
+
console.log(chalk.red('The --sync and --action options cannot be used together.'));
|
|
21
|
+
return Err('invalid_options');
|
|
22
|
+
}
|
|
23
|
+
const validArguments = validateDeployArguments({ environment: environmentName, file, options });
|
|
24
|
+
if (validArguments.isErr()) {
|
|
25
|
+
console.log(chalk.red(validArguments.error.message));
|
|
26
|
+
return Err(validArguments.error);
|
|
27
|
+
}
|
|
28
|
+
if (options.sync || options.action) {
|
|
29
|
+
console.log(chalk.yellow('--sync and --action are deprecated. Use `nango deploy <environment> <file>` to deploy a single script.'));
|
|
30
|
+
}
|
|
31
|
+
const plan = [];
|
|
19
32
|
const spinnerPackage = spinnerFactory.start('Packaging');
|
|
20
33
|
try {
|
|
21
34
|
const def = await parseIntegrationDefinitions({ fullPath, debug });
|
|
@@ -25,47 +38,57 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
25
38
|
console.log(def.error instanceof ReadableError ? def.error.toText() : chalk.red(def.error.message));
|
|
26
39
|
return Err(def.error);
|
|
27
40
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
optionalSyncName: options.sync,
|
|
32
|
-
optionalActionName: options.action
|
|
33
|
-
});
|
|
34
|
-
if (deploymentKind === 'mixed') {
|
|
41
|
+
if (options.integration &&
|
|
42
|
+
!def.value.integrations.some((integration) => integration.providerConfigKey === options.integration) &&
|
|
43
|
+
!def.value.functions.some((config) => config.integrationId === options.integration)) {
|
|
35
44
|
spinnerPackage.fail();
|
|
36
|
-
console.log(chalk.red(
|
|
45
|
+
console.log(chalk.red(`No local definitions found for integration "${options.integration}".`));
|
|
37
46
|
return Err('no_data');
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
optionalSyncName: options.sync,
|
|
47
|
-
optionalActionName: options.action
|
|
48
|
-
});
|
|
49
|
-
if (postData.isErr()) {
|
|
50
|
-
spinnerPackage.fail();
|
|
51
|
-
console.log(chalk.red(postData.error.message));
|
|
52
|
-
return Err('no_data');
|
|
53
|
-
}
|
|
54
|
-
deployment = { kind: 'legacy', package: postData.value };
|
|
48
|
+
const selections = file !== undefined
|
|
49
|
+
? planFileDeployment({ fullPath, file, parsed: def.value }).map((selection) => [selection])
|
|
50
|
+
: Ok(planDeployment({ parsed: def.value, options }));
|
|
51
|
+
if (selections.isErr()) {
|
|
52
|
+
spinnerPackage.fail();
|
|
53
|
+
console.log(chalk.red(selections.error.message));
|
|
54
|
+
return Err('no_data');
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
for (const selection of selections.value) {
|
|
57
|
+
if (selection.kind === 'legacy') {
|
|
58
|
+
const postData = await createLegacyPackage({
|
|
59
|
+
parsed: def.value,
|
|
60
|
+
fullPath,
|
|
61
|
+
debug,
|
|
62
|
+
version,
|
|
63
|
+
optionalIntegrationId: selection.integrationId,
|
|
64
|
+
optionalSyncName: selection.syncName,
|
|
65
|
+
optionalActionName: selection.actionName
|
|
66
|
+
});
|
|
67
|
+
if (postData.isErr()) {
|
|
68
|
+
spinnerPackage.fail();
|
|
69
|
+
console.log(chalk.red(postData.error.message));
|
|
70
|
+
return Err('no_data');
|
|
71
|
+
}
|
|
72
|
+
plan.push({ kind: 'legacy', package: postData.value });
|
|
61
73
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
else {
|
|
75
|
+
const bundle = await createFunctionsBundle({
|
|
76
|
+
fullPath,
|
|
77
|
+
functionConfigs: def.value.functions,
|
|
78
|
+
reconciliationScope: selection.reconciliationScope
|
|
79
|
+
});
|
|
80
|
+
if (bundle.isErr()) {
|
|
81
|
+
spinnerPackage.fail();
|
|
82
|
+
console.log(chalk.red(bundle.error.message));
|
|
83
|
+
return Err('no_data');
|
|
84
|
+
}
|
|
85
|
+
plan.push({ kind: 'functions', bundle: bundle.value });
|
|
67
86
|
}
|
|
68
|
-
|
|
87
|
+
}
|
|
88
|
+
if (plan.length === 0) {
|
|
89
|
+
spinnerPackage.fail();
|
|
90
|
+
console.log(chalk.red('No functions to deploy.'));
|
|
91
|
+
return Err('no_data');
|
|
69
92
|
}
|
|
70
93
|
spinnerPackage.succeed();
|
|
71
94
|
}
|
|
@@ -74,6 +97,9 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
74
97
|
console.error(chalk.red('Unknown error'), err);
|
|
75
98
|
return Err('failed');
|
|
76
99
|
}
|
|
100
|
+
if (version && plan.some((deployment) => deployment.kind === 'functions')) {
|
|
101
|
+
console.log(chalk.yellow('The --version option only applies to syncs/actions; it has no effect on functions.'));
|
|
102
|
+
}
|
|
77
103
|
// Get the private key before reaching the API
|
|
78
104
|
await parseSecretKey(environmentName, debug);
|
|
79
105
|
const nangoYamlBody = '';
|
|
@@ -81,28 +107,16 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
81
107
|
const hostport = resolveHostport(env);
|
|
82
108
|
// Check remote state
|
|
83
109
|
const spinnerState = spinnerFactory.start(`Acquiring remote state ${chalk.gray(`(${new URL(hostport).origin})`)}`);
|
|
84
|
-
|
|
110
|
+
const confirmations = [];
|
|
85
111
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
hostport,
|
|
89
|
-
body: { ...deployment.package, reconcile: false, debug, sdkVersion }
|
|
90
|
-
});
|
|
91
|
-
if (confirmationRes.isErr()) {
|
|
92
|
-
spinnerState.fail();
|
|
93
|
-
console.log(chalk.red(confirmationRes.error.message));
|
|
94
|
-
return Err(confirmationRes.error);
|
|
95
|
-
}
|
|
96
|
-
confirmation = { kind: 'legacy', value: confirmationRes.value };
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
const confirmationRes = await previewFunctionsDeployment({ hostport, body: deployment.bundle });
|
|
112
|
+
const confirmationResults = await Promise.all(plan.map((deployment) => previewDeployment({ deployment, hostport, debug, sdkVersion })));
|
|
113
|
+
for (const confirmationRes of confirmationResults) {
|
|
100
114
|
if (confirmationRes.isErr()) {
|
|
101
115
|
spinnerState.fail();
|
|
102
116
|
console.log(chalk.red(confirmationRes.error.message));
|
|
103
117
|
return Err(confirmationRes.error);
|
|
104
118
|
}
|
|
105
|
-
|
|
119
|
+
confirmations.push(confirmationRes.value);
|
|
106
120
|
}
|
|
107
121
|
spinnerState.succeed();
|
|
108
122
|
}
|
|
@@ -112,88 +126,151 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
112
126
|
}
|
|
113
127
|
const deploySource = resolveDeploySource();
|
|
114
128
|
const autoconfirm = process.env['NANGO_DEPLOY_AUTO_CONFIRM'] === 'true' || options.autoConfirm;
|
|
115
|
-
const confirmed =
|
|
116
|
-
|
|
117
|
-
:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
});
|
|
129
|
+
const confirmed = await handleConfirmation({
|
|
130
|
+
autoconfirm,
|
|
131
|
+
allowDestructive: options.allowDestructive || false,
|
|
132
|
+
confirmations,
|
|
133
|
+
interactive
|
|
134
|
+
});
|
|
122
135
|
if (confirmed.isErr()) {
|
|
123
136
|
return Err('not_confirmed');
|
|
124
137
|
}
|
|
125
138
|
console.log('');
|
|
126
139
|
// Actual deploy
|
|
127
|
-
const total =
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
const total = plan.reduce((count, deployment) => count +
|
|
141
|
+
(deployment.kind === 'legacy'
|
|
142
|
+
? deployment.package.flowConfigs.length + (deployment.package.onEventScriptsByProvider?.reduce((v, t) => v + t.scripts.length, 0) || 0)
|
|
143
|
+
: deployment.bundle.functions.length), 0);
|
|
130
144
|
const spinnerDeploy = spinnerFactory.start(`Deploying ${total} functions`);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
console.log(chalk.green(deployRes.value));
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
const deployRes = await deployFunctions({ hostport, body: deployment.bundle });
|
|
147
|
-
if (deployRes.isErr()) {
|
|
148
|
-
spinnerDeploy.fail();
|
|
149
|
-
console.log(chalk.red(deployRes.error.message));
|
|
150
|
-
return Err('failed_to_deploy');
|
|
145
|
+
const results = [];
|
|
146
|
+
// Deployments are intentionally not atomic: completed ones are not rolled back on failure.
|
|
147
|
+
for (const [failedIndex, confirmation] of confirmations.entries()) {
|
|
148
|
+
const deployRes = await executeDeployment({ confirmation, hostport, debug, nangoYamlBody, sdkVersion, source: deploySource });
|
|
149
|
+
if (deployRes.isErr()) {
|
|
150
|
+
spinnerDeploy.fail();
|
|
151
|
+
printDeploymentMessage(results);
|
|
152
|
+
console.log(chalk.red(deployRes.error.message));
|
|
153
|
+
console.log(chalk.red(failedIndex > 0 ? 'Deployment partially completed.' : 'Deployment failed.'));
|
|
154
|
+
for (const [index, { kind }] of confirmations.entries()) {
|
|
155
|
+
const status = index < failedIndex ? 'completed' : index === failedIndex ? 'failed' : 'not attempted';
|
|
156
|
+
console.log(`- ${kind === 'legacy' ? 'Syncs/actions/on-events' : 'Functions'}: ${status}`);
|
|
151
157
|
}
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
console.log(chalk.yellow('The failed request may have applied changes. Check remote state before retrying.'));
|
|
159
|
+
return Err('failed_to_deploy');
|
|
154
160
|
}
|
|
155
|
-
|
|
161
|
+
results.push(deployRes.value);
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
spinnerDeploy.succeed('Deployed');
|
|
164
|
+
if (results.every((result) => result.deployed.length + result.deleted.length === 0)) {
|
|
165
|
+
console.log(chalk.gray('No changes were deployed.'));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
printDeploymentMessage(results);
|
|
160
169
|
}
|
|
170
|
+
return Ok(true);
|
|
161
171
|
}
|
|
162
|
-
function
|
|
163
|
-
if (
|
|
164
|
-
|
|
172
|
+
async function previewDeployment({ deployment, hostport, debug, sdkVersion }) {
|
|
173
|
+
if (deployment.kind === 'legacy') {
|
|
174
|
+
const result = await postLegacyConfirmation({ hostport, body: { ...deployment.package, reconcile: false, debug, sdkVersion } });
|
|
175
|
+
return result.map((value) => ({ kind: 'legacy', value, deployment }));
|
|
165
176
|
}
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
const result = await previewFunctionsDeployment({ hostport, body: deployment.bundle });
|
|
178
|
+
return result.map((value) => ({ kind: 'functions', value, deployment }));
|
|
179
|
+
}
|
|
180
|
+
async function executeDeployment({ confirmation, hostport, debug, nangoYamlBody, sdkVersion, source }) {
|
|
181
|
+
try {
|
|
182
|
+
if (confirmation.kind === 'legacy') {
|
|
183
|
+
const { deployment, value } = confirmation;
|
|
184
|
+
const result = await postLegacyDeploy({ hostport, body: { ...deployment.package, reconcile: true, debug, nangoYamlBody, sdkVersion, source } });
|
|
185
|
+
return result.map((deployed) => ({
|
|
186
|
+
deployed: deployed.map(({ providerConfigKey, name, version }) => ({ integrationId: providerConfigKey, name, version })),
|
|
187
|
+
// The legacy API only returns deployed entries.
|
|
188
|
+
// Getting deleted entries from the confirmation response instead.
|
|
189
|
+
deleted: [...value.deletedSyncs, ...value.deletedActions, ...value.deletedOnEventScripts].map(({ providerConfigKey, name }) => ({
|
|
190
|
+
integrationId: providerConfigKey,
|
|
191
|
+
name
|
|
192
|
+
}))
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
const result = await deployFunctions({ hostport, body: confirmation.deployment.bundle });
|
|
196
|
+
return result.map((value) => ({ deployed: [...value.created, ...value.updated], deleted: value.deleted }));
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return Err(new Error(`Unexpected deploy response:\n${err instanceof Error ? err.message : String(err)}`));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function planDeployment({ parsed, options }) {
|
|
203
|
+
const { integration: integrationId, sync: syncName, action: actionName } = options;
|
|
204
|
+
if (syncName || actionName) {
|
|
205
|
+
return [{ kind: 'legacy', integrationId, syncName, actionName }];
|
|
173
206
|
}
|
|
207
|
+
const integrations = integrationId ? parsed.integrations.filter((integration) => integration.providerConfigKey === integrationId) : parsed.integrations;
|
|
208
|
+
const hasLegacyScripts = integrations.some((integration) => integration.syncs.length > 0 || integration.actions.length > 0 || Object.values(integration.onEventScripts).some((scripts) => scripts.length > 0));
|
|
209
|
+
const hasFunctions = parsed.functions.some((config) => !integrationId || config.integrationId === integrationId);
|
|
210
|
+
const selections = [];
|
|
174
211
|
if (hasLegacyScripts) {
|
|
175
|
-
|
|
212
|
+
selections.push({ kind: 'legacy', integrationId });
|
|
176
213
|
}
|
|
177
|
-
|
|
214
|
+
if (hasFunctions) {
|
|
215
|
+
selections.push({ kind: 'functions', reconciliationScope: integrationId ? { kind: 'integration', integrationId } : { kind: 'environment' } });
|
|
216
|
+
}
|
|
217
|
+
return selections;
|
|
178
218
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
219
|
+
const environmentNameSchema = z
|
|
220
|
+
.string()
|
|
221
|
+
.regex(/^[a-z0-9_-]+$/)
|
|
222
|
+
.max(255);
|
|
223
|
+
export function validateDeployArguments({ environment, file, options }) {
|
|
224
|
+
if (!environmentNameSchema.safeParse(environment).success) {
|
|
225
|
+
return Err(new Error(`'${environment}' is not a valid environment name. Usage: nango deploy <environment> [file]`));
|
|
226
|
+
}
|
|
227
|
+
if (file === undefined) {
|
|
228
|
+
return Ok(undefined);
|
|
229
|
+
}
|
|
230
|
+
if (file.trim() === '') {
|
|
231
|
+
return Err(new Error('The file argument is empty.'));
|
|
232
|
+
}
|
|
233
|
+
if (options.sync || options.action || options.integration) {
|
|
234
|
+
return Err(new Error('A file cannot be combined with --sync, --action or --integration.'));
|
|
235
|
+
}
|
|
236
|
+
return Ok(undefined);
|
|
237
|
+
}
|
|
238
|
+
function planFileDeployment({ fullPath, file, parsed }) {
|
|
239
|
+
const tsFile = file.replace(/\.js$/, '.ts');
|
|
240
|
+
const resolved = resolveProjectPath({ projectRoot: fullPath, filePath: path.isAbsolute(tsFile) ? path.relative(fullPath, tsFile) : tsFile });
|
|
241
|
+
if (!resolved) {
|
|
242
|
+
return Err(new Error(`'${file}' must be inside the project folder.`));
|
|
243
|
+
}
|
|
244
|
+
const entryPoint = parsed.entryPoints.find((candidate) => normalizeProjectRelativePath(candidate.filePath) === resolved.relative);
|
|
245
|
+
if (!entryPoint) {
|
|
246
|
+
return Err(new Error(`'${file}' is not a script. Make sure it is imported from index.ts.`));
|
|
247
|
+
}
|
|
248
|
+
const { integrationId, name } = entryPoint;
|
|
249
|
+
switch (entryPoint.type) {
|
|
250
|
+
case 'function':
|
|
251
|
+
return Ok({ kind: 'functions', reconciliationScope: { kind: 'function', integrationId, name } });
|
|
252
|
+
case 'sync':
|
|
253
|
+
return Ok({ kind: 'legacy', integrationId, syncName: name });
|
|
254
|
+
case 'action':
|
|
255
|
+
return Ok({ kind: 'legacy', integrationId, actionName: name });
|
|
256
|
+
case 'on-event':
|
|
257
|
+
return Err(new Error(`'${file}' is an on-event script and can't be deployed on its own. Use \`nango deploy <environment> --integration ${integrationId}\` or deploy the whole project.`));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function createFunctionsBundle({ fullPath, functionConfigs, reconciliationScope }) {
|
|
261
|
+
const selectedConfigs = functionConfigs.filter((config) => {
|
|
262
|
+
switch (reconciliationScope.kind) {
|
|
263
|
+
case 'environment':
|
|
264
|
+
return true;
|
|
265
|
+
case 'function':
|
|
266
|
+
return config.integrationId === reconciliationScope.integrationId && config.name === reconciliationScope.name;
|
|
267
|
+
case 'integration':
|
|
268
|
+
return config.integrationId === reconciliationScope.integrationId;
|
|
194
269
|
}
|
|
270
|
+
});
|
|
271
|
+
if (reconciliationScope.kind === 'function' && selectedConfigs.length === 0) {
|
|
272
|
+
return Err(new Error(`No compiled function found for "${reconciliationScope.name}"`));
|
|
195
273
|
}
|
|
196
|
-
const selectedConfigs = optionalIntegrationId ? functionConfigs.filter((config) => config.integrationId === optionalIntegrationId) : functionConfigs;
|
|
197
274
|
const functions = [];
|
|
198
275
|
for (const config of selectedConfigs) {
|
|
199
276
|
const { filePath, ...artifact } = config;
|
|
@@ -213,17 +290,14 @@ async function createFunctionsBundle({ fullPath, optionalIntegrationId }) {
|
|
|
213
290
|
}
|
|
214
291
|
functions.push({ ...artifact, fileBody });
|
|
215
292
|
}
|
|
216
|
-
return Ok({
|
|
217
|
-
reconciliationScope: optionalIntegrationId ? { kind: 'integration', integrationId: optionalIntegrationId } : { kind: 'environment' },
|
|
218
|
-
functions
|
|
219
|
-
});
|
|
293
|
+
return Ok({ reconciliationScope, functions });
|
|
220
294
|
}
|
|
221
295
|
/**
|
|
222
296
|
* Maps NangoYamlParsed (which is a list of integrations and its function definitions) into the shape expected by the API,
|
|
223
297
|
* while also loading the content of the related script files.
|
|
224
298
|
* It also supports filtering by integration, sync or action name for single deploys.
|
|
225
299
|
*/
|
|
226
|
-
async function
|
|
300
|
+
async function createLegacyPackage({ parsed, fullPath, debug, version = '', optionalIntegrationId, optionalSyncName, optionalActionName }) {
|
|
227
301
|
printDebug('Packaging', debug);
|
|
228
302
|
const postData = [];
|
|
229
303
|
const onEventScriptsByProvider = optionalActionName || optionalSyncName ? undefined : []; // only load on-event scripts if we're not deploying a single sync or action
|
|
@@ -433,19 +507,11 @@ async function postLegacyDeploy({ hostport, body }) {
|
|
|
433
507
|
if ('error' in json) {
|
|
434
508
|
return Err(new Error(`Error deploying:\n${json.error.message} ${chalk.gray(`(${json.error.code})`)}`));
|
|
435
509
|
}
|
|
436
|
-
|
|
437
|
-
return Ok(`Successfully removed the syncs/actions.`);
|
|
438
|
-
}
|
|
439
|
-
const nameAndVersions = json.map((result) => `${result.name}@v${result.version}`);
|
|
440
|
-
return Ok(`Successfully deployed the functions: \r\n${nameAndVersions
|
|
441
|
-
.map((row) => {
|
|
442
|
-
return `- ${row}`;
|
|
443
|
-
})
|
|
444
|
-
.join('\r\n')}`);
|
|
510
|
+
return Ok(json);
|
|
445
511
|
}
|
|
446
512
|
catch (err) {
|
|
447
513
|
const errorMessage = getFetchError(err);
|
|
448
|
-
return Err(new Error(`Error deploying
|
|
514
|
+
return Err(new Error(`Error deploying:\n${errorMessage}`));
|
|
449
515
|
}
|
|
450
516
|
}
|
|
451
517
|
function formatFunctionsDeploymentError(error) {
|
|
@@ -499,21 +565,32 @@ async function deployFunctions({ hostport, body }) {
|
|
|
499
565
|
return Err(new Error(`Error deploying functions:\n${getFetchError(err)}`));
|
|
500
566
|
}
|
|
501
567
|
}
|
|
502
|
-
function
|
|
503
|
-
const deployed =
|
|
504
|
-
|
|
505
|
-
|
|
568
|
+
function printDeploymentMessage(results) {
|
|
569
|
+
const deployed = results.flatMap((result) => result.deployed);
|
|
570
|
+
const deleted = results.flatMap((result) => result.deleted);
|
|
571
|
+
const messages = [];
|
|
572
|
+
if (deployed.length > 0) {
|
|
573
|
+
messages.push(`Successfully deployed: \r\n${deployed.map((func) => `- ${func.integrationId} → ${func.name}${func.version ? `@v${func.version}` : ''}`).join('\r\n')}`);
|
|
574
|
+
}
|
|
575
|
+
if (deleted.length > 0) {
|
|
576
|
+
messages.push(`Successfully removed: \r\n${deleted.map((func) => `- ${func.integrationId} → ${func.name}`).join('\r\n')}`);
|
|
577
|
+
}
|
|
578
|
+
if (messages.length > 0) {
|
|
579
|
+
console.log(chalk.green(messages.join('\r\n')));
|
|
506
580
|
}
|
|
507
|
-
return `Successfully deployed the functions: \r\n${deployed.map((func) => `- ${func.integrationId} → ${func.name}`).join('\r\n')}`;
|
|
508
581
|
}
|
|
509
|
-
|
|
510
|
-
* Handle state, display plan and eventually ask for confirmation if destructive
|
|
511
|
-
*/
|
|
512
|
-
async function handleLegacyConfirmation({ autoconfirm, allowDestructive, confirmation }) {
|
|
582
|
+
function renderLegacyConfirmation(confirmation) {
|
|
513
583
|
const { newSyncs, updatedSyncs, deletedSyncs, newActions, updatedActions, deletedActions, newOnEventScripts, updatedOnEventScripts, deletedOnEventScripts, deletedModels } = confirmation;
|
|
584
|
+
const scriptSummaries = [
|
|
585
|
+
{ name: 'Actions', newItems: newActions, updatedItems: updatedActions, deleteItems: deletedActions },
|
|
586
|
+
{ name: 'OnEvents', newItems: newOnEventScripts, updatedItems: updatedOnEventScripts, deleteItems: deletedOnEventScripts }
|
|
587
|
+
];
|
|
588
|
+
const summaries = [{ name: 'Syncs', newItems: newSyncs, updatedItems: updatedSyncs, deleteItems: deletedSyncs }, ...scriptSummaries];
|
|
589
|
+
if (deletedModels.length === 0 &&
|
|
590
|
+
summaries.every(({ newItems, updatedItems, deleteItems }) => newItems.length + updatedItems.length + deleteItems.length === 0)) {
|
|
591
|
+
return { rows: [], requiresConfirmation: false };
|
|
592
|
+
}
|
|
514
593
|
let deletedSyncsConnectionsCount = 0;
|
|
515
|
-
console.log('');
|
|
516
|
-
console.log('', chalk.underline('Nango will perform this plan:'));
|
|
517
594
|
// Syncs
|
|
518
595
|
if (newSyncs.length > 0 || deletedSyncs.length > 0) {
|
|
519
596
|
console.log('');
|
|
@@ -544,101 +621,32 @@ async function handleLegacyConfirmation({ autoconfirm, allowDestructive, confirm
|
|
|
544
621
|
});
|
|
545
622
|
console.log(columns);
|
|
546
623
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
console.log(shortSummaryMessage({ name: 'Actions', newItems: newActions, deleteItems: deletedActions }));
|
|
551
|
-
for (const action of newActions) {
|
|
552
|
-
console.log(` ${chalk.green('+')} ${action.providerConfigKey} → ${action.name}`);
|
|
553
|
-
}
|
|
554
|
-
for (const action of deletedActions) {
|
|
555
|
-
console.log(` ${chalk.red('-')} ${action.providerConfigKey} → ${action.name}`);
|
|
624
|
+
for (const { name, newItems, deleteItems } of scriptSummaries) {
|
|
625
|
+
if (newItems.length === 0 && deleteItems.length === 0) {
|
|
626
|
+
continue;
|
|
556
627
|
}
|
|
557
|
-
}
|
|
558
|
-
// OnEvents
|
|
559
|
-
if (newOnEventScripts.length > 0 || deletedOnEventScripts.length > 0) {
|
|
560
628
|
console.log('');
|
|
561
|
-
console.log(shortSummaryMessage({ name
|
|
562
|
-
for (const
|
|
563
|
-
console.log(` ${chalk.green('+')} ${
|
|
629
|
+
console.log(shortSummaryMessage({ name, newItems, deleteItems }));
|
|
630
|
+
for (const item of newItems) {
|
|
631
|
+
console.log(` ${chalk.green('+')} ${item.providerConfigKey} → ${item.name}`);
|
|
564
632
|
}
|
|
565
|
-
for (const
|
|
566
|
-
console.log(` ${chalk.red('-')} ${
|
|
633
|
+
for (const item of deleteItems) {
|
|
634
|
+
console.log(` ${chalk.red('-')} ${item.providerConfigKey} → ${item.name}`);
|
|
567
635
|
}
|
|
568
636
|
}
|
|
569
637
|
if (deletedModels.length > 0) {
|
|
570
638
|
console.log(chalk.red(`The following models have been removed: ${deletedModels.join(', ')}. `));
|
|
571
639
|
console.log(chalk.red("WARNING: Renaming a model is the equivalent of deleting the old model and creating a new one. Records from the old model won't be transferred to the new model. Consider running a full sync to transfer records."));
|
|
572
640
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
newOnEventScripts.length <= 0 &&
|
|
578
|
-
deletedOnEventScripts.length <= 0 &&
|
|
579
|
-
deletedModels.length <= 0) {
|
|
580
|
-
console.log('');
|
|
581
|
-
console.log(chalk.gray.italic(' Only updates'));
|
|
582
|
-
}
|
|
583
|
-
console.log('');
|
|
584
|
-
console.log('', chalk.underline('Summary'));
|
|
585
|
-
const columns = columnify([
|
|
586
|
-
summaryMessageColumns({ name: 'Syncs', newItems: newSyncs, updatedItems: updatedSyncs, deleteItems: deletedSyncs }),
|
|
587
|
-
summaryMessageColumns({ name: 'Actions', newItems: newActions, updatedItems: updatedActions, deleteItems: deletedActions }),
|
|
588
|
-
summaryMessageColumns({ name: 'OnEvents', newItems: newOnEventScripts, updatedItems: updatedOnEventScripts, deleteItems: deletedOnEventScripts })
|
|
589
|
-
], {
|
|
590
|
-
showHeaders: false,
|
|
591
|
-
minWidth: 18,
|
|
592
|
-
config: {
|
|
593
|
-
name: {
|
|
594
|
-
dataTransform: (a) => {
|
|
595
|
-
return `\u2063\u2063${a}`;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
console.log(columns);
|
|
601
|
-
console.log('');
|
|
602
|
-
const shouldConfirmDestructive = deletedSyncsConnectionsCount > 0 || deletedModels.length > 0;
|
|
603
|
-
if (!shouldConfirmDestructive) {
|
|
604
|
-
console.log(chalk.grey('No sync deleted with active connections, proceeding without confirmation'));
|
|
605
|
-
return Ok(true);
|
|
606
|
-
}
|
|
607
|
-
if (autoconfirm && !shouldConfirmDestructive) {
|
|
608
|
-
console.log(chalk.yellow('autoconfirm flag is on, proceeding without confirmation'));
|
|
609
|
-
return Ok(true);
|
|
610
|
-
}
|
|
611
|
-
else if (autoconfirm && shouldConfirmDestructive && allowDestructive) {
|
|
612
|
-
console.log(chalk.yellow('allowDestructive flag is on, proceeding without confirmation'));
|
|
613
|
-
return Ok(true);
|
|
614
|
-
}
|
|
615
|
-
// Can't do anything here
|
|
616
|
-
if (isCI) {
|
|
617
|
-
console.log(chalk.red(`Syncs/Actions were not deployed. Confirm the deploy by passing the --auto-confirm flag${shouldConfirmDestructive ? ' and --allow-destructive flag' : ''}. Exiting`));
|
|
618
|
-
return Err('is_ci');
|
|
619
|
-
}
|
|
620
|
-
try {
|
|
621
|
-
const wait = await promptly.confirm(`Are you sure you want to continue y/n?`);
|
|
622
|
-
if (!wait) {
|
|
623
|
-
console.log(chalk.yellow('Deployed aborted. Exiting'));
|
|
624
|
-
return Err('not_confirmed');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
console.log('');
|
|
629
|
-
console.log(chalk.yellow('Deployed aborted. Exiting'));
|
|
630
|
-
return Err('not_confirmed');
|
|
631
|
-
}
|
|
632
|
-
return Ok(true);
|
|
641
|
+
return {
|
|
642
|
+
rows: summaries.map(summaryMessageColumns),
|
|
643
|
+
requiresConfirmation: deletedSyncsConnectionsCount > 0 || deletedModels.length > 0
|
|
644
|
+
};
|
|
633
645
|
}
|
|
634
|
-
|
|
646
|
+
function renderFunctionsConfirmation(confirmation) {
|
|
635
647
|
const { created, updated, deleted } = confirmation;
|
|
636
|
-
console.log('');
|
|
637
|
-
console.log('', chalk.underline('Nango will perform this plan:'));
|
|
638
648
|
if (created.length + updated.length + deleted.length === 0) {
|
|
639
|
-
|
|
640
|
-
console.log(chalk.gray.italic(' No changes'));
|
|
641
|
-
return Ok(true);
|
|
649
|
+
return { rows: [], requiresConfirmation: false };
|
|
642
650
|
}
|
|
643
651
|
else if (created.length > 0 || deleted.length > 0) {
|
|
644
652
|
console.log('');
|
|
@@ -650,32 +658,42 @@ async function handleFunctionsConfirmation({ autoconfirm, allowDestructive, conf
|
|
|
650
658
|
console.log(` ${chalk.red('-')} ${func.integrationId} → ${func.name}`);
|
|
651
659
|
}
|
|
652
660
|
}
|
|
653
|
-
|
|
661
|
+
return {
|
|
662
|
+
rows: [summaryMessageColumns({ name: 'Functions', newItems: created, updatedItems: updated, deleteItems: deleted })],
|
|
663
|
+
requiresConfirmation: deleted.length > 0
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async function handleConfirmation({ autoconfirm, allowDestructive, confirmations, interactive }) {
|
|
667
|
+
console.log('');
|
|
668
|
+
console.log('', chalk.underline('Nango will perform this plan:'));
|
|
669
|
+
const summaries = confirmations.map((confirmation) => confirmation.kind === 'legacy' ? renderLegacyConfirmation(confirmation.value) : renderFunctionsConfirmation(confirmation.value));
|
|
670
|
+
const rows = summaries.flatMap((summary) => summary.rows);
|
|
671
|
+
if (rows.length > 0) {
|
|
672
|
+
console.log('');
|
|
673
|
+
console.log('', chalk.underline('Summary'));
|
|
674
|
+
console.log(columnify(rows, {
|
|
675
|
+
showHeaders: false,
|
|
676
|
+
minWidth: 18,
|
|
677
|
+
config: { name: { dataTransform: (value) => `\u2063\u2063${value}` } }
|
|
678
|
+
}));
|
|
654
679
|
console.log('');
|
|
655
|
-
console.log(chalk.gray.italic(' Only updates'));
|
|
656
680
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
dataTransform: (value) => `\u2063\u2063${value}`
|
|
665
|
-
}
|
|
681
|
+
else {
|
|
682
|
+
console.log('');
|
|
683
|
+
console.log(chalk.gray.italic(' No changes'));
|
|
684
|
+
}
|
|
685
|
+
if (!summaries.some((summary) => summary.requiresConfirmation)) {
|
|
686
|
+
if (rows.length > 0) {
|
|
687
|
+
console.log(chalk.grey('No destructive changes, proceeding without confirmation'));
|
|
666
688
|
}
|
|
667
|
-
}));
|
|
668
|
-
console.log('');
|
|
669
|
-
if (deleted.length === 0) {
|
|
670
|
-
console.log(chalk.grey('No functions deleted, proceeding without confirmation'));
|
|
671
689
|
return Ok(true);
|
|
672
690
|
}
|
|
673
691
|
if (autoconfirm && allowDestructive) {
|
|
674
692
|
console.log(chalk.yellow('allowDestructive flag is on, proceeding without confirmation'));
|
|
675
693
|
return Ok(true);
|
|
676
694
|
}
|
|
677
|
-
if (
|
|
678
|
-
console.log(chalk.red('
|
|
695
|
+
if (!interactive) {
|
|
696
|
+
console.log(chalk.red('Nothing was deployed. Confirm the deploy by passing the --auto-confirm and --allow-destructive flags. Exiting'));
|
|
679
697
|
return Err('is_ci');
|
|
680
698
|
}
|
|
681
699
|
try {
|