carto-cli 0.1.0-rc.1
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/.nvmrc +1 -0
- package/ARCHITECTURE.md +497 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +15 -0
- package/MAP_JSON.md +516 -0
- package/README.md +1595 -0
- package/WORKFLOW_JSON.md +623 -0
- package/dist/api.js +489 -0
- package/dist/auth-oauth.js +485 -0
- package/dist/auth-server.js +432 -0
- package/dist/browser.js +30 -0
- package/dist/colors.js +45 -0
- package/dist/commands/activity.js +427 -0
- package/dist/commands/admin.js +177 -0
- package/dist/commands/ai.js +489 -0
- package/dist/commands/auth.js +652 -0
- package/dist/commands/connections.js +412 -0
- package/dist/commands/credentials.js +606 -0
- package/dist/commands/imports.js +234 -0
- package/dist/commands/maps.js +1022 -0
- package/dist/commands/org.js +195 -0
- package/dist/commands/sql.js +326 -0
- package/dist/commands/users.js +459 -0
- package/dist/commands/workflows.js +1025 -0
- package/dist/config.js +320 -0
- package/dist/download.js +108 -0
- package/dist/help.js +285 -0
- package/dist/http.js +139 -0
- package/dist/index.js +1133 -0
- package/dist/logo.js +11 -0
- package/dist/prompt.js +67 -0
- package/dist/schedule-parser.js +287 -0
- package/jest.config.ts +43 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.workflowsList = workflowsList;
|
|
4
|
+
exports.workflowsDelete = workflowsDelete;
|
|
5
|
+
exports.workflowsGet = workflowsGet;
|
|
6
|
+
exports.workflowsCreate = workflowsCreate;
|
|
7
|
+
exports.workflowsUpdate = workflowsUpdate;
|
|
8
|
+
exports.workflowsExtensionsInstall = workflowsExtensionsInstall;
|
|
9
|
+
exports.workflowScheduleAdd = workflowScheduleAdd;
|
|
10
|
+
exports.workflowScheduleUpdate = workflowScheduleUpdate;
|
|
11
|
+
exports.workflowsCopy = workflowsCopy;
|
|
12
|
+
exports.workflowScheduleRemove = workflowScheduleRemove;
|
|
13
|
+
const api_1 = require("../api");
|
|
14
|
+
const colors_1 = require("../colors");
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const os_1 = require("os");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const schedule_parser_1 = require("../schedule-parser");
|
|
20
|
+
const prompt_1 = require("../prompt");
|
|
21
|
+
async function workflowsList(options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
22
|
+
try {
|
|
23
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
24
|
+
let result;
|
|
25
|
+
if (options.all) {
|
|
26
|
+
// Fetch all pages automatically
|
|
27
|
+
const baseParams = {};
|
|
28
|
+
if (options.orderBy)
|
|
29
|
+
baseParams['order_by'] = options.orderBy;
|
|
30
|
+
if (options.orderDirection)
|
|
31
|
+
baseParams['order_direction'] = options.orderDirection;
|
|
32
|
+
if (options.search)
|
|
33
|
+
baseParams['search'] = options.search;
|
|
34
|
+
if (options.privacy)
|
|
35
|
+
baseParams['privacy'] = options.privacy;
|
|
36
|
+
if (options.tags)
|
|
37
|
+
baseParams['tags'] = JSON.stringify(options.tags);
|
|
38
|
+
const paginatedResult = await client.getAllPaginated('/v3/organization-workflows', baseParams);
|
|
39
|
+
result = {
|
|
40
|
+
data: paginatedResult.data,
|
|
41
|
+
meta: {
|
|
42
|
+
page: {
|
|
43
|
+
total: paginatedResult.total,
|
|
44
|
+
current: 1,
|
|
45
|
+
pageSize: paginatedResult.total,
|
|
46
|
+
totalPages: 1
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Fetch single page
|
|
53
|
+
const params = new URLSearchParams();
|
|
54
|
+
if (options.orderBy)
|
|
55
|
+
params.append('order_by', options.orderBy);
|
|
56
|
+
if (options.orderDirection)
|
|
57
|
+
params.append('order_direction', options.orderDirection);
|
|
58
|
+
if (options.pageSize)
|
|
59
|
+
params.append('page_size', options.pageSize);
|
|
60
|
+
if (options.page)
|
|
61
|
+
params.append('page', options.page);
|
|
62
|
+
if (options.search)
|
|
63
|
+
params.append('search', options.search);
|
|
64
|
+
if (options.privacy)
|
|
65
|
+
params.append('privacy', options.privacy);
|
|
66
|
+
if (options.tags)
|
|
67
|
+
params.append('tags', JSON.stringify(options.tags));
|
|
68
|
+
const queryString = params.toString();
|
|
69
|
+
const path = `/v3/organization-workflows${queryString ? '?' + queryString : ''}`;
|
|
70
|
+
result = await client.get(path);
|
|
71
|
+
}
|
|
72
|
+
if (jsonOutput) {
|
|
73
|
+
console.log(JSON.stringify(result));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const total = result.meta?.page?.total || result.data?.length || 0;
|
|
77
|
+
// Display header with search query if present
|
|
78
|
+
const header = options.search
|
|
79
|
+
? `\n${total} workflows found for '${options.search}':\n`
|
|
80
|
+
: `\nWorkflows (${total}):\n`;
|
|
81
|
+
console.log((0, colors_1.bold)(header));
|
|
82
|
+
const workflows = result.data || [];
|
|
83
|
+
workflows.forEach((workflow) => {
|
|
84
|
+
console.log((0, colors_1.bold)('Workflow ID: ') + workflow.id);
|
|
85
|
+
console.log(' Name: ' + (workflow.title || workflow.name));
|
|
86
|
+
console.log(' Privacy: ' + (workflow.privacy || 'N/A'));
|
|
87
|
+
if (workflow.tags && workflow.tags.length > 0) {
|
|
88
|
+
console.log(' Tags: ' + workflow.tags.join(', '));
|
|
89
|
+
}
|
|
90
|
+
console.log(' Created: ' + new Date(workflow.createdAt || workflow.created_at).toLocaleString());
|
|
91
|
+
console.log(' Updated: ' + new Date(workflow.updatedAt || workflow.updated_at).toLocaleString());
|
|
92
|
+
console.log('');
|
|
93
|
+
});
|
|
94
|
+
if (options.all) {
|
|
95
|
+
console.log((0, colors_1.dim)(`Fetched all ${total} workflows`));
|
|
96
|
+
}
|
|
97
|
+
else if (result.meta?.page) {
|
|
98
|
+
const page = result.meta.page;
|
|
99
|
+
console.log((0, colors_1.dim)(`Page ${page.current || 1} of ${page.totalPages || 1} (use --all to fetch all pages)`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (jsonOutput) {
|
|
105
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
109
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
110
|
+
console.log('Please run: carto auth login');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log((0, colors_1.error)('✗ Failed to list workflows: ' + err.message));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function workflowsDelete(workflowId, token, baseUrl, jsonOutput, debug = false, profile, yes) {
|
|
120
|
+
try {
|
|
121
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
122
|
+
// Skip confirmation if --yes or --json flag is set
|
|
123
|
+
const skipConfirmation = yes || jsonOutput;
|
|
124
|
+
if (!skipConfirmation) {
|
|
125
|
+
// Fetch workflow details to show in confirmation prompt
|
|
126
|
+
let workflowDetails;
|
|
127
|
+
try {
|
|
128
|
+
workflowDetails = await client.get(`/v3/workflows/${workflowId}`);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// If we can't fetch details, proceed with generic message
|
|
132
|
+
console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete this workflow.'));
|
|
133
|
+
console.log((0, colors_1.warning)(` Workflow ID: ${workflowId}`));
|
|
134
|
+
}
|
|
135
|
+
if (workflowDetails) {
|
|
136
|
+
const title = workflowDetails.title || workflowDetails.config?.title || '(Untitled)';
|
|
137
|
+
console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete:'));
|
|
138
|
+
console.log(` Workflow: "${title}"`);
|
|
139
|
+
console.log(` ID: ${workflowId}`);
|
|
140
|
+
if (workflowDetails.privacy) {
|
|
141
|
+
console.log(` Privacy: ${workflowDetails.privacy}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
console.log('');
|
|
145
|
+
const confirmed = await (0, prompt_1.promptForConfirmation)("Type 'delete' to confirm: ", 'delete');
|
|
146
|
+
if (!confirmed) {
|
|
147
|
+
console.log('\nDeletion cancelled');
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
console.log('');
|
|
151
|
+
}
|
|
152
|
+
await client.delete(`/v3/organization-workflows/${workflowId}`);
|
|
153
|
+
if (jsonOutput) {
|
|
154
|
+
console.log(JSON.stringify({ success: true, message: 'Workflow deleted successfully', id: workflowId }));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log((0, colors_1.success)(`✓ Workflow ${workflowId} deleted successfully`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (jsonOutput) {
|
|
162
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.log((0, colors_1.error)('✗ Failed to delete workflow: ' + err.message));
|
|
166
|
+
}
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function workflowsGet(workflowId, options, token, baseUrl, jsonOutput, debug = false) {
|
|
171
|
+
try {
|
|
172
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug);
|
|
173
|
+
// Build query parameters
|
|
174
|
+
const params = new URLSearchParams();
|
|
175
|
+
if (options.client)
|
|
176
|
+
params.append('client', options.client);
|
|
177
|
+
const queryString = params.toString();
|
|
178
|
+
const path = `/v3/workflows/${workflowId}${queryString ? '?' + queryString : ''}`;
|
|
179
|
+
const result = await client.get(path);
|
|
180
|
+
if (jsonOutput) {
|
|
181
|
+
console.log(JSON.stringify(result));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log((0, colors_1.bold)('\nWorkflow Details:\n'));
|
|
185
|
+
console.log((0, colors_1.bold)('ID: ') + result.id);
|
|
186
|
+
const title = result.title || result.config?.title || result.name;
|
|
187
|
+
console.log(' Title: ' + title);
|
|
188
|
+
const description = result.description || result.config?.description;
|
|
189
|
+
if (description)
|
|
190
|
+
console.log(' Description: ' + description);
|
|
191
|
+
console.log(' Privacy: ' + (result.privacy || 'N/A'));
|
|
192
|
+
if (result.tags && result.tags.length > 0) {
|
|
193
|
+
console.log(' Tags: ' + result.tags.join(', '));
|
|
194
|
+
}
|
|
195
|
+
if (result.connectionProvider || result.providerId) {
|
|
196
|
+
console.log(' Connection Provider: ' + (result.connectionProvider || result.providerId));
|
|
197
|
+
}
|
|
198
|
+
if (result.schemaVersion)
|
|
199
|
+
console.log(' Schema Version: ' + result.schemaVersion);
|
|
200
|
+
console.log(' Created: ' + new Date(result.createdAt || result.created_at).toLocaleString());
|
|
201
|
+
console.log(' Updated: ' + new Date(result.updatedAt || result.updated_at).toLocaleString());
|
|
202
|
+
if (result.config) {
|
|
203
|
+
console.log('\n' + (0, colors_1.bold)('Configuration:'));
|
|
204
|
+
console.log(' Nodes: ' + (result.config.nodes?.length || 0));
|
|
205
|
+
console.log(' Edges: ' + (result.config.edges?.length || 0));
|
|
206
|
+
if (result.config.procedure !== undefined)
|
|
207
|
+
console.log(' Procedure: ' + result.config.procedure);
|
|
208
|
+
if (result.config.useCache !== undefined)
|
|
209
|
+
console.log(' Use Cache: ' + result.config.useCache);
|
|
210
|
+
}
|
|
211
|
+
if (result.executionInfo) {
|
|
212
|
+
console.log('\n' + (0, colors_1.bold)('Execution Info:'));
|
|
213
|
+
if (result.executionInfo.status)
|
|
214
|
+
console.log(' Status: ' + result.executionInfo.status);
|
|
215
|
+
if (result.executionInfo.startedAt)
|
|
216
|
+
console.log(' Started: ' + new Date(result.executionInfo.startedAt).toLocaleString());
|
|
217
|
+
if (result.executionInfo.finishedAt)
|
|
218
|
+
console.log(' Finished: ' + new Date(result.executionInfo.finishedAt).toLocaleString());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (jsonOutput) {
|
|
224
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log((0, colors_1.error)('✗ Failed to get workflow: ' + err.message));
|
|
228
|
+
}
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get JSON from various input sources (arg, file, stdin)
|
|
234
|
+
*/
|
|
235
|
+
async function getJsonFromInput(jsonString, options) {
|
|
236
|
+
let json = jsonString;
|
|
237
|
+
// Priority 1: --file flag
|
|
238
|
+
if (options.file) {
|
|
239
|
+
try {
|
|
240
|
+
json = (0, fs_1.readFileSync)(options.file, 'utf-8').trim();
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
throw new Error(`Failed to read file: ${err.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Priority 2: stdin
|
|
247
|
+
if (!json && !process.stdin.isTTY) {
|
|
248
|
+
const chunks = [];
|
|
249
|
+
for await (const chunk of process.stdin) {
|
|
250
|
+
chunks.push(chunk);
|
|
251
|
+
}
|
|
252
|
+
json = Buffer.concat(chunks).toString('utf-8').trim();
|
|
253
|
+
}
|
|
254
|
+
// Parse JSON if we have it
|
|
255
|
+
if (!json) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(json);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
throw new Error(`Invalid JSON: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function workflowsCreate(jsonString, options, token, baseUrl, jsonOutput, debug = false) {
|
|
266
|
+
try {
|
|
267
|
+
if (!jsonOutput) {
|
|
268
|
+
console.error((0, colors_1.dim)('⚠️ Note: workflows create is experimental and subject to change\n'));
|
|
269
|
+
}
|
|
270
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug);
|
|
271
|
+
// Get workflow config from various sources
|
|
272
|
+
const body = await getJsonFromInput(jsonString, options);
|
|
273
|
+
if (!body) {
|
|
274
|
+
throw new Error('No workflow config provided. Use: carto workflows create <json> or --file <path> or pipe via stdin');
|
|
275
|
+
}
|
|
276
|
+
// Ensure required fields
|
|
277
|
+
if (!body.connectionId) {
|
|
278
|
+
throw new Error('connectionId is required');
|
|
279
|
+
}
|
|
280
|
+
// Set default client if not provided
|
|
281
|
+
if (!body.client) {
|
|
282
|
+
body.client = 'carto-cli';
|
|
283
|
+
}
|
|
284
|
+
const result = await client.post('/v3/workflows', body);
|
|
285
|
+
if (jsonOutput) {
|
|
286
|
+
console.log(JSON.stringify(result));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.log((0, colors_1.success)(`✓ Workflow created successfully`));
|
|
290
|
+
console.log((0, colors_1.bold)('\nWorkflow ID: ') + result.id);
|
|
291
|
+
if (result.title)
|
|
292
|
+
console.log('Title: ' + result.title);
|
|
293
|
+
if (result.connectionProvider)
|
|
294
|
+
console.log('Connection Provider: ' + result.connectionProvider);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
if (jsonOutput) {
|
|
299
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.log((0, colors_1.error)('✗ Failed to create workflow: ' + err.message));
|
|
303
|
+
}
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function workflowsUpdate(workflowId, jsonString, options, token, baseUrl, jsonOutput, debug = false) {
|
|
308
|
+
try {
|
|
309
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug);
|
|
310
|
+
// Get update config from various sources
|
|
311
|
+
const body = await getJsonFromInput(jsonString, options);
|
|
312
|
+
if (!body) {
|
|
313
|
+
throw new Error('No update config provided. Use: carto workflows update <id> <json> or --file <path> or pipe via stdin');
|
|
314
|
+
}
|
|
315
|
+
// Ensure required fields
|
|
316
|
+
if (!body.config) {
|
|
317
|
+
throw new Error('config object is required');
|
|
318
|
+
}
|
|
319
|
+
// Set default client if not provided
|
|
320
|
+
if (!body.client) {
|
|
321
|
+
body.client = 'carto-cli';
|
|
322
|
+
}
|
|
323
|
+
const result = await client.patch(`/v3/workflows/${workflowId}`, body);
|
|
324
|
+
if (jsonOutput) {
|
|
325
|
+
console.log(JSON.stringify(result));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
console.log((0, colors_1.success)(`✓ Workflow ${workflowId} updated successfully`));
|
|
329
|
+
if (result.title)
|
|
330
|
+
console.log('Title: ' + result.title);
|
|
331
|
+
if (result.updated_at)
|
|
332
|
+
console.log('Updated: ' + new Date(result.updated_at).toLocaleString());
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (jsonOutput) {
|
|
337
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
console.log((0, colors_1.error)('✗ Failed to update workflow: ' + err.message));
|
|
341
|
+
}
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Install a workflow extension from a local zip file
|
|
347
|
+
*/
|
|
348
|
+
async function workflowsExtensionsInstall(extensionFile, connectionName, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
349
|
+
let tempDir;
|
|
350
|
+
try {
|
|
351
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
352
|
+
if (!jsonOutput) {
|
|
353
|
+
console.log((0, colors_1.dim)('Installing extension...'));
|
|
354
|
+
}
|
|
355
|
+
// Create temp directory
|
|
356
|
+
tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'carto-extension-'));
|
|
357
|
+
// Extract zip file
|
|
358
|
+
try {
|
|
359
|
+
(0, child_process_1.execSync)(`unzip -q -o "${extensionFile}" -d "${tempDir}"`, { stdio: 'pipe' });
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
throw new Error(`Failed to extract extension file: ${err.message}`);
|
|
363
|
+
}
|
|
364
|
+
// Read metadata.json
|
|
365
|
+
const metadataPath = (0, path_1.join)(tempDir, 'metadata.json');
|
|
366
|
+
let metadata;
|
|
367
|
+
try {
|
|
368
|
+
const metadataContent = (0, fs_1.readFileSync)(metadataPath, 'utf-8');
|
|
369
|
+
metadata = JSON.parse(metadataContent);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
throw new Error(`Failed to read metadata.json: ${err.message}`);
|
|
373
|
+
}
|
|
374
|
+
if (!metadata.provider) {
|
|
375
|
+
throw new Error('Extension metadata missing "provider" field');
|
|
376
|
+
}
|
|
377
|
+
if (!jsonOutput) {
|
|
378
|
+
console.log((0, colors_1.dim)(`Extension: ${metadata.name || 'unknown'} (provider: ${metadata.provider})`));
|
|
379
|
+
console.log((0, colors_1.dim)(`Finding connection: ${connectionName}...`));
|
|
380
|
+
}
|
|
381
|
+
// Find connection by name
|
|
382
|
+
const connectionsResult = await client.getAllPaginated('/v3/connections', {});
|
|
383
|
+
const connectionSummary = connectionsResult.data.find((conn) => conn.name === connectionName);
|
|
384
|
+
if (!connectionSummary) {
|
|
385
|
+
throw new Error(`Connection '${connectionName}' not found`);
|
|
386
|
+
}
|
|
387
|
+
// Get full connection details (list endpoint doesn't include extensionsLocations)
|
|
388
|
+
const connection = await client.get(`/v3/connections/${connectionSummary.id}`);
|
|
389
|
+
// Validate provider compatibility
|
|
390
|
+
if (metadata.provider !== connection.provider_id) {
|
|
391
|
+
throw new Error(`Extension provider '${metadata.provider}' does not match connection provider '${connection.provider_id}'`);
|
|
392
|
+
}
|
|
393
|
+
// Get extensionsLocations FQN
|
|
394
|
+
if (!connection.extensionsLocations || connection.extensionsLocations.length === 0) {
|
|
395
|
+
throw new Error(`Connection '${connectionName}' has no extensionsLocations configured`);
|
|
396
|
+
}
|
|
397
|
+
const fqn = connection.extensionsLocations[0].fqn;
|
|
398
|
+
if (!jsonOutput) {
|
|
399
|
+
console.log((0, colors_1.dim)(`Connection found: ${connection.id} (${connection.provider_id})`));
|
|
400
|
+
console.log((0, colors_1.dim)(`Extensions location: ${fqn}`));
|
|
401
|
+
console.log((0, colors_1.dim)('Processing SQL...'));
|
|
402
|
+
}
|
|
403
|
+
// Read and process extension.sql
|
|
404
|
+
const sqlPath = (0, path_1.join)(tempDir, 'extension.sql');
|
|
405
|
+
let sql;
|
|
406
|
+
try {
|
|
407
|
+
sql = (0, fs_1.readFileSync)(sqlPath, 'utf-8');
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
throw new Error(`Failed to read extension.sql: ${err.message}`);
|
|
411
|
+
}
|
|
412
|
+
// Replace placeholder
|
|
413
|
+
sql = sql.replace(/@@workflows_temp@@/g, fqn);
|
|
414
|
+
if (!jsonOutput) {
|
|
415
|
+
console.log((0, colors_1.dim)('Executing extension SQL...'));
|
|
416
|
+
}
|
|
417
|
+
// Execute SQL job
|
|
418
|
+
const jobBody = {
|
|
419
|
+
query: sql,
|
|
420
|
+
queryParameters: {}
|
|
421
|
+
};
|
|
422
|
+
const jobResult = await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/job`, jobBody);
|
|
423
|
+
const jobId = jobResult.externalId || jobResult.jobId;
|
|
424
|
+
if (!jsonOutput) {
|
|
425
|
+
console.log((0, colors_1.dim)(`Job created: ${jobId}`));
|
|
426
|
+
console.log((0, colors_1.dim)('Waiting for completion...'));
|
|
427
|
+
}
|
|
428
|
+
// Poll for job completion
|
|
429
|
+
const maxRetries = 600; // 10 minutes
|
|
430
|
+
let retries = 0;
|
|
431
|
+
let jobStatus;
|
|
432
|
+
while (retries < maxRetries) {
|
|
433
|
+
jobStatus = await client.get(`/v3/sql/${encodeURIComponent(connectionName)}/job/${encodeURIComponent(jobId)}`);
|
|
434
|
+
if (!jsonOutput && retries > 0 && retries % 5 === 0) {
|
|
435
|
+
console.log((0, colors_1.dim)(`Status: ${jobStatus.status}...`));
|
|
436
|
+
}
|
|
437
|
+
if (jobStatus.status === 'success' || jobStatus.status === 'failed' || jobStatus.status === 'failure') {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
441
|
+
retries++;
|
|
442
|
+
}
|
|
443
|
+
if (retries >= maxRetries) {
|
|
444
|
+
throw new Error('Job timeout: Installation took longer than 10 minutes');
|
|
445
|
+
}
|
|
446
|
+
// Check final status
|
|
447
|
+
if (jobStatus.status === 'failed' || jobStatus.status === 'failure') {
|
|
448
|
+
const errorMsg = jobStatus.error?.msg || jobStatus.error?.message || jobStatus.error || 'Unknown error';
|
|
449
|
+
throw new Error(`Installation failed: ${errorMsg}`);
|
|
450
|
+
}
|
|
451
|
+
if (jsonOutput) {
|
|
452
|
+
console.log(JSON.stringify({
|
|
453
|
+
success: true,
|
|
454
|
+
extensionName: metadata.name,
|
|
455
|
+
provider: metadata.provider,
|
|
456
|
+
connection: connectionName,
|
|
457
|
+
jobId: jobId
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
console.log((0, colors_1.success)(`\n✓ Extension '${metadata.name}' installed successfully`));
|
|
462
|
+
console.log((0, colors_1.dim)(`Job ID: ${jobId}`));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
if (jsonOutput) {
|
|
467
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
471
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
472
|
+
console.log('Please run: carto auth login');
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
console.log((0, colors_1.error)('✗ ' + err.message));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
// Clean up temp directory
|
|
482
|
+
if (tempDir) {
|
|
483
|
+
try {
|
|
484
|
+
(0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
// Ignore cleanup errors
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Add a schedule to a workflow
|
|
494
|
+
*/
|
|
495
|
+
async function workflowScheduleAdd(workflowId, options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
496
|
+
let tempDir;
|
|
497
|
+
try {
|
|
498
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
499
|
+
if (!options.expression) {
|
|
500
|
+
throw new Error('Schedule expression is required (use --expression)');
|
|
501
|
+
}
|
|
502
|
+
if (!jsonOutput) {
|
|
503
|
+
console.log((0, colors_1.dim)('Fetching workflow details...'));
|
|
504
|
+
}
|
|
505
|
+
// 1. Get workflow details
|
|
506
|
+
const workflow = await client.get(`/v3/workflows/${workflowId}`);
|
|
507
|
+
if (!workflow.connectionId) {
|
|
508
|
+
throw new Error('Workflow does not have a connectionId');
|
|
509
|
+
}
|
|
510
|
+
if (!jsonOutput) {
|
|
511
|
+
console.log((0, colors_1.dim)(`Workflow: ${workflow.title || workflow.config?.title || workflowId}`));
|
|
512
|
+
console.log((0, colors_1.dim)('Fetching connection details...'));
|
|
513
|
+
}
|
|
514
|
+
// 2. Get connection details
|
|
515
|
+
const connection = await client.get(`/v3/connections/${workflow.connectionId}`);
|
|
516
|
+
const connectionName = connection.name;
|
|
517
|
+
const region = connection.region || 'US';
|
|
518
|
+
const provider = connection.provider_id || connection.providerId;
|
|
519
|
+
if (!jsonOutput) {
|
|
520
|
+
console.log((0, colors_1.dim)(`Connection: ${connectionName} (${provider}, region: ${region})`));
|
|
521
|
+
console.log((0, colors_1.dim)('Generating SQL from workflow...'));
|
|
522
|
+
}
|
|
523
|
+
// 3. Generate SQL from workflow config
|
|
524
|
+
tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'carto-workflow-'));
|
|
525
|
+
const diagramPath = (0, path_1.join)(tempDir, 'workflow.json');
|
|
526
|
+
(0, fs_1.writeFileSync)(diagramPath, JSON.stringify(workflow.config, null, 2), 'utf-8');
|
|
527
|
+
let sql;
|
|
528
|
+
try {
|
|
529
|
+
sql = (0, child_process_1.execSync)(`workflows-engine diagram to-sql "${diagramPath}"`, {
|
|
530
|
+
encoding: 'utf-8',
|
|
531
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
throw new Error(`Failed to generate SQL: ${err.message}`);
|
|
536
|
+
}
|
|
537
|
+
if (!jsonOutput) {
|
|
538
|
+
console.log((0, colors_1.dim)('Creating schedule in data warehouse...'));
|
|
539
|
+
}
|
|
540
|
+
// 4. Create schedule via SQL API
|
|
541
|
+
const scheduleBody = {
|
|
542
|
+
schedule: options.expression,
|
|
543
|
+
query: sql,
|
|
544
|
+
region: region,
|
|
545
|
+
name: `workflows.${workflowId}`,
|
|
546
|
+
client: 'workflows-schedule'
|
|
547
|
+
};
|
|
548
|
+
await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/schedule`, scheduleBody);
|
|
549
|
+
if (!jsonOutput) {
|
|
550
|
+
console.log((0, colors_1.dim)('Parsing schedule expression...'));
|
|
551
|
+
}
|
|
552
|
+
// 5. Parse schedule expression to metadata
|
|
553
|
+
const scheduleMetadata = (0, schedule_parser_1.parseScheduleExpression)(options.expression, provider);
|
|
554
|
+
if (!jsonOutput) {
|
|
555
|
+
console.log((0, colors_1.dim)('Updating workflow metadata...'));
|
|
556
|
+
}
|
|
557
|
+
// 6. Update workflow with schedule metadata in config
|
|
558
|
+
const updatedConfig = {
|
|
559
|
+
...workflow.config,
|
|
560
|
+
schedule: {
|
|
561
|
+
time: scheduleMetadata.time,
|
|
562
|
+
frequency: scheduleMetadata.frequency,
|
|
563
|
+
daysOfWeek: scheduleMetadata.daysOfWeek,
|
|
564
|
+
expression: options.expression,
|
|
565
|
+
daysOfMonth: scheduleMetadata.daysOfMonth,
|
|
566
|
+
repeatInterval: scheduleMetadata.repeatInterval
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
const updateBody = {
|
|
570
|
+
config: updatedConfig,
|
|
571
|
+
client: 'carto-cli'
|
|
572
|
+
};
|
|
573
|
+
await client.patch(`/v3/workflows/${workflowId}`, updateBody);
|
|
574
|
+
if (jsonOutput) {
|
|
575
|
+
console.log(JSON.stringify({
|
|
576
|
+
success: true,
|
|
577
|
+
workflowId,
|
|
578
|
+
schedule: options.expression,
|
|
579
|
+
metadata: scheduleMetadata
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
console.log((0, colors_1.success)(`\n✓ Schedule added successfully`));
|
|
584
|
+
console.log((0, colors_1.bold)('Workflow: ') + workflowId);
|
|
585
|
+
console.log((0, colors_1.bold)('Schedule: ') + options.expression);
|
|
586
|
+
console.log((0, colors_1.bold)('Time: ') + scheduleMetadata.time);
|
|
587
|
+
if (scheduleMetadata.daysOfWeek.length < 7) {
|
|
588
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
589
|
+
const days = scheduleMetadata.daysOfWeek.map(d => dayNames[d]).join(', ');
|
|
590
|
+
console.log((0, colors_1.bold)('Days: ') + days);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
if (jsonOutput) {
|
|
596
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
600
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
601
|
+
console.log('Please run: carto auth login');
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
console.log((0, colors_1.error)('✗ Failed to add schedule: ' + err.message));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
if (tempDir) {
|
|
611
|
+
try {
|
|
612
|
+
(0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
// Ignore cleanup errors
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Update a workflow schedule
|
|
622
|
+
*/
|
|
623
|
+
async function workflowScheduleUpdate(workflowId, options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
624
|
+
let tempDir;
|
|
625
|
+
try {
|
|
626
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
627
|
+
if (!options.expression) {
|
|
628
|
+
throw new Error('Schedule expression is required (use --expression)');
|
|
629
|
+
}
|
|
630
|
+
if (!jsonOutput) {
|
|
631
|
+
console.log((0, colors_1.dim)('Fetching workflow details...'));
|
|
632
|
+
}
|
|
633
|
+
// 1. Get workflow details
|
|
634
|
+
const workflow = await client.get(`/v3/workflows/${workflowId}`);
|
|
635
|
+
if (!workflow.connectionId) {
|
|
636
|
+
throw new Error('Workflow does not have a connectionId');
|
|
637
|
+
}
|
|
638
|
+
if (!jsonOutput) {
|
|
639
|
+
console.log((0, colors_1.dim)(`Workflow: ${workflow.title || workflow.config?.title || workflowId}`));
|
|
640
|
+
console.log((0, colors_1.dim)('Fetching connection details...'));
|
|
641
|
+
}
|
|
642
|
+
// 2. Get connection details
|
|
643
|
+
const connection = await client.get(`/v3/connections/${workflow.connectionId}`);
|
|
644
|
+
const connectionName = connection.name;
|
|
645
|
+
const region = connection.region || 'US';
|
|
646
|
+
const provider = connection.provider_id || connection.providerId;
|
|
647
|
+
if (!jsonOutput) {
|
|
648
|
+
console.log((0, colors_1.dim)(`Connection: ${connectionName} (${provider}, region: ${region})`));
|
|
649
|
+
console.log((0, colors_1.dim)('Generating SQL from workflow...'));
|
|
650
|
+
}
|
|
651
|
+
// 3. Generate SQL from workflow config
|
|
652
|
+
tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'carto-workflow-'));
|
|
653
|
+
const diagramPath = (0, path_1.join)(tempDir, 'workflow.json');
|
|
654
|
+
(0, fs_1.writeFileSync)(diagramPath, JSON.stringify(workflow.config, null, 2), 'utf-8');
|
|
655
|
+
let sql;
|
|
656
|
+
try {
|
|
657
|
+
sql = (0, child_process_1.execSync)(`workflows-engine diagram to-sql "${diagramPath}"`, {
|
|
658
|
+
encoding: 'utf-8',
|
|
659
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
throw new Error(`Failed to generate SQL: ${err.message}`);
|
|
664
|
+
}
|
|
665
|
+
if (!jsonOutput) {
|
|
666
|
+
console.log((0, colors_1.dim)('Updating schedule in data warehouse...'));
|
|
667
|
+
}
|
|
668
|
+
// 4. Update schedule via SQL API
|
|
669
|
+
const schedulePath = `/v3/sql/${encodeURIComponent(connectionName)}/schedule/workflows.${workflowId}?region=${region}`;
|
|
670
|
+
const scheduleBody = {
|
|
671
|
+
schedule: options.expression,
|
|
672
|
+
query: sql,
|
|
673
|
+
name: `workflows.${workflowId}`,
|
|
674
|
+
region: region,
|
|
675
|
+
client: 'workflows-schedule'
|
|
676
|
+
};
|
|
677
|
+
await client.patch(schedulePath, scheduleBody);
|
|
678
|
+
if (!jsonOutput) {
|
|
679
|
+
console.log((0, colors_1.dim)('Parsing schedule expression...'));
|
|
680
|
+
}
|
|
681
|
+
// 5. Parse schedule expression to metadata
|
|
682
|
+
const scheduleMetadata = (0, schedule_parser_1.parseScheduleExpression)(options.expression, provider);
|
|
683
|
+
if (!jsonOutput) {
|
|
684
|
+
console.log((0, colors_1.dim)('Updating workflow metadata...'));
|
|
685
|
+
}
|
|
686
|
+
// 6. Update workflow with new schedule metadata in config
|
|
687
|
+
const updatedConfig = {
|
|
688
|
+
...workflow.config,
|
|
689
|
+
schedule: {
|
|
690
|
+
time: scheduleMetadata.time,
|
|
691
|
+
frequency: scheduleMetadata.frequency,
|
|
692
|
+
daysOfWeek: scheduleMetadata.daysOfWeek,
|
|
693
|
+
expression: options.expression,
|
|
694
|
+
daysOfMonth: scheduleMetadata.daysOfMonth,
|
|
695
|
+
repeatInterval: scheduleMetadata.repeatInterval
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
const updateBody = {
|
|
699
|
+
config: updatedConfig,
|
|
700
|
+
client: 'carto-cli'
|
|
701
|
+
};
|
|
702
|
+
await client.patch(`/v3/workflows/${workflowId}`, updateBody);
|
|
703
|
+
if (jsonOutput) {
|
|
704
|
+
console.log(JSON.stringify({
|
|
705
|
+
success: true,
|
|
706
|
+
workflowId,
|
|
707
|
+
schedule: options.expression,
|
|
708
|
+
metadata: scheduleMetadata
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
console.log((0, colors_1.success)(`\n✓ Schedule updated successfully`));
|
|
713
|
+
console.log((0, colors_1.bold)('Workflow: ') + workflowId);
|
|
714
|
+
console.log((0, colors_1.bold)('New schedule: ') + options.expression);
|
|
715
|
+
console.log((0, colors_1.bold)('Time: ') + scheduleMetadata.time);
|
|
716
|
+
if (scheduleMetadata.daysOfWeek.length < 7) {
|
|
717
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
718
|
+
const days = scheduleMetadata.daysOfWeek.map(d => dayNames[d]).join(', ');
|
|
719
|
+
console.log((0, colors_1.bold)('Days: ') + days);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
if (jsonOutput) {
|
|
725
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
729
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
730
|
+
console.log('Please run: carto auth login');
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
console.log((0, colors_1.error)('✗ Failed to update schedule: ' + err.message));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
if (tempDir) {
|
|
740
|
+
try {
|
|
741
|
+
(0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
// Ignore cleanup errors
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Validate that a table source is accessible in the destination connection
|
|
751
|
+
* Uses dry-run SQL queries with WHERE 1=0 to test without transferring data
|
|
752
|
+
*/
|
|
753
|
+
async function validateTableSource(client, connectionName, tableFqn, jsonOutput) {
|
|
754
|
+
try {
|
|
755
|
+
// Build dry-run query
|
|
756
|
+
const validationSQL = `SELECT * FROM ${tableFqn} WHERE 1=0 LIMIT 1`;
|
|
757
|
+
if (!jsonOutput) {
|
|
758
|
+
const displayTable = tableFqn.length > 60
|
|
759
|
+
? tableFqn.substring(0, 57) + '...'
|
|
760
|
+
: tableFqn;
|
|
761
|
+
console.log((0, colors_1.dim)(` ✓ ${displayTable}`));
|
|
762
|
+
}
|
|
763
|
+
// Execute validation query via SQL API
|
|
764
|
+
await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/query`, {
|
|
765
|
+
q: validationSQL,
|
|
766
|
+
queryParameters: {}
|
|
767
|
+
});
|
|
768
|
+
// Success - table is accessible
|
|
769
|
+
return { accessible: true };
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
// Extract meaningful error message from API response
|
|
773
|
+
let errorMsg = 'Unknown error';
|
|
774
|
+
if (err.message) {
|
|
775
|
+
errorMsg = err.message;
|
|
776
|
+
// Try to extract detail from API error JSON
|
|
777
|
+
try {
|
|
778
|
+
const match = err.message.match(/"detail":"([^"]+)"/);
|
|
779
|
+
if (match) {
|
|
780
|
+
errorMsg = match[1];
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
// Try to extract just the main error message
|
|
784
|
+
const errorMatch = err.message.match(/"error":"([^"]+)"/);
|
|
785
|
+
if (errorMatch) {
|
|
786
|
+
errorMsg = errorMatch[1];
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
// Keep original error message if parsing fails
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
accessible: false,
|
|
796
|
+
error: errorMsg
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Copy a workflow from one profile to another
|
|
802
|
+
*/
|
|
803
|
+
async function workflowsCopy(workflowId, connectionName, sourceProfile, destProfile, options, token, baseUrl, jsonOutput, debug = false) {
|
|
804
|
+
try {
|
|
805
|
+
// Step 1: Create API clients for source and destination
|
|
806
|
+
const sourceClient = await api_1.ApiClient.create(token, baseUrl, debug, sourceProfile);
|
|
807
|
+
const destClient = await api_1.ApiClient.create(token, baseUrl, debug, destProfile);
|
|
808
|
+
if (!jsonOutput) {
|
|
809
|
+
console.log((0, colors_1.dim)(`Copying workflow ${workflowId} from ${sourceProfile} to ${destProfile}...`));
|
|
810
|
+
}
|
|
811
|
+
// Step 2: Fetch source workflow
|
|
812
|
+
if (!jsonOutput) {
|
|
813
|
+
console.log((0, colors_1.dim)('→ Fetching source workflow...'));
|
|
814
|
+
}
|
|
815
|
+
const workflow = await sourceClient.get(`/v3/workflows/${workflowId}`);
|
|
816
|
+
if (!workflow.connectionId) {
|
|
817
|
+
throw new Error('Source workflow does not have a connectionId');
|
|
818
|
+
}
|
|
819
|
+
// Step 3: Get source connection details
|
|
820
|
+
if (!jsonOutput) {
|
|
821
|
+
console.log((0, colors_1.dim)('→ Fetching source connection details...'));
|
|
822
|
+
}
|
|
823
|
+
const sourceConnection = await sourceClient.get(`/v3/connections/${workflow.connectionId}`);
|
|
824
|
+
const sourceConnectionName = sourceConnection.name;
|
|
825
|
+
if (!jsonOutput) {
|
|
826
|
+
console.log((0, colors_1.dim)(`→ Source connection: ${sourceConnectionName}`));
|
|
827
|
+
}
|
|
828
|
+
// Step 4: Resolve destination connection
|
|
829
|
+
const destConnectionName = connectionName || sourceConnectionName;
|
|
830
|
+
if (!jsonOutput) {
|
|
831
|
+
if (connectionName) {
|
|
832
|
+
console.log((0, colors_1.dim)(`→ Using destination connection: ${destConnectionName} (manual)`));
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
console.log((0, colors_1.dim)(`→ Using destination connection: ${destConnectionName} (auto-mapped by name)`));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Fetch destination connections
|
|
839
|
+
if (!jsonOutput) {
|
|
840
|
+
console.log((0, colors_1.dim)('→ Fetching connections from destination...'));
|
|
841
|
+
}
|
|
842
|
+
const destConnections = await destClient.getAllPaginated('/v3/connections', {});
|
|
843
|
+
const destConnection = destConnections.data.find((c) => c.name === destConnectionName);
|
|
844
|
+
if (!destConnection) {
|
|
845
|
+
const errorMsg = [
|
|
846
|
+
`Connection "${destConnectionName}" not found in destination organization.`,
|
|
847
|
+
'',
|
|
848
|
+
'Solutions:',
|
|
849
|
+
' 1. Create the connection in destination organization',
|
|
850
|
+
' 2. Use --connection to specify a different destination connection name'
|
|
851
|
+
].join('\n');
|
|
852
|
+
throw new Error(errorMsg);
|
|
853
|
+
}
|
|
854
|
+
const destConnectionId = destConnection.id;
|
|
855
|
+
// Step 5: Validate source tables (unless --skip-source-validation)
|
|
856
|
+
if (!options.skipSourceValidation) {
|
|
857
|
+
// Extract source tables from workflow config nodes
|
|
858
|
+
const sourceTables = [];
|
|
859
|
+
if (workflow.config && workflow.config.nodes && Array.isArray(workflow.config.nodes)) {
|
|
860
|
+
for (const node of workflow.config.nodes) {
|
|
861
|
+
// Look for nodes that reference source tables
|
|
862
|
+
// Common patterns: node.data.source, node.data.tableName, node.data.table
|
|
863
|
+
if (node.data) {
|
|
864
|
+
const source = node.data.source || node.data.tableName || node.data.table;
|
|
865
|
+
if (source && typeof source === 'string' && source.includes('.')) {
|
|
866
|
+
// Only include if it looks like a FQN (has dots) and not a temp table
|
|
867
|
+
if (!source.startsWith('temp_') && !source.startsWith('tmp_')) {
|
|
868
|
+
if (!sourceTables.includes(source)) {
|
|
869
|
+
sourceTables.push(source);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (sourceTables.length > 0) {
|
|
877
|
+
if (!jsonOutput) {
|
|
878
|
+
console.log((0, colors_1.dim)(`→ Validating ${sourceTables.length} source table(s)...`));
|
|
879
|
+
}
|
|
880
|
+
const inaccessibleTables = [];
|
|
881
|
+
for (const tableFqn of sourceTables) {
|
|
882
|
+
const result = await validateTableSource(destClient, destConnectionName, tableFqn, jsonOutput);
|
|
883
|
+
if (!result.accessible) {
|
|
884
|
+
inaccessibleTables.push({
|
|
885
|
+
table: tableFqn,
|
|
886
|
+
error: result.error
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (inaccessibleTables.length > 0) {
|
|
891
|
+
const errorMsg = [
|
|
892
|
+
'Source validation failed - workflow references inaccessible tables:',
|
|
893
|
+
...inaccessibleTables.map(t => ` • ${t.table}\n Error: ${t.error}`),
|
|
894
|
+
'',
|
|
895
|
+
'Solutions:',
|
|
896
|
+
' 1. Grant access to these tables in destination connection',
|
|
897
|
+
' 2. Ensure tables exist in destination data warehouse',
|
|
898
|
+
' 3. Use --skip-source-validation to create workflow anyway (may fail at runtime)'
|
|
899
|
+
].join('\n');
|
|
900
|
+
throw new Error(errorMsg);
|
|
901
|
+
}
|
|
902
|
+
if (!jsonOutput) {
|
|
903
|
+
console.log((0, colors_1.dim)(`→ All source table(s) validated successfully`));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
if (!jsonOutput) {
|
|
908
|
+
console.log((0, colors_1.dim)('→ No source tables found to validate'));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Step 6: Create workflow in destination
|
|
913
|
+
const newTitle = options.title || workflow.title || workflow.config?.title;
|
|
914
|
+
if (!jsonOutput) {
|
|
915
|
+
console.log((0, colors_1.dim)(`→ Creating workflow "${newTitle}" in destination...`));
|
|
916
|
+
}
|
|
917
|
+
// Update config with new title if provided
|
|
918
|
+
const updatedConfig = { ...workflow.config };
|
|
919
|
+
if (options.title) {
|
|
920
|
+
updatedConfig.title = options.title;
|
|
921
|
+
}
|
|
922
|
+
const workflowPayload = {
|
|
923
|
+
connectionId: destConnectionId,
|
|
924
|
+
config: updatedConfig,
|
|
925
|
+
client: 'carto-cli'
|
|
926
|
+
};
|
|
927
|
+
const newWorkflow = await destClient.post('/v3/workflows', workflowPayload);
|
|
928
|
+
const newWorkflowId = newWorkflow.id;
|
|
929
|
+
if (!newWorkflowId) {
|
|
930
|
+
throw new Error('Failed to create workflow in destination');
|
|
931
|
+
}
|
|
932
|
+
if (jsonOutput) {
|
|
933
|
+
console.log(JSON.stringify({
|
|
934
|
+
success: true,
|
|
935
|
+
sourceWorkflowId: workflowId,
|
|
936
|
+
newWorkflowId: newWorkflowId,
|
|
937
|
+
sourceProfile: sourceProfile,
|
|
938
|
+
destProfile: destProfile,
|
|
939
|
+
sourceConnection: sourceConnectionName,
|
|
940
|
+
destConnection: destConnectionName,
|
|
941
|
+
}));
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
console.log((0, colors_1.success)(`\n✓ Workflow copied successfully!`));
|
|
945
|
+
console.log((0, colors_1.bold)('New Workflow ID: ') + newWorkflowId);
|
|
946
|
+
console.log((0, colors_1.dim)(`Source connection: ${sourceConnectionName} → Destination connection: ${destConnectionName}`));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
catch (err) {
|
|
950
|
+
if (jsonOutput) {
|
|
951
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
console.log((0, colors_1.error)('✗ Failed to copy workflow: ' + err.message));
|
|
955
|
+
}
|
|
956
|
+
process.exit(1);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Remove a workflow schedule
|
|
961
|
+
*/
|
|
962
|
+
async function workflowScheduleRemove(workflowId, options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
963
|
+
try {
|
|
964
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
965
|
+
if (!jsonOutput) {
|
|
966
|
+
console.log((0, colors_1.dim)('Fetching workflow details...'));
|
|
967
|
+
}
|
|
968
|
+
// 1. Get workflow details
|
|
969
|
+
const workflow = await client.get(`/v3/workflows/${workflowId}`);
|
|
970
|
+
if (!workflow.connectionId) {
|
|
971
|
+
throw new Error('Workflow does not have a connectionId');
|
|
972
|
+
}
|
|
973
|
+
if (!jsonOutput) {
|
|
974
|
+
console.log((0, colors_1.dim)(`Workflow: ${workflow.title || workflow.config?.title || workflowId}`));
|
|
975
|
+
console.log((0, colors_1.dim)('Fetching connection details...'));
|
|
976
|
+
}
|
|
977
|
+
// 2. Get connection details
|
|
978
|
+
const connection = await client.get(`/v3/connections/${workflow.connectionId}`);
|
|
979
|
+
const connectionName = connection.name;
|
|
980
|
+
const region = connection.region || 'US';
|
|
981
|
+
if (!jsonOutput) {
|
|
982
|
+
console.log((0, colors_1.dim)(`Connection: ${connectionName} (region: ${region})`));
|
|
983
|
+
console.log((0, colors_1.dim)('Removing schedule from data warehouse...'));
|
|
984
|
+
}
|
|
985
|
+
// 3. Delete schedule via SQL API
|
|
986
|
+
const schedulePath = `/v3/sql/${encodeURIComponent(connectionName)}/schedule/workflows.${workflowId}?region=${region}&client=workflows-schedule`;
|
|
987
|
+
await client.delete(schedulePath);
|
|
988
|
+
if (!jsonOutput) {
|
|
989
|
+
console.log((0, colors_1.dim)('Updating workflow metadata...'));
|
|
990
|
+
}
|
|
991
|
+
// 4. Update workflow to remove schedule metadata from config
|
|
992
|
+
const updatedConfig = { ...workflow.config };
|
|
993
|
+
delete updatedConfig.schedule;
|
|
994
|
+
const updateBody = {
|
|
995
|
+
config: updatedConfig,
|
|
996
|
+
client: 'carto-cli'
|
|
997
|
+
};
|
|
998
|
+
await client.patch(`/v3/workflows/${workflowId}`, updateBody);
|
|
999
|
+
if (jsonOutput) {
|
|
1000
|
+
console.log(JSON.stringify({
|
|
1001
|
+
success: true,
|
|
1002
|
+
workflowId
|
|
1003
|
+
}));
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
console.log((0, colors_1.success)(`\n✓ Schedule removed successfully`));
|
|
1007
|
+
console.log((0, colors_1.bold)('Workflow: ') + workflowId);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
catch (err) {
|
|
1011
|
+
if (jsonOutput) {
|
|
1012
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
1016
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
1017
|
+
console.log('Please run: carto auth login');
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
console.log((0, colors_1.error)('✗ Failed to remove schedule: ' + err.message));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
}
|