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,1022 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapsList = mapsList;
|
|
4
|
+
exports.mapsGet = mapsGet;
|
|
5
|
+
exports.mapsDelete = mapsDelete;
|
|
6
|
+
exports.mapsCopy = mapsCopy;
|
|
7
|
+
exports.mapsCreate = mapsCreate;
|
|
8
|
+
exports.mapsUpdate = mapsUpdate;
|
|
9
|
+
exports.mapsClone = mapsClone;
|
|
10
|
+
const api_1 = require("../api");
|
|
11
|
+
const colors_1 = require("../colors");
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const prompt_1 = require("../prompt");
|
|
14
|
+
async function mapsList(options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
15
|
+
try {
|
|
16
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
17
|
+
let result;
|
|
18
|
+
let userId;
|
|
19
|
+
// If --mine flag is set, get current user ID
|
|
20
|
+
if (options.mine) {
|
|
21
|
+
const userInfo = await client.getAccounts('/users/me', true);
|
|
22
|
+
userId = userInfo.user_id;
|
|
23
|
+
}
|
|
24
|
+
if (options.all) {
|
|
25
|
+
// Fetch all pages automatically using Workspace API
|
|
26
|
+
const baseParams = {};
|
|
27
|
+
if (options.orderBy)
|
|
28
|
+
baseParams['order_by'] = options.orderBy;
|
|
29
|
+
if (options.orderDirection)
|
|
30
|
+
baseParams['order_direction'] = options.orderDirection;
|
|
31
|
+
if (options.search)
|
|
32
|
+
baseParams['search'] = options.search;
|
|
33
|
+
if (options.privacy)
|
|
34
|
+
baseParams['privacy'] = options.privacy;
|
|
35
|
+
if (options.tags)
|
|
36
|
+
baseParams['tags'] = JSON.stringify(options.tags);
|
|
37
|
+
if (userId)
|
|
38
|
+
baseParams['user_id'] = userId;
|
|
39
|
+
const paginatedResult = await client.getAllPaginatedWorkspace('/maps', baseParams);
|
|
40
|
+
result = {
|
|
41
|
+
data: paginatedResult.data,
|
|
42
|
+
meta: {
|
|
43
|
+
page: {
|
|
44
|
+
total: paginatedResult.total,
|
|
45
|
+
current: 1,
|
|
46
|
+
pageSize: paginatedResult.total,
|
|
47
|
+
totalPages: 1
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Fetch single page using Workspace API
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
if (options.orderBy)
|
|
56
|
+
params.append('order_by', options.orderBy);
|
|
57
|
+
if (options.orderDirection)
|
|
58
|
+
params.append('order_direction', options.orderDirection);
|
|
59
|
+
if (options.pageSize)
|
|
60
|
+
params.append('page_size', options.pageSize);
|
|
61
|
+
if (options.page)
|
|
62
|
+
params.append('page', options.page);
|
|
63
|
+
if (options.search)
|
|
64
|
+
params.append('search', options.search);
|
|
65
|
+
if (options.privacy)
|
|
66
|
+
params.append('privacy', options.privacy);
|
|
67
|
+
if (options.tags)
|
|
68
|
+
params.append('tags', JSON.stringify(options.tags));
|
|
69
|
+
if (userId)
|
|
70
|
+
params.append('user_id', userId);
|
|
71
|
+
const queryString = params.toString();
|
|
72
|
+
const path = `/maps${queryString ? '?' + queryString : ''}`;
|
|
73
|
+
result = await client.getWorkspace(path);
|
|
74
|
+
}
|
|
75
|
+
if (jsonOutput) {
|
|
76
|
+
console.log(JSON.stringify(result));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const maps = result.data || result.results || result;
|
|
80
|
+
const total = result.meta?.page?.total || result.total || maps.length;
|
|
81
|
+
const currentPage = result.meta?.page?.current || result.page || 1;
|
|
82
|
+
const pageSize = result.meta?.page?.pageSize || result.page_size || 12;
|
|
83
|
+
const totalPages = result.meta?.page?.totalPages || Math.ceil(total / pageSize);
|
|
84
|
+
// Display header with search query if present
|
|
85
|
+
const header = options.search
|
|
86
|
+
? `\n${total} maps found for '${options.search}':\n`
|
|
87
|
+
: `\nMaps (${total}):\n`;
|
|
88
|
+
console.log((0, colors_1.bold)(header));
|
|
89
|
+
maps.forEach((map) => {
|
|
90
|
+
console.log((0, colors_1.bold)('Map ID: ') + map.id);
|
|
91
|
+
console.log(' Name: ' + (map.title || '(Untitled)'));
|
|
92
|
+
console.log(' Owner: ' + (map.ownerEmail || 'N/A'));
|
|
93
|
+
console.log(' Privacy: ' + (map.privacy || 'N/A'));
|
|
94
|
+
console.log(' Views: ' + (map.views !== undefined ? map.views : 'N/A'));
|
|
95
|
+
if (map.collaborative)
|
|
96
|
+
console.log(' Collaborative: Yes');
|
|
97
|
+
if (map.tags && map.tags.length > 0) {
|
|
98
|
+
console.log(' Tags: ' + map.tags.join(', '));
|
|
99
|
+
}
|
|
100
|
+
console.log(' Created: ' + new Date(map.createdAt || map.created_at).toLocaleString());
|
|
101
|
+
console.log(' Updated: ' + new Date(map.updatedAt || map.updated_at).toLocaleString());
|
|
102
|
+
console.log('');
|
|
103
|
+
});
|
|
104
|
+
if (options.all) {
|
|
105
|
+
console.log((0, colors_1.dim)(`Fetched all ${total} maps`));
|
|
106
|
+
}
|
|
107
|
+
else if (totalPages > 1) {
|
|
108
|
+
console.log((0, colors_1.dim)(`Page ${currentPage} of ${totalPages} (use --all to fetch all pages)`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (jsonOutput) {
|
|
114
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
118
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
119
|
+
console.log('Please run: carto auth login');
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log((0, colors_1.error)('✗ Failed to list maps: ' + err.message));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function mapsGet(mapId, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
129
|
+
try {
|
|
130
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
131
|
+
// Fetch map details and datasets from Workspace API
|
|
132
|
+
const map = await client.getWorkspace(`/maps/${mapId}`);
|
|
133
|
+
const datasets = await client.getWorkspace(`/maps/${mapId}/datasets`);
|
|
134
|
+
if (jsonOutput) {
|
|
135
|
+
console.log(JSON.stringify({ map, datasets }));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Display map metadata
|
|
139
|
+
console.log((0, colors_1.bold)('\n=== Map Details ===\n'));
|
|
140
|
+
console.log((0, colors_1.bold)('ID: ') + map.id);
|
|
141
|
+
console.log((0, colors_1.bold)('Title: ') + (map.title || '(Untitled)'));
|
|
142
|
+
if (map.description)
|
|
143
|
+
console.log((0, colors_1.bold)('Description: ') + map.description);
|
|
144
|
+
console.log((0, colors_1.bold)('Owner: ') + (map.ownerEmail || 'N/A'));
|
|
145
|
+
console.log((0, colors_1.bold)('Privacy: ') + (map.privacy || 'N/A'));
|
|
146
|
+
console.log((0, colors_1.bold)('Views: ') + (map.views !== undefined ? map.views : 'N/A'));
|
|
147
|
+
console.log((0, colors_1.bold)('Collaborative: ') + (map.collaborative ? 'Yes' : 'No'));
|
|
148
|
+
console.log((0, colors_1.bold)('Agent Enabled: ') + (map.agentEnabled || map.agent?.enabledForViewer ? 'Yes' : 'No'));
|
|
149
|
+
if (map.publishedWithPassword)
|
|
150
|
+
console.log((0, colors_1.bold)('Password Protected: ') + 'Yes');
|
|
151
|
+
if (map.sharingScope)
|
|
152
|
+
console.log((0, colors_1.bold)('Sharing Scope: ') + map.sharingScope);
|
|
153
|
+
// Tags
|
|
154
|
+
if (map.tags && map.tags.length > 0) {
|
|
155
|
+
console.log((0, colors_1.bold)('Tags: ') + map.tags.join(', '));
|
|
156
|
+
}
|
|
157
|
+
// Timestamps
|
|
158
|
+
console.log((0, colors_1.bold)('Created: ') + new Date(map.createdAt).toLocaleString());
|
|
159
|
+
console.log((0, colors_1.bold)('Updated: ') + new Date(map.updatedAt).toLocaleString());
|
|
160
|
+
// Datasets and Connections
|
|
161
|
+
if (datasets && datasets.length > 0) {
|
|
162
|
+
console.log((0, colors_1.bold)(`\n=== Datasets (${datasets.length}) ===\n`));
|
|
163
|
+
// Extract unique connections
|
|
164
|
+
const connectionsMap = new Map();
|
|
165
|
+
datasets.forEach((ds) => {
|
|
166
|
+
if (ds.connectionId && ds.connectionName) {
|
|
167
|
+
connectionsMap.set(ds.connectionId, {
|
|
168
|
+
id: ds.connectionId,
|
|
169
|
+
name: ds.connectionName
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// Display connections
|
|
174
|
+
if (connectionsMap.size > 0) {
|
|
175
|
+
console.log((0, colors_1.bold)('Connections used:'));
|
|
176
|
+
connectionsMap.forEach((conn) => {
|
|
177
|
+
console.log(` • ${conn.name} (${conn.id})`);
|
|
178
|
+
});
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
// Display datasets
|
|
182
|
+
console.log((0, colors_1.bold)('Datasets:'));
|
|
183
|
+
datasets.forEach((ds, idx) => {
|
|
184
|
+
console.log(` ${idx + 1}. ${(0, colors_1.bold)(ds.label || ds.name || ds.id)}`);
|
|
185
|
+
console.log(` Connection: ${ds.connectionName || 'N/A'}`);
|
|
186
|
+
if (ds.source)
|
|
187
|
+
console.log(` Source: ${ds.source}`);
|
|
188
|
+
if (ds.type)
|
|
189
|
+
console.log(` Type: ${ds.type}`);
|
|
190
|
+
console.log('');
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log((0, colors_1.dim)('\nNo datasets found for this map'));
|
|
195
|
+
}
|
|
196
|
+
// Map URL
|
|
197
|
+
const mapUrl = `${client.workspaceUrl}/map/${mapId}`;
|
|
198
|
+
console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (jsonOutput) {
|
|
203
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
207
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
208
|
+
console.log('Please run: carto auth login');
|
|
209
|
+
}
|
|
210
|
+
else if (err.message.includes('404')) {
|
|
211
|
+
console.log((0, colors_1.error)('✗ Map not found: ' + mapId));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.log((0, colors_1.error)('✗ Failed to get map: ' + err.message));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function mapsDelete(mapId, token, baseUrl, jsonOutput, debug = false, profile, yes) {
|
|
221
|
+
try {
|
|
222
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
223
|
+
// Skip confirmation if --yes or --json flag is set
|
|
224
|
+
const skipConfirmation = yes || jsonOutput;
|
|
225
|
+
if (!skipConfirmation) {
|
|
226
|
+
// Fetch map details to show in confirmation prompt
|
|
227
|
+
let mapDetails;
|
|
228
|
+
try {
|
|
229
|
+
mapDetails = await client.getWorkspace(`/maps/${mapId}`);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
// If we can't fetch details, proceed with generic message
|
|
233
|
+
console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete this map.'));
|
|
234
|
+
console.log((0, colors_1.warning)(` Map ID: ${mapId}`));
|
|
235
|
+
}
|
|
236
|
+
if (mapDetails) {
|
|
237
|
+
console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete:'));
|
|
238
|
+
console.log(` Map: "${mapDetails.title || '(Untitled)'}"`);
|
|
239
|
+
console.log(` ID: ${mapId}`);
|
|
240
|
+
if (mapDetails.ownerEmail) {
|
|
241
|
+
console.log(` Owner: ${mapDetails.ownerEmail}`);
|
|
242
|
+
}
|
|
243
|
+
if (mapDetails.privacy) {
|
|
244
|
+
console.log(` Privacy: ${mapDetails.privacy}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
console.log('');
|
|
248
|
+
const confirmed = await (0, prompt_1.promptForConfirmation)("Type 'delete' to confirm: ", 'delete');
|
|
249
|
+
if (!confirmed) {
|
|
250
|
+
console.log('\nDeletion cancelled');
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
console.log('');
|
|
254
|
+
}
|
|
255
|
+
await client.delete(`/v3/organization-maps/${mapId}`);
|
|
256
|
+
if (jsonOutput) {
|
|
257
|
+
console.log(JSON.stringify({ success: true, message: 'Map deleted successfully', id: mapId }));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.log((0, colors_1.success)(`✓ Map ${mapId} deleted successfully`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
if (jsonOutput) {
|
|
265
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.log((0, colors_1.error)('✗ Failed to delete map: ' + err.message));
|
|
269
|
+
}
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Check if CartoAI is enabled in the destination account
|
|
275
|
+
* Returns true if enabled, false otherwise
|
|
276
|
+
*/
|
|
277
|
+
async function checkCartoAIEnabled(client, jsonOutput) {
|
|
278
|
+
try {
|
|
279
|
+
const settings = await client.getWorkspace('/settings/carto-ai');
|
|
280
|
+
return settings?.enabled === true;
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
// If endpoint doesn't exist or returns error, assume not enabled
|
|
284
|
+
if (!jsonOutput) {
|
|
285
|
+
// Only log in debug mode, don't pollute normal output
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Copy agent configuration from source map to destination map
|
|
292
|
+
* Uses PATCH /maps/:id endpoint which properly checks CartoAI settings
|
|
293
|
+
*/
|
|
294
|
+
async function copyMapAgent(sourceClient, destClient, sourceMapId, destMapId, datasetsMapping, jsonOutput) {
|
|
295
|
+
try {
|
|
296
|
+
// Get source map to extract agent field
|
|
297
|
+
const sourceMap = await sourceClient.getWorkspace(`/maps/${sourceMapId}`);
|
|
298
|
+
if (!sourceMap.agent || !sourceMap.agent.config) {
|
|
299
|
+
// No agent configured in source
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
// Clone agent configuration
|
|
303
|
+
const agentConfig = JSON.parse(JSON.stringify(sourceMap.agent));
|
|
304
|
+
// Replace dataset IDs in agent config if needed (some agent configs may reference datasets)
|
|
305
|
+
let agentConfigStr = JSON.stringify(agentConfig);
|
|
306
|
+
for (const [oldId, newId] of Object.entries(datasetsMapping)) {
|
|
307
|
+
agentConfigStr = agentConfigStr.replace(new RegExp(oldId, 'g'), newId);
|
|
308
|
+
}
|
|
309
|
+
const updatedAgentConfig = JSON.parse(agentConfigStr);
|
|
310
|
+
// Update destination map with agent configuration
|
|
311
|
+
// Backend will handle:
|
|
312
|
+
// - CartoAI settings validation via ModelsService
|
|
313
|
+
// - Agent token creation
|
|
314
|
+
// - Workflow tool filtering
|
|
315
|
+
await destClient.patchWorkspace(`/maps/${destMapId}`, {
|
|
316
|
+
agent: updatedAgentConfig
|
|
317
|
+
});
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
throw new Error(`Failed to copy agent: ${err.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Validate that a dataset source is accessible in the destination connection
|
|
326
|
+
* Uses dry-run SQL queries with WHERE 1=0 to test without transferring data
|
|
327
|
+
*/
|
|
328
|
+
async function validateDatasetSource(client, connectionName, dataset, jsonOutput) {
|
|
329
|
+
try {
|
|
330
|
+
let validationSQL;
|
|
331
|
+
// Build dry-run query based on dataset type
|
|
332
|
+
if (dataset.type === 'table') {
|
|
333
|
+
// For tables: SELECT * FROM table WHERE 1=0 LIMIT 1
|
|
334
|
+
validationSQL = `SELECT * FROM ${dataset.source} WHERE 1=0 LIMIT 1`;
|
|
335
|
+
}
|
|
336
|
+
else if (dataset.type === 'query') {
|
|
337
|
+
// For queries: wrap in subquery with WHERE 1=0
|
|
338
|
+
validationSQL = `SELECT * FROM (${dataset.source}) AS _validate WHERE 1=0`;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// For tilesets or other types, treat as table
|
|
342
|
+
validationSQL = `SELECT * FROM ${dataset.source} WHERE 1=0 LIMIT 1`;
|
|
343
|
+
}
|
|
344
|
+
if (!jsonOutput) {
|
|
345
|
+
const displaySource = dataset.source.length > 60
|
|
346
|
+
? dataset.source.substring(0, 57) + '...'
|
|
347
|
+
: dataset.source;
|
|
348
|
+
console.log((0, colors_1.dim)(` ✓ ${dataset.label || dataset.name || 'Unnamed'} → ${displaySource}`));
|
|
349
|
+
}
|
|
350
|
+
// Execute validation query via SQL API
|
|
351
|
+
await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/query`, {
|
|
352
|
+
q: validationSQL,
|
|
353
|
+
queryParameters: {}
|
|
354
|
+
});
|
|
355
|
+
// Success - source is accessible
|
|
356
|
+
return { accessible: true };
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
// Extract meaningful error message from API response
|
|
360
|
+
let errorMsg = 'Unknown error';
|
|
361
|
+
if (err.message) {
|
|
362
|
+
errorMsg = err.message;
|
|
363
|
+
// Try to extract detail from API error JSON
|
|
364
|
+
try {
|
|
365
|
+
const match = err.message.match(/"detail":"([^"]+)"/);
|
|
366
|
+
if (match) {
|
|
367
|
+
errorMsg = match[1];
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
// Try to extract just the main error message
|
|
371
|
+
const errorMatch = err.message.match(/"error":"([^"]+)"/);
|
|
372
|
+
if (errorMatch) {
|
|
373
|
+
errorMsg = errorMatch[1];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// Keep original error message if parsing fails
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
accessible: false,
|
|
383
|
+
error: errorMsg
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function mapsCopy(mapId, connectionName, sourceProfile, destProfile, options, token, baseUrl, jsonOutput, debug = false) {
|
|
388
|
+
try {
|
|
389
|
+
// Step 1: Create API clients for source and destination
|
|
390
|
+
const sourceClient = await api_1.ApiClient.create(token, baseUrl, debug, sourceProfile);
|
|
391
|
+
const destClient = await api_1.ApiClient.create(token, baseUrl, debug, destProfile);
|
|
392
|
+
if (!jsonOutput) {
|
|
393
|
+
console.log((0, colors_1.dim)(`Copying map ${mapId} from ${sourceProfile} to ${destProfile}...`));
|
|
394
|
+
}
|
|
395
|
+
// Step 2: Fetch source map config and datasets
|
|
396
|
+
if (!jsonOutput) {
|
|
397
|
+
console.log((0, colors_1.dim)('→ Fetching source map configuration...'));
|
|
398
|
+
}
|
|
399
|
+
const mapConfig = await sourceClient.getWorkspace(`/maps/${mapId}`);
|
|
400
|
+
const datasets = await sourceClient.getWorkspace(`/maps/${mapId}/datasets`);
|
|
401
|
+
if (!jsonOutput) {
|
|
402
|
+
console.log((0, colors_1.dim)(`→ Found ${datasets.length} dataset(s) in source map`));
|
|
403
|
+
}
|
|
404
|
+
// Step 3: Parse connection mapping if provided
|
|
405
|
+
const manualMapping = {};
|
|
406
|
+
if (options.connectionMapping) {
|
|
407
|
+
const pairs = options.connectionMapping.split(',');
|
|
408
|
+
for (const pair of pairs) {
|
|
409
|
+
const [source, dest] = pair.split('=').map((s) => s.trim());
|
|
410
|
+
if (!source || !dest) {
|
|
411
|
+
throw new Error(`Invalid connection mapping format: "${pair}". Use format: "source1=dest1,source2=dest2"`);
|
|
412
|
+
}
|
|
413
|
+
if (manualMapping[source]) {
|
|
414
|
+
throw new Error(`Duplicate source connection in mapping: "${source}"`);
|
|
415
|
+
}
|
|
416
|
+
manualMapping[source] = dest;
|
|
417
|
+
}
|
|
418
|
+
// Check for circular mappings
|
|
419
|
+
const destValues = Object.values(manualMapping);
|
|
420
|
+
const duplicateDests = destValues.filter((item, index) => destValues.indexOf(item) !== index);
|
|
421
|
+
if (duplicateDests.length > 0) {
|
|
422
|
+
throw new Error(`Circular or duplicate destination mappings detected: ${duplicateDests.join(', ')}`);
|
|
423
|
+
}
|
|
424
|
+
if (!jsonOutput) {
|
|
425
|
+
console.log((0, colors_1.dim)(`→ Manual connection mapping: ${JSON.stringify(manualMapping)}`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Step 4: Collect unique source connections from datasets
|
|
429
|
+
const sourceConnectionsMap = new Map();
|
|
430
|
+
datasets.forEach((ds) => {
|
|
431
|
+
if (ds.connectionName) {
|
|
432
|
+
const existing = sourceConnectionsMap.get(ds.connectionName);
|
|
433
|
+
if (existing) {
|
|
434
|
+
existing.datasetCount++;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
sourceConnectionsMap.set(ds.connectionName, {
|
|
438
|
+
id: ds.connectionId,
|
|
439
|
+
name: ds.connectionName,
|
|
440
|
+
datasetCount: 1
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
if (!jsonOutput && sourceConnectionsMap.size > 0) {
|
|
446
|
+
console.log((0, colors_1.dim)(`→ Source map uses ${sourceConnectionsMap.size} connection(s):`));
|
|
447
|
+
sourceConnectionsMap.forEach((conn) => {
|
|
448
|
+
console.log((0, colors_1.dim)(` • ${conn.name} (${conn.datasetCount} dataset${conn.datasetCount > 1 ? 's' : ''})`));
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// Warn about mappings that reference non-existent source connections
|
|
452
|
+
if (Object.keys(manualMapping).length > 0) {
|
|
453
|
+
const sourceConnectionNames = Array.from(sourceConnectionsMap.keys());
|
|
454
|
+
for (const mappedSource of Object.keys(manualMapping)) {
|
|
455
|
+
if (!sourceConnectionNames.includes(mappedSource)) {
|
|
456
|
+
if (!jsonOutput) {
|
|
457
|
+
console.log((0, colors_1.error)(`⚠️ Warning: Mapping references "${mappedSource}" which is not used in source map`));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Step 5: Fetch all connections from destination
|
|
463
|
+
if (!jsonOutput) {
|
|
464
|
+
console.log((0, colors_1.dim)('→ Fetching connections from destination...'));
|
|
465
|
+
}
|
|
466
|
+
const destConnections = await destClient.getWorkspace('/connections');
|
|
467
|
+
const destConnectionsByName = new Map();
|
|
468
|
+
destConnections.forEach((c) => {
|
|
469
|
+
destConnectionsByName.set(c.name, { id: c.id, name: c.name });
|
|
470
|
+
});
|
|
471
|
+
// Step 6: Resolve connections (manual mapping → auto-map by name → legacy single connection)
|
|
472
|
+
const connectionResolution = {};
|
|
473
|
+
const missingConnections = [];
|
|
474
|
+
for (const [sourceName, sourceInfo] of sourceConnectionsMap.entries()) {
|
|
475
|
+
let resolved = false;
|
|
476
|
+
// Try manual mapping first
|
|
477
|
+
if (manualMapping[sourceName]) {
|
|
478
|
+
const mappedDestName = manualMapping[sourceName];
|
|
479
|
+
const destConn = destConnectionsByName.get(mappedDestName);
|
|
480
|
+
if (destConn) {
|
|
481
|
+
connectionResolution[sourceName] = {
|
|
482
|
+
destName: mappedDestName,
|
|
483
|
+
destId: destConn.id,
|
|
484
|
+
method: 'manual'
|
|
485
|
+
};
|
|
486
|
+
resolved = true;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// Mapped connection doesn't exist in destination
|
|
490
|
+
connectionResolution[sourceName] = {
|
|
491
|
+
destName: mappedDestName,
|
|
492
|
+
destId: null,
|
|
493
|
+
method: 'missing'
|
|
494
|
+
};
|
|
495
|
+
missingConnections.push({ sourceName, datasetCount: sourceInfo.datasetCount });
|
|
496
|
+
resolved = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Try auto-map by name
|
|
500
|
+
if (!resolved) {
|
|
501
|
+
const destConn = destConnectionsByName.get(sourceName);
|
|
502
|
+
if (destConn) {
|
|
503
|
+
connectionResolution[sourceName] = {
|
|
504
|
+
destName: sourceName,
|
|
505
|
+
destId: destConn.id,
|
|
506
|
+
method: 'auto'
|
|
507
|
+
};
|
|
508
|
+
resolved = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Legacy: use single --connection flag for ALL connections
|
|
512
|
+
if (!resolved && connectionName) {
|
|
513
|
+
const destConn = destConnectionsByName.get(connectionName);
|
|
514
|
+
if (destConn) {
|
|
515
|
+
connectionResolution[sourceName] = {
|
|
516
|
+
destName: connectionName,
|
|
517
|
+
destId: destConn.id,
|
|
518
|
+
method: 'legacy'
|
|
519
|
+
};
|
|
520
|
+
resolved = true;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
throw new Error(`Legacy connection "${connectionName}" not found in destination organization`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Mark as missing if still not resolved
|
|
527
|
+
if (!resolved) {
|
|
528
|
+
connectionResolution[sourceName] = {
|
|
529
|
+
destName: sourceName,
|
|
530
|
+
destId: null,
|
|
531
|
+
method: 'missing'
|
|
532
|
+
};
|
|
533
|
+
missingConnections.push({ sourceName, datasetCount: sourceInfo.datasetCount });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Step 7: Display connection resolution results
|
|
537
|
+
if (!jsonOutput) {
|
|
538
|
+
console.log((0, colors_1.dim)('→ Connection resolution:'));
|
|
539
|
+
for (const [sourceName, resolution] of Object.entries(connectionResolution)) {
|
|
540
|
+
if (resolution.method === 'manual') {
|
|
541
|
+
console.log((0, colors_1.dim)(` • ${sourceName} → ${resolution.destName} (manual mapping)`));
|
|
542
|
+
}
|
|
543
|
+
else if (resolution.method === 'auto') {
|
|
544
|
+
console.log((0, colors_1.dim)(` • ${sourceName} → ${resolution.destName} (auto-mapped)`));
|
|
545
|
+
}
|
|
546
|
+
else if (resolution.method === 'legacy') {
|
|
547
|
+
console.log((0, colors_1.dim)(` • ${sourceName} → ${resolution.destName} (legacy --connection)`));
|
|
548
|
+
}
|
|
549
|
+
else if (resolution.method === 'missing') {
|
|
550
|
+
console.log((0, colors_1.error)(` ✗ ${sourceName} → NOT FOUND`));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Step 8: Check for missing connections and fail
|
|
555
|
+
if (missingConnections.length > 0) {
|
|
556
|
+
const errorMsg = [
|
|
557
|
+
'Missing connections in destination organization:',
|
|
558
|
+
...missingConnections.map(mc => ` • "${mc.sourceName}" (used by ${mc.datasetCount} dataset${mc.datasetCount > 1 ? 's' : ''})`),
|
|
559
|
+
'',
|
|
560
|
+
'Solutions:',
|
|
561
|
+
' 1. Create missing connections in destination organization',
|
|
562
|
+
' 2. Use --connection-mapping to map to different connection names'
|
|
563
|
+
].join('\n');
|
|
564
|
+
throw new Error(errorMsg);
|
|
565
|
+
}
|
|
566
|
+
// Step 8.5: Validate source accessibility (unless --skip-source-validation)
|
|
567
|
+
if (!options.skipSourceValidation) {
|
|
568
|
+
if (!jsonOutput) {
|
|
569
|
+
console.log((0, colors_1.dim)(`→ Validating ${datasets.length} dataset source(s)...`));
|
|
570
|
+
}
|
|
571
|
+
const inaccessibleDatasets = [];
|
|
572
|
+
for (const dataset of datasets) {
|
|
573
|
+
const sourceConnectionName = dataset.connectionName;
|
|
574
|
+
const resolution = connectionResolution[sourceConnectionName];
|
|
575
|
+
// Use the destination connection name for the SQL query
|
|
576
|
+
const destConnectionName = resolution.destName;
|
|
577
|
+
const result = await validateDatasetSource(destClient, destConnectionName, dataset, jsonOutput);
|
|
578
|
+
if (!result.accessible) {
|
|
579
|
+
inaccessibleDatasets.push({
|
|
580
|
+
label: dataset.label || dataset.name || 'Unnamed',
|
|
581
|
+
source: dataset.source,
|
|
582
|
+
error: result.error
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (inaccessibleDatasets.length > 0) {
|
|
587
|
+
const errorMsg = [
|
|
588
|
+
'Source validation failed - datasets cannot access their data sources:',
|
|
589
|
+
...inaccessibleDatasets.map(ds => ` • "${ds.label}" → ${ds.source}\n Error: ${ds.error}`),
|
|
590
|
+
'',
|
|
591
|
+
'Solutions:',
|
|
592
|
+
' 1. Grant access to these tables/views in destination connection',
|
|
593
|
+
' 2. Ensure tables/views exist in destination data warehouse',
|
|
594
|
+
' 3. Use --skip-source-validation to create map anyway (datasets will not load data)'
|
|
595
|
+
].join('\n');
|
|
596
|
+
throw new Error(errorMsg);
|
|
597
|
+
}
|
|
598
|
+
if (!jsonOutput) {
|
|
599
|
+
console.log((0, colors_1.dim)(`→ All source(s) validated successfully`));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Step 9: Create new map in destination
|
|
603
|
+
const newTitle = options.title || mapConfig.title;
|
|
604
|
+
if (!jsonOutput) {
|
|
605
|
+
console.log((0, colors_1.dim)(`→ Creating new map "${newTitle}" in destination...`));
|
|
606
|
+
}
|
|
607
|
+
const newMap = await destClient.postWorkspace('/maps', {
|
|
608
|
+
title: newTitle,
|
|
609
|
+
});
|
|
610
|
+
const newMapId = newMap.id;
|
|
611
|
+
if (!newMapId) {
|
|
612
|
+
throw new Error('Failed to create map in destination');
|
|
613
|
+
}
|
|
614
|
+
// Step 10: Create datasets in destination with resolved connections
|
|
615
|
+
if (!jsonOutput) {
|
|
616
|
+
console.log((0, colors_1.dim)(`→ Creating ${datasets.length} dataset(s) in destination...`));
|
|
617
|
+
}
|
|
618
|
+
const datasetsMapping = {};
|
|
619
|
+
for (const dataset of datasets) {
|
|
620
|
+
const sourceConnectionName = dataset.connectionName;
|
|
621
|
+
const resolution = connectionResolution[sourceConnectionName];
|
|
622
|
+
const newDataset = {
|
|
623
|
+
...dataset,
|
|
624
|
+
connectionId: resolution.destId, // All connections are validated at this point
|
|
625
|
+
mapId: newMapId,
|
|
626
|
+
};
|
|
627
|
+
// Remove fields that shouldn't be copied
|
|
628
|
+
delete newDataset.id;
|
|
629
|
+
delete newDataset.connectionName;
|
|
630
|
+
delete newDataset.providerId;
|
|
631
|
+
delete newDataset.sourceWorkflowId;
|
|
632
|
+
delete newDataset.sourceWorkflowNodeId;
|
|
633
|
+
const createdDataset = await destClient.postWorkspace(`/maps/${newMapId}/datasets`, newDataset);
|
|
634
|
+
if (!createdDataset.id) {
|
|
635
|
+
throw new Error(`Failed to create dataset: ${dataset.name || dataset.label || 'unnamed'}`);
|
|
636
|
+
}
|
|
637
|
+
datasetsMapping[dataset.id] = createdDataset.id;
|
|
638
|
+
}
|
|
639
|
+
// Step 11: Update map configuration with new dataset IDs
|
|
640
|
+
if (!jsonOutput) {
|
|
641
|
+
console.log((0, colors_1.dim)('→ Updating map configuration with new dataset IDs...'));
|
|
642
|
+
}
|
|
643
|
+
let updatedConfig = JSON.stringify(mapConfig.keplerMapConfig || {});
|
|
644
|
+
// Replace all old dataset IDs with new ones
|
|
645
|
+
for (const [oldId, newId] of Object.entries(datasetsMapping)) {
|
|
646
|
+
updatedConfig = updatedConfig.replace(new RegExp(oldId, 'g'), newId);
|
|
647
|
+
}
|
|
648
|
+
await destClient.patchWorkspace(`/maps/${newMapId}`, {
|
|
649
|
+
keplerMapConfig: JSON.parse(updatedConfig),
|
|
650
|
+
});
|
|
651
|
+
// Step 12: Set privacy if needed
|
|
652
|
+
if (options.keepPrivacy !== false && mapConfig.privacy === 'public') {
|
|
653
|
+
if (!jsonOutput) {
|
|
654
|
+
console.log((0, colors_1.dim)('→ Setting map privacy to public...'));
|
|
655
|
+
}
|
|
656
|
+
await destClient.postWorkspace(`/maps/${newMapId}/privacy`, {
|
|
657
|
+
privacy: 'public',
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
// Step 13: Copy agent configuration (if enabled and not skipped)
|
|
661
|
+
if (!options.skipAgent) {
|
|
662
|
+
// Check if carto-ai is enabled in destination
|
|
663
|
+
if (!jsonOutput) {
|
|
664
|
+
console.log((0, colors_1.dim)('→ Checking AI capabilities in destination...'));
|
|
665
|
+
}
|
|
666
|
+
const cartoAIEnabled = await checkCartoAIEnabled(destClient, jsonOutput);
|
|
667
|
+
if (!cartoAIEnabled) {
|
|
668
|
+
// CartoAI not enabled - skip gracefully with warning
|
|
669
|
+
if (!jsonOutput) {
|
|
670
|
+
console.log((0, colors_1.warning)('⚠️ CartoAI not enabled in destination - skipping agent copy'));
|
|
671
|
+
console.log((0, colors_1.dim)(' Enable CartoAI in destination account settings to copy AI agents'));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
// CartoAI is enabled, proceed with agent copy
|
|
676
|
+
if (!jsonOutput) {
|
|
677
|
+
console.log((0, colors_1.dim)('→ Copying AI agent configuration...'));
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const agentCopied = await copyMapAgent(sourceClient, destClient, mapId, newMapId, datasetsMapping, jsonOutput);
|
|
681
|
+
if (agentCopied) {
|
|
682
|
+
if (!jsonOutput) {
|
|
683
|
+
console.log((0, colors_1.dim)('→ Agent copied successfully'));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
if (!jsonOutput) {
|
|
688
|
+
console.log((0, colors_1.dim)('→ No agent configuration found'));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
// Log agent copy failure as warning, but don't fail the whole operation
|
|
694
|
+
if (!jsonOutput) {
|
|
695
|
+
console.log((0, colors_1.warning)(`⚠️ Could not copy agent: ${err.message}`));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
if (!jsonOutput) {
|
|
702
|
+
console.log((0, colors_1.dim)('→ Skipping agent configuration copy'));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Get destination workspace URL for constructing map URL
|
|
706
|
+
const mapUrl = `${destClient.workspaceUrl}/map/${newMapId}`;
|
|
707
|
+
if (jsonOutput) {
|
|
708
|
+
console.log(JSON.stringify({
|
|
709
|
+
success: true,
|
|
710
|
+
sourceMapId: mapId,
|
|
711
|
+
newMapId: newMapId,
|
|
712
|
+
mapUrl: mapUrl,
|
|
713
|
+
sourceProfile: sourceProfile,
|
|
714
|
+
destProfile: destProfile,
|
|
715
|
+
datasetsCreated: datasets.length,
|
|
716
|
+
connectionResolution: connectionResolution,
|
|
717
|
+
}));
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
console.log((0, colors_1.success)(`\n✓ Map copied successfully!`));
|
|
721
|
+
console.log((0, colors_1.bold)('New Map ID: ') + newMapId);
|
|
722
|
+
console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
|
|
723
|
+
console.log((0, colors_1.dim)(`Datasets created: ${datasets.length}`));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
if (jsonOutput) {
|
|
728
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
console.log((0, colors_1.error)('✗ Failed to copy map: ' + err.message));
|
|
732
|
+
}
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Get JSON from various input sources (arg, file, stdin)
|
|
738
|
+
*/
|
|
739
|
+
async function getJsonFromInput(jsonString, options) {
|
|
740
|
+
let json = jsonString;
|
|
741
|
+
// Priority 1: --file flag
|
|
742
|
+
if (options.file) {
|
|
743
|
+
try {
|
|
744
|
+
json = (0, fs_1.readFileSync)(options.file, 'utf-8').trim();
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
throw new Error(`Failed to read file: ${err.message}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// Priority 2: stdin
|
|
751
|
+
if (!json && !process.stdin.isTTY) {
|
|
752
|
+
const chunks = [];
|
|
753
|
+
for await (const chunk of process.stdin) {
|
|
754
|
+
chunks.push(chunk);
|
|
755
|
+
}
|
|
756
|
+
json = Buffer.concat(chunks).toString('utf-8').trim();
|
|
757
|
+
}
|
|
758
|
+
// Parse JSON if we have it
|
|
759
|
+
if (!json) {
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
return JSON.parse(json);
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
throw new Error(`Invalid JSON: ${err.message}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function mapsCreate(jsonString, options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
770
|
+
try {
|
|
771
|
+
if (!jsonOutput) {
|
|
772
|
+
console.error((0, colors_1.dim)('⚠️ Note: maps create is experimental and subject to change\n'));
|
|
773
|
+
}
|
|
774
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
775
|
+
// Get map config from various sources
|
|
776
|
+
const config = await getJsonFromInput(jsonString, options);
|
|
777
|
+
if (!config) {
|
|
778
|
+
throw new Error('No map config provided. Use: carto maps create <json> or --file <path> or pipe via stdin');
|
|
779
|
+
}
|
|
780
|
+
if (!jsonOutput) {
|
|
781
|
+
console.log((0, colors_1.dim)('Creating map...'));
|
|
782
|
+
}
|
|
783
|
+
// Step 1: Create the map with basic metadata
|
|
784
|
+
const mapPayload = {};
|
|
785
|
+
if (config.title)
|
|
786
|
+
mapPayload.title = config.title;
|
|
787
|
+
if (config.description)
|
|
788
|
+
mapPayload.description = config.description;
|
|
789
|
+
if (config.collaborative !== undefined)
|
|
790
|
+
mapPayload.collaborative = config.collaborative;
|
|
791
|
+
const newMap = await client.postWorkspace('/maps', mapPayload);
|
|
792
|
+
const newMapId = newMap.id;
|
|
793
|
+
if (!newMapId) {
|
|
794
|
+
throw new Error('Failed to create map');
|
|
795
|
+
}
|
|
796
|
+
if (!jsonOutput) {
|
|
797
|
+
console.log((0, colors_1.dim)(`→ Map created: ${newMapId}`));
|
|
798
|
+
}
|
|
799
|
+
// Step 2: Create datasets if provided
|
|
800
|
+
let datasetsCreated = 0;
|
|
801
|
+
if (config.datasets && Array.isArray(config.datasets)) {
|
|
802
|
+
if (!jsonOutput) {
|
|
803
|
+
console.log((0, colors_1.dim)(`→ Creating ${config.datasets.length} dataset(s)...`));
|
|
804
|
+
}
|
|
805
|
+
for (const dataset of config.datasets) {
|
|
806
|
+
// Get connection ID if connectionName is provided
|
|
807
|
+
let connectionId = dataset.connectionId;
|
|
808
|
+
if (!connectionId && dataset.connectionName) {
|
|
809
|
+
const connections = await client.getWorkspace('/connections');
|
|
810
|
+
const connection = connections.find((c) => c.name === dataset.connectionName);
|
|
811
|
+
if (!connection) {
|
|
812
|
+
throw new Error(`Connection "${dataset.connectionName}" not found`);
|
|
813
|
+
}
|
|
814
|
+
connectionId = connection.id;
|
|
815
|
+
}
|
|
816
|
+
const datasetPayload = {
|
|
817
|
+
...dataset,
|
|
818
|
+
connectionId: connectionId,
|
|
819
|
+
mapId: newMapId,
|
|
820
|
+
};
|
|
821
|
+
// Remove fields that shouldn't be sent
|
|
822
|
+
delete datasetPayload.connectionName;
|
|
823
|
+
delete datasetPayload.id;
|
|
824
|
+
await client.postWorkspace(`/maps/${newMapId}/datasets`, datasetPayload);
|
|
825
|
+
datasetsCreated++;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Step 3: Update map with keplerMapConfig and agent if provided
|
|
829
|
+
const updatePayload = {};
|
|
830
|
+
if (config.keplerMapConfig)
|
|
831
|
+
updatePayload.keplerMapConfig = config.keplerMapConfig;
|
|
832
|
+
if (config.agent)
|
|
833
|
+
updatePayload.agent = config.agent;
|
|
834
|
+
if (config.tags)
|
|
835
|
+
updatePayload.tags = config.tags;
|
|
836
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
837
|
+
if (!jsonOutput) {
|
|
838
|
+
console.log((0, colors_1.dim)('→ Updating map configuration...'));
|
|
839
|
+
}
|
|
840
|
+
await client.patchWorkspace(`/maps/${newMapId}`, updatePayload);
|
|
841
|
+
}
|
|
842
|
+
// Step 4: Set privacy if provided
|
|
843
|
+
if (config.privacy) {
|
|
844
|
+
if (!jsonOutput) {
|
|
845
|
+
console.log((0, colors_1.dim)(`→ Setting privacy to ${config.privacy}...`));
|
|
846
|
+
}
|
|
847
|
+
const privacyPayload = { privacy: config.privacy };
|
|
848
|
+
if (config.sharingScope) {
|
|
849
|
+
privacyPayload.sharingScope = config.sharingScope;
|
|
850
|
+
}
|
|
851
|
+
await client.postWorkspace(`/maps/${newMapId}/privacy`, privacyPayload);
|
|
852
|
+
}
|
|
853
|
+
// Get map URL
|
|
854
|
+
const mapUrl = `${client.workspaceUrl}/map/${newMapId}`;
|
|
855
|
+
if (jsonOutput) {
|
|
856
|
+
console.log(JSON.stringify({
|
|
857
|
+
success: true,
|
|
858
|
+
mapId: newMapId,
|
|
859
|
+
mapUrl: mapUrl,
|
|
860
|
+
datasetsCreated: datasetsCreated,
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
console.log((0, colors_1.success)('\n✓ Map created successfully!'));
|
|
865
|
+
console.log((0, colors_1.bold)('Map ID: ') + newMapId);
|
|
866
|
+
console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
|
|
867
|
+
if (datasetsCreated > 0) {
|
|
868
|
+
console.log((0, colors_1.dim)(`Datasets created: ${datasetsCreated}`));
|
|
869
|
+
}
|
|
870
|
+
if (config.agent?.enabledForViewer) {
|
|
871
|
+
console.log((0, colors_1.dim)('Agent: enabled'));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
if (jsonOutput) {
|
|
877
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
881
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
882
|
+
console.log('Please run: carto auth login');
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
console.log((0, colors_1.error)('✗ Failed to create map: ' + err.message));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async function mapsUpdate(mapId, jsonString, options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
892
|
+
try {
|
|
893
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
894
|
+
// Get update config from various sources
|
|
895
|
+
const config = await getJsonFromInput(jsonString, options);
|
|
896
|
+
if (!config) {
|
|
897
|
+
throw new Error('No update config provided. Use: carto maps update <id> <json> or --file <path> or pipe via stdin');
|
|
898
|
+
}
|
|
899
|
+
if (!jsonOutput) {
|
|
900
|
+
console.log((0, colors_1.dim)(`Updating map ${mapId}...`));
|
|
901
|
+
}
|
|
902
|
+
// Build update payload (can be partial)
|
|
903
|
+
const updatePayload = {};
|
|
904
|
+
if (config.title !== undefined)
|
|
905
|
+
updatePayload.title = config.title;
|
|
906
|
+
if (config.description !== undefined)
|
|
907
|
+
updatePayload.description = config.description;
|
|
908
|
+
if (config.collaborative !== undefined)
|
|
909
|
+
updatePayload.collaborative = config.collaborative;
|
|
910
|
+
if (config.keplerMapConfig)
|
|
911
|
+
updatePayload.keplerMapConfig = config.keplerMapConfig;
|
|
912
|
+
if (config.agent)
|
|
913
|
+
updatePayload.agent = config.agent;
|
|
914
|
+
if (config.tags)
|
|
915
|
+
updatePayload.tags = config.tags;
|
|
916
|
+
// Update the map
|
|
917
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
918
|
+
await client.patchWorkspace(`/maps/${mapId}`, updatePayload);
|
|
919
|
+
}
|
|
920
|
+
// Update privacy if provided (separate endpoint)
|
|
921
|
+
if (config.privacy) {
|
|
922
|
+
if (!jsonOutput) {
|
|
923
|
+
console.log((0, colors_1.dim)(`→ Updating privacy to ${config.privacy}...`));
|
|
924
|
+
}
|
|
925
|
+
const privacyPayload = { privacy: config.privacy };
|
|
926
|
+
if (config.sharingScope) {
|
|
927
|
+
privacyPayload.sharingScope = config.sharingScope;
|
|
928
|
+
}
|
|
929
|
+
await client.postWorkspace(`/maps/${mapId}/privacy`, privacyPayload);
|
|
930
|
+
}
|
|
931
|
+
// Get map URL
|
|
932
|
+
const mapUrl = `${client.workspaceUrl}/map/${mapId}`;
|
|
933
|
+
if (jsonOutput) {
|
|
934
|
+
console.log(JSON.stringify({
|
|
935
|
+
success: true,
|
|
936
|
+
mapId: mapId,
|
|
937
|
+
mapUrl: mapUrl,
|
|
938
|
+
updated: Object.keys(updatePayload).concat(config.privacy ? ['privacy'] : []),
|
|
939
|
+
}));
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
console.log((0, colors_1.success)('✓ Map updated successfully!'));
|
|
943
|
+
console.log((0, colors_1.bold)('Map ID: ') + mapId);
|
|
944
|
+
console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
|
|
945
|
+
const updatedFields = Object.keys(updatePayload).concat(config.privacy ? ['privacy'] : []);
|
|
946
|
+
if (updatedFields.length > 0) {
|
|
947
|
+
console.log((0, colors_1.dim)(`Updated: ${updatedFields.join(', ')}`));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch (err) {
|
|
952
|
+
if (jsonOutput) {
|
|
953
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
957
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
958
|
+
console.log('Please run: carto auth login');
|
|
959
|
+
}
|
|
960
|
+
else if (err.message.includes('404')) {
|
|
961
|
+
console.log((0, colors_1.error)('✗ Map not found: ' + mapId));
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
console.log((0, colors_1.error)('✗ Failed to update map: ' + err.message));
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function mapsClone(mapId, options, token, baseUrl, jsonOutput, debug = false, profile) {
|
|
971
|
+
try {
|
|
972
|
+
const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
|
|
973
|
+
if (!jsonOutput) {
|
|
974
|
+
console.log((0, colors_1.dim)(`Cloning map ${mapId}...`));
|
|
975
|
+
}
|
|
976
|
+
// Call the clone endpoint
|
|
977
|
+
const clonedMap = await client.postWorkspace(`/maps/${mapId}/clone`, {});
|
|
978
|
+
// Optionally rename the cloned map
|
|
979
|
+
if (options.title) {
|
|
980
|
+
if (!jsonOutput) {
|
|
981
|
+
console.log((0, colors_1.dim)(`→ Renaming cloned map to "${options.title}"...`));
|
|
982
|
+
}
|
|
983
|
+
await client.patchWorkspace(`/maps/${clonedMap.id}`, {
|
|
984
|
+
title: options.title,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
// Get map URL
|
|
988
|
+
const mapUrl = `${client.workspaceUrl}/map/${clonedMap.id}`;
|
|
989
|
+
if (jsonOutput) {
|
|
990
|
+
console.log(JSON.stringify({
|
|
991
|
+
success: true,
|
|
992
|
+
sourceMapId: mapId,
|
|
993
|
+
clonedMapId: clonedMap.id,
|
|
994
|
+
mapUrl: mapUrl,
|
|
995
|
+
}));
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
console.log((0, colors_1.success)('\n✓ Map cloned successfully!'));
|
|
999
|
+
console.log((0, colors_1.bold)('Source Map ID: ') + mapId);
|
|
1000
|
+
console.log((0, colors_1.bold)('Cloned Map ID: ') + clonedMap.id);
|
|
1001
|
+
console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
catch (err) {
|
|
1005
|
+
if (jsonOutput) {
|
|
1006
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
if (err.message.includes('401') || err.message.includes('Token not defined')) {
|
|
1010
|
+
console.log((0, colors_1.error)('✗ Authentication required'));
|
|
1011
|
+
console.log('Please run: carto auth login');
|
|
1012
|
+
}
|
|
1013
|
+
else if (err.message.includes('404')) {
|
|
1014
|
+
console.log((0, colors_1.error)('✗ Map not found: ' + mapId));
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
console.log((0, colors_1.error)('✗ Failed to clone map: ' + err.message));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
}
|
|
1022
|
+
}
|