diff --git a/TwingateApiClient.mjs b/TwingateApiClient.mjs index 68568c3..58a24be 100644 --- a/TwingateApiClient.mjs +++ b/TwingateApiClient.mjs @@ -99,14 +99,20 @@ export class TwingateApiClient { isNode: true, canCreate: true, fields: [ - {name: "name", type: "string", isLabel: true, canQuery: true}, {name: "createdAt", type: "datetime"}, {name: "updatedAt", type: "datetime"}, - {name: "isActive", type: "boolean"}, + {name: "name", type: "string", isLabel: true, canQuery: true}, {name: "address", type: "Object", typeName: "ResourceAddress"}, + {name: "alias", type: "string"}, {name: "protocols", type: "Object", typeName: "ResourceProtocols"}, + {name: "isActive", type: "boolean"}, {name: "remoteNetwork", type: "Node", typeName: "RemoteNetwork"}, - {name: "groups", type: "Connection", typeName: "Group"} + {name: "groups", type: "Connection", typeName: "Group"}, // deprecated + {name: "serviceAccounts", type: "Connection", typeName: "ServiceAccount"}, // deprecated + //{name: "access", type: "Connection", typeName: "AccessConnection"}, // future to replace groups & serviceAccounts + {name: "isVisible", type: "boolean"}, + {name: "isBrowserShortcutEnabled", type: "boolean"}, + {name: "securityPolicy", type: "Node", typeName: "SecurityPolicy"} ] }, "RemoteNetwork": { @@ -907,13 +913,20 @@ export class TwingateApiClient { } - async createResource(name, address, remoteNetworkId, protocols = null, groupIds = []) { - const createResourceQuery = "mutation CreateResource($name:String!,$address:String!,$remoteNetworkId:ID!,$protocols:ProtocolsInput,$groupIds:[ID]){result:resourceCreate(address:$address,groupIds:$groupIds,name:$name,protocols:$protocols,remoteNetworkId:$remoteNetworkId){error entity{id name address{value} remoteNetwork{name} groups{edges{node{id name}}}}}}"; - let createResourceResponse = await this.exec(createResourceQuery, {name, address, remoteNetworkId, protocols, groupIds} ); + async createResource(name, address, remoteNetworkId, protocols = null, groupIds = [], alias = null, isBrowserShortcutEnabled = null, isVisible = null, securityPolicyId = null) { + const createResourceQuery = "mutation CreateResource($address:String!,$alias:String,$groupIds:[ID],$isBrowserShortcutEnabled:Boolean,$isVisible:Boolean,$name:String!,$protocols:ProtocolsInput,$remoteNetworkId:ID!,$securityPolicyId:ID){result:resourceCreate(address:$address,alias:$alias,groupIds:$groupIds,isBrowserShortcutEnabled:$isBrowserShortcutEnabled,isVisible:$isVisible,name:$name,protocols:$protocols,remoteNetworkId:$remoteNetworkId,securityPolicyId:$securityPolicyId){ok error entity{id name address{value} remoteNetwork{name} groups{edges{node{id name}}}}}}"; + let createResourceResponse = await this.exec(createResourceQuery, {address, alias, groupIds, isBrowserShortcutEnabled, isVisible, name, protocols, remoteNetworkId, securityPolicyId}); if ( createResourceResponse.result.error !== null ) throw new Error(`Error creating resource: '${createResourceResponse.result.error}'`) return createResourceResponse.result.entity; } + async updateResource(addedGroupIds = [], address, alias = null, groupIds = [], id, isActive, isBrowserShortcutEnabled = null, isVisible = null, name, protocols = null, remoteNetworkId, removedGroupIds = [], securityPolicyId = null) { + const updateResourceQuery = "mutation UpdateResource($addedGroupIds:[ID],$address:String,$alias:String,$groupIds:[ID],$id:ID!,$isActive:Boolean,$isBrowserShortcutEnabled:Boolean,$isVisible:Boolean,$name:String,$protocols:ProtocolsInput,$remoteNetworkId:ID,$removedGroupIds:[ID],$securityPolicyId:ID){result:resourceUpdate(addedGroupIds:$addedGroupIds,address:$address,alias:$alias,groupIds:$groupIds,id:$id,isActive:$isActive,isBrowserShortcutEnabled:$isBrowserShortcutEnabled,isVisible:$isVisible,name:$name,protocols:$protocols,remoteNetworkId:$remoteNetworkId,removedGroupIds:$removedGroupIds,securityPolicyId:$securityPolicyId){ok error entity{id name address{value} remoteNetwork{name} groups{edges{node{id name}}}}}}"; + let updateResourceResponse = await this.exec(updateResourceQuery, {addedGroupIds, address, alias, groupIds, id, isActive, isBrowserShortcutEnabled, isVisible, name, protocols, remoteNetworkId, removedGroupIds, securityPolicyId} ); + if ( updateResourceResponse.result.error !== null ) throw new Error(`Error updating resource: '${updateResourceResponse.result.error}'`); + return updateResourceResponse.result; + } + async removeGroup(id) { const removeGroupQuery = "mutation RemoveGroup($id:ID!){result:groupDelete(id:$id){ok, error}}"; let removeGroupResponse = await this.exec(removeGroupQuery, {id}); diff --git a/cliCmd/exportCmd.mjs b/cliCmd/exportCmd.mjs index 5d8f225..b146431 100644 --- a/cliCmd/exportCmd.mjs +++ b/cliCmd/exportCmd.mjs @@ -139,14 +139,18 @@ async function outputDot(client, options) { async function exportDot(client, options) { let dot = await outputDot(client, options); options.outputFile = options.outputFile || genFileNameFromNetworkName(options.accountName, options.format); - return await Deno.writeTextFile(`./${options.outputFile}`, dot); + let outputDir = `./output/${options.outputFile}_export`; + await Deno.mkdir(outputDir, {recursive: true}); + return await Deno.writeTextFile(`${outputDir}/${options.outputFile}_export`, dot); } async function exportImage(client, options) { let dot = await outputDot(client, options); options.outputFile = options.outputFile || genFileNameFromNetworkName(options.accountName, options.format); - return await renderDot(dot, `./${options.outputFile}`, {format: options.format}); + let outputDir = `./output/${options.outputFile}_export`; + await Deno.mkdir(outputDir, {recursive: true}); + return await renderDot(dot, `${outputDir}/${options.outputFile}_export`, {format: options.format}); } @@ -170,8 +174,10 @@ async function exportJson(client, options) { const allNodes = await client.fetchAll(configForExport); setLastConnectedOnUser(allNodes); - options.outputFile = options.outputFile || genFileNameFromNetworkName(options.accountName, "json"); - await Deno.writeTextFile(`./${options.outputFile}`, JSON.stringify(allNodes)); + options.outputFile = options.outputFile || genFileNameFromNetworkName(options.accountName, options.format); + let outputDir = `./output/${options.outputFile}_export`; + await Deno.mkdir(outputDir, {recursive: true}); + await Deno.writeTextFile(`${outputDir}/${options.outputFile}_export.${options.format}`, JSON.stringify(allNodes)); } @@ -203,8 +209,10 @@ async function exportExcel(client, options) { ws['!autofilter'] = {ref: ws["!ref"]}; XLSX.utils.book_append_sheet(wb, ws, typeName); } - options.outputFile = options.outputFile || genFileNameFromNetworkName(options.accountName); - await Deno.writeFile(`./${options.outputFile}`, new Uint8Array(XLSX.write(wb, {type: "array"}))); + options.outputFile = options.outputFile || genFileNameFromNetworkName(options.accountName, "xlsx"); + let outputDir = `./output/${options.outputFile}_export`; + await Deno.mkdir(outputDir, {recursive: true}); + await Deno.writeFile(`${outputDir}/${options.outputFile}_export.${options.format}`, new Uint8Array(XLSX.write(wb, {type: "array"}))); } @@ -223,6 +231,7 @@ export const exportCmd = new Command() .option("-n, --remote-networks [boolean]", "Include Remote Networks") .option("-r, --resources [boolean]", "Include Resources") .option("-g, --groups [boolean]", "Include Groups") + .option("-p, --security-policies [boolean]", "Include Security Policies") .option("-u, --users [boolean]", "Include Users") .option("-d, --devices [boolean]", "Include Devices (trust)") .description("Export from account to various formats") @@ -236,6 +245,7 @@ export const exportCmd = new Command() if ( options.groups === true ) options.typesToFetch.push("Group") if ( options.users === true ) options.typesToFetch.push("User") if ( options.devices === true ) options.typesToFetch.push("Device") + if ( options.securityPolicies === true ) options.typesToFetch.push("SecurityPolicy") let outputFn = outputFnMap[options.format]; if (outputFn == null) { diff --git a/cliCmd/importCmd.mjs b/cliCmd/importCmd.mjs index 945ef4b..409daa4 100644 --- a/cliCmd/importCmd.mjs +++ b/cliCmd/importCmd.mjs @@ -11,7 +11,8 @@ const optionToSheetMap = { groups: "Group", remoteNetworks: "RemoteNetwork", resources: "Resource", - devices: "Device" + devices: "Device", + securityPolicies: "SecurityPolicy" } const ImportAction = { @@ -63,11 +64,13 @@ async function fetchDataForImport(client, options, wb) { // If we're importing resources we prob need Groups and Remote Networks too if ( !typesToFetch.includes("Group") ) typesToFetch.push("Group"); if ( !typesToFetch.includes("RemoteNetwork") ) typesToFetch.push("RemoteNetwork"); + if ( !typesToFetch.includes("SecurityPolicy") ) typesToFetch.push("SecurityPolicy"); } else if ( typesToFetch.includes("Group") ) { // note 'else' is intentional // If we're importing groups we prob need Resources and Users too if ( !typesToFetch.includes("Resource") ) typesToFetch.push("Resource"); if ( !typesToFetch.includes("User") ) typesToFetch.push("User"); + if ( !typesToFetch.includes("SecurityPolicy") ) typesToFetch.push("SecurityPolicy"); } const allNodes = await client.fetchAll({ @@ -81,6 +84,7 @@ async function fetchDataForImport(client, options, wb) { allNodes.Group = allNodes.Group || []; allNodes.Device = allNodes.Device || []; allNodes.User = allNodes.User || []; + allNodes.SecurityPolicy = allNodes.SecurityPolicy || []; return {typesToFetch, allNodes}; } @@ -105,14 +109,16 @@ function tryResourceRowToProtocols(resourceRow) { return null; } + let resourceAllowIcmp = (resourceRow.protocolsAllowIcmp === "true" || resourceRow.protocolsAllowIcmp === "TRUE" || resourceRow.protocolsAllowIcmp == undefined || resourceRow.protocolsAllowIcmp === true); + let protocols = { - allowIcmp: resourceRow.protocolsAllowIcmp, + allowIcmp: resourceAllowIcmp, tcp: { - policy: resourceRow.protocolsTcpPolicy, + policy: resourceRow.protocolsTcpPolicy == undefined ? "ALLOW_ALL" : resourceRow.protocolsTcpPolicy, ports: tryProcessPortRestrictionString(resourceRow.protocolsTcpPorts) }, udp: { - policy: resourceRow.protocolsUdpPolicy, + policy: resourceRow.protocolsUdpPolicy == undefined ? "ALLOW_ALL" : resourceRow.protocolsUdpPolicy, ports: tryProcessPortRestrictionString(resourceRow.protocolsUdpPorts) } } @@ -125,12 +131,15 @@ let nodeLabelIdMap = { Resource: {}, Group: {}, Device: {}, - User: {} + User: {}, + SecurityPolicy: {} } const groupIdByName = (name) => nodeLabelIdMap.Group[name]; const userIdByEmail = (email) => nodeLabelIdMap.User[email]; const resourceIdByName = (resourceName) => nodeLabelIdMap.Resource[resourceName]; +const remoteNetworkIdByName = (remoteNetworkName) => nodeLabelIdMap.RemoteNetwork[remoteNetworkName]; +const securityPolicyIdByName = (securityPolicyLabel) => nodeLabelIdMap.SecurityPolicy[securityPolicyLabel]; export const importCmd = new Command() .option("-f, --file ", "Path to Excel file to import from", { @@ -141,6 +150,7 @@ export const importCmd = new Command() .option("-g, --groups [boolean]", "Include Groups") //.option("-u, --users [boolean]", "Include Users") .option("-d, --devices [boolean]", "Include Devices (trust)") + .option("-p, --security-policies [boolean]", "Include Security Policies") .option("-s, --sync [boolean]", "Attempt to synchronise entities with the same natural identifier") .option("-y, --assume-yes [boolean]", "Automatic yes to prompts; assume 'yes' as answer to all prompts") .description("Import from excel file to a Twingate account") @@ -168,7 +178,8 @@ export const importCmd = new Command() Resource: {}, Group: {}, Device: {}, - User: {} + User: {}, + SecurityPolicy: {} }; let nodeIdMap = Object.fromEntries([ @@ -176,7 +187,8 @@ export const importCmd = new Command() ...allNodes.Resource, ...allNodes.Group, ...allNodes.Device, - ...allNodes.User + ...allNodes.User, + ...allNodes.SecurityPolicy ].map(n => [n.id, n])); // Pre-process users @@ -233,6 +245,14 @@ export const importCmd = new Command() } } + // Pre-process security policies + for ( let node of allNodes.SecurityPolicy) { + if ( securityPolicyIdByName(node.name) != null ) { + throw new Error(`Security policy with duplicate name found: '${node.name}' - Ids: ['${securityPolicyIdByName(node.name)}', '${node.id}']`); + } + nodeLabelIdMap.SecurityPolicy[node.name] = node.id; + } + // Map of old id to new id let mergeMap = {}; let importCount = 0; @@ -343,25 +363,112 @@ export const importCmd = new Command() } break; case "Resource": + let duplicateResourceTracker = []; for ( let resourceRow of sheetData ) { - let existingRemoteNetwork = nodeIdMap[nodeLabelIdMap.RemoteNetwork[resourceRow.remoteNetworkLabel]]; - if ( existingRemoteNetwork != null && existingRemoteNetwork.resourceNames.includes(resourceRow.name) ) { - Log.info(`Resource with same name exists, will skip: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}'`); + // ID cell is not empty AND matches an existing resource AND name/address/remotenetwork are provided => UPDATE + // ID cell is empty AND name/address/remotenetwork are provided => CREATE + // else => IGNORE + + // A - check if ID was provided AND if it matches an existing resource ID AND there are no duplicates + let existingResourceId = null; + + // A1 - check if ID was provided + if (resourceRow.id != null && resourceRow.id != "" && resourceRow.id != undefined){ + + // A2 - check if ID matches an existing resource + if ( Object.values(nodeLabelIdMap.Resource).includes(resourceRow.id) ) { + existingResourceId = resourceRow.id; + + // A3 - check for duplicates before proceeding + let existingFound = false; + for (let i = 0; i < duplicateResourceTracker.length; i++){ + if (existingResourceId == duplicateResourceTracker[i]){ + existingFound = true; + break; + } + } + if (existingFound){ + Log.error(`Resource will be skipped: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}' - duplicate Id exists.`); + resourceRow["importAction"] = ImportAction.IGNORE; + resourceRow["importId"] = null; + + resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + importCount++; + continue; + } else { + duplicateResourceTracker.push(existingResourceId); + } + } + } + + // B - ensure resource CREATE/UPDATE requirements are met (name/addressValue/remoteNetworkLabel) + // name + if (resourceRow.name == null || resourceRow.name == "" || resourceRow.name == undefined){ + Log.error(`Resource will be skipped: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}' - name is required.`); resourceRow["importAction"] = ImportAction.IGNORE; - resourceRow["importId"] = existingRemoteNetwork.resources.filter(r => r.name === resourceRow.name)[0]; + resourceRow["importId"] = null; + + resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + importCount++; continue; + //break; } - if ( typeof resourceRow["addressValue"] !== "string" || resourceRow["addressValue"].length > 255 ) { - Log.error(`Resource will be skipped: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}' - Invalid address`); + // addressValue + if (resourceRow.addressValue == null || resourceRow.addressValue == "" || resourceRow.addressValue == undefined){ + Log.error(`Resource will be skipped: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}' - addressValue is required.`); resourceRow["importAction"] = ImportAction.IGNORE; resourceRow["importId"] = null; + + resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + importCount++; + continue; + //break; } - resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + // remoteNetworkLabel + if (resourceRow.remoteNetworkLabel == null || resourceRow.remoteNetworkLabel == "" || resourceRow.remoteNetworkLabel == undefined){ + Log.error(`Resource will be skipped: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}' - remoteNetworkLabel is required.`); + resourceRow["importAction"] = ImportAction.IGNORE; + resourceRow["importId"] = null; - Log.info(`Resource will be created: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}'`); - resourceRow["importAction"] = ImportAction.CREATE; - resourceRow["importId"] = null; - importCount++; + resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + importCount++; + continue; + //break; + } + + // 1 - if resourceRow.id is not empty AND matches an existing resource id (A) AND name/addressValue/remoteNetworkLabel are provided (B) => UPDATE + if (existingResourceId != null){ + Log.info(`Resource will be updated: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}'.`); + resourceRow["importAction"] = ImportAction.UPDATE; + resourceRow["importId"] = existingResourceId; + + resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + importCount++; + continue; + //break; + + // 2 - if resourceRow.id is empty (A) AND name/addressValue/remoteNetworkLabel are provided (B) => CREATE + } else if (existingResourceId == null){ + Log.info(`Resource will be created: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}'.`); + resourceRow["importAction"] = ImportAction.CREATE; + resourceRow["importId"] = null; + + resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + importCount++; + continue; + //break; + + // 3 - ELSE => IGNORE + } else { + Log.error(`Resource will be skipped: '${resourceRow.name}' in Remote Network '${resourceRow.remoteNetworkLabel}' - default ignore.`); + resourceRow["importAction"] = ImportAction.IGNORE; + resourceRow["importId"] = null; + + resourceRow["_protocol"] = tryResourceRowToProtocols(resourceRow); + importCount++; + continue; + //break; + } } break; case "Device": @@ -390,6 +497,23 @@ export const importCmd = new Command() importCount++; } break; + case "SecurityPolicy": + for ( let securityPolicyRow of sheetData) { + // 1. Check if network exists + let existingId = nodeLabelIdMap.SecurityPolicy[securityPolicyRow.name]; + if ( existingId != null ) { + Log.info(`Security Policy with same name already exists, will skip: '${securityPolicyRow.name}'`); + securityPolicyRow["importAction"] = ImportAction.IGNORE; + securityPolicyRow["importId"] = existingId; + continue; + } + + Log.info(`Remote Network will be created: '${securityPolicyRow.name}'`); + securityPolicyRow["importAction"] = ImportAction.CREATE; + securityPolicyRow["importId"] = null; + importCount++; + } + break; default: // NoOp break; @@ -441,29 +565,86 @@ export const importCmd = new Command() break; case "Resource": for ( let resourceRow of recordsToImport ) { - let remoteNetwork = nodeIdMap[nodeLabelIdMap.RemoteNetwork[resourceRow.remoteNetworkLabel]]; - if ( remoteNetwork == null ) { - // TODO - Log.warn(`Remote network not matched '${resourceRow.remoteNetworkLabel}' in resource '${resourceRow.name}' not matched, will skip.`); - continue; - } - let groups = resourceRow.groups || ""; - let groupIds = groups - .split(",") - .map(r => r.trim()) - .map(groupName => { - let groupId = groupIdByName(groupName); - if ( groupId == null ) { - Log.warn(`Group with name '${groupName}' in resource '${resourceRow.name}' not matched, will skip.`); + switch ( resourceRow.importAction ) { + case ImportAction.CREATE: + let remoteNetwork = nodeIdMap[nodeLabelIdMap.RemoteNetwork[resourceRow.remoteNetworkLabel]]; + if ( remoteNetwork == null ) { + // TODO + Log.warn(`Remote network not matched '${resourceRow.remoteNetworkLabel}' in resource '${resourceRow.name}' not matched, will skip.`); + continue; } - return groupId; - }) - .filter(groupId => groupId != null) - let newResource = await client.createResource(resourceRow.name, resourceRow.addressValue, remoteNetwork.id, resourceRow._protocol, groupIds); - resourceRow.importId = newResource.id; - delete resourceRow._protocol; - remoteNetwork.resourceNames.push(resourceRow.name); - remoteNetwork.resources.push({name: resourceRow.name, _imported: true}); + let groups = resourceRow.groups || ""; + let groupIds = groups + .split(",") + .map(r => r.trim()) + .map(groupName => { + let groupId = groupIdByName(groupName); + if ( groupId == null ) { + Log.warn(`Group with name '${groupName}' in resource '${resourceRow.name}' not matched. Resource will be created without a group assignment.`); + } + return groupId; + }) + .filter(groupId => groupId != null) + + // NEEDS FR: + //isActive (Boolean) + //serviceAccounts ([]) + + let remoteNetworkId = remoteNetworkIdByName(resourceRow.remoteNetworkLabel); + let securityPolicyId = securityPolicyIdByName(resourceRow.securityPolicyLabel); + let isVisible = (resourceRow.isVisible === "true" || resourceRow.isVisible === "TRUE" || resourceRow.isVisible == undefined || resourceRow.isVisible === true); + let isBrowserShortcutEnabled = (resourceRow.isBrowserShortcutEnabled === "true" || resourceRow.isBrowserShortcutEnabled === "TRUE" || resourceRow.isBrowserShortcutEnabled == undefined || resourceRow.isBrowserShortcutEnabled === true); + let newResource = await client.createResource(resourceRow.name, resourceRow.addressValue, remoteNetworkId, resourceRow._protocol, groupIds, resourceRow.alias, isBrowserShortcutEnabled, isVisible, securityPolicyId); + + resourceRow.importId = newResource.id; + delete resourceRow._protocol; + remoteNetwork.resourceNames.push(resourceRow.name); + remoteNetwork.resources.push({name: resourceRow.name, _imported: true}); + break; + case ImportAction.UPDATE: + // FUTURE + let r_addedGroupIds = []; + let r_removedGroupIds = []; + // ServiceAccounts ([]) + + //groupIds + let groups2 = resourceRow.groups || ""; + let groupIds2 = groups2 + .split(",") + .map(r => r.trim()) + .map(groupName => { + let groupId = groupIdByName(groupName); + if ( groupId == null ) { + Log.warn(`Group with name '${groupName}' in resource '${resourceRow.name}' not matched, will skip.`); + } + return groupId; + }) + .filter(groupId => groupId != null) + let r_groupIds = groupIds2; + + // All other + let r_address = resourceRow.addressValue; + let r_alias = resourceRow.alias; + let r_id = resourceRow.id; + let r_isActive = resourceRow.isActive; + let r_isBrowserShortcutEnabled = (resourceRow.isBrowserShortcutEnabled === "true" || resourceRow.isBrowserShortcutEnabled === "TRUE" || resourceRow.isBrowserShortcutEnabled == undefined || resourceRow.isBrowserShortcutEnabled === true); + let r_isVisible = (resourceRow.isVisible === "true" || resourceRow.isVisible === "TRUE" || resourceRow.isVisible == undefined || resourceRow.isVisible === true); + let r_name = resourceRow.name; + let r_protocols = resourceRow._protocol; + let r_remoteNetworkId = remoteNetworkIdByName(resourceRow.remoteNetworkLabel); + let r_securityPolicyId = securityPolicyIdByName(resourceRow.securityPolicyLabel); + + // Send to client + let result = await client.updateResource(r_addedGroupIds, r_address, r_alias, r_groupIds, r_id, r_isActive, r_isBrowserShortcutEnabled, r_isVisible, r_name, r_protocols, r_remoteNetworkId, r_removedGroupIds, r_securityPolicyId); + if ( result.ok !== true || result.error != null ) { + Log.error(`Error syncing resource: '${resourceRow.name}'(${resourceRow.importId}): ${result.error}`); + } + delete resourceRow._protocol; + break; + default: + // NoOp + break; + } } break; case "Device": @@ -495,9 +676,10 @@ export const importCmd = new Command() } } // Write results - let outputFilename = `importResults-${genFileNameFromNetworkName(options.accountName)}`; - await writeImportResults(mergeMap, outputFilename); - // Log completion - Log.success(`Import to '${networkName}' completed. Results written to: '${outputFilename}'.`); + options.outputFile = options.outputFile || genFileNameFromNetworkName(options.accountName); + let outputDir = `./output/${options.outputFile}_import`; + await Deno.mkdir(outputDir, {recursive: true}); + await writeImportResults(mergeMap, `${outputDir}/${options.outputFile}_import.xlsx`); + Log.success(`Import to '${networkName}' completed. Results written to: '${outputDir}/${options.outputFile}_import.xlsx'.`) return; }); \ No newline at end of file diff --git a/utils/smallUtilFuncs.mjs b/utils/smallUtilFuncs.mjs index 58f3230..f10d85e 100644 --- a/utils/smallUtilFuncs.mjs +++ b/utils/smallUtilFuncs.mjs @@ -9,8 +9,8 @@ import {decryptData, encryptData} from "../crypto.mjs"; import {Log} from "./log.js"; import {Input} from "https://deno.land/x/cliffy/prompt/input.ts"; import {VERSION} from "../version.js"; -import { readerFromStreamReader } from "https://deno.land/std/io/mod.ts"; - +//import { readerFromStreamReader } from "https://deno.land/std/io/mod.ts"; +import { readerFromStreamReader } from "https://deno.land/std@0.201.0/streams/mod.ts"; export function genFileNameFromNetworkName(networkName, extension = "xlsx") { @@ -18,7 +18,7 @@ export function genFileNameFromNetworkName(networkName, extension = "xlsx") { d = new Date(), date = d.toISOString().split('T')[0], time = (d.toTimeString().split(' ')[0]).replaceAll(":", "-"); - return `${networkName}-${date}_${time}.${extension}`; + return `${date}_${time}_${networkName}`; } export async function loadClientForCLI(options) {