{ "backed_up_at": "2026-07-10T12:58:15.925364", "workflow_count": 43, "workflows": { "Verona Networks PPPoE lease": { "id": "workflow_2ibweh1uRsDp5M289jEPqa", "description": null, "definition": { "steps": [ { "name": "Assign IP address to MAC address", "slug": "assign-ip-address-to-mac-address", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowAssignIpAddressToMacAddress($input: AssignIpAddressToMacAddressInput!) {\n assignIpAddressToMacAddress(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n ipAddress: state.input.framedip,\n macAddress: state.input.callingstationid,\n accountId: state.steps['get-account-id-from-radius-username'].output?.accounts[0].accountId\n }\n})" } ], "nextSteps": [ { "slug": "run-update-preseem-on-pppoe-ip-change" } ] }, { "name": "Should assign IP address", "slug": "should-assign-ip-address", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n return state.input.bound === '1';\n}", "elseSteps": [ { "slug": "end" } ], "thenSteps": [ { "slug": "get-account-id-from-radius-username" } ] }, { "name": "Get Account ID from RADIUS Username", "slug": "get-account-id-from-radius-username", "type": "CUSTOM_WORKFLOW", "nodeSlug": "get-account-id-from-radius-username", "allowSkipExecution": false, "inputs": [ { "key": "username", "type": "STATE", "value": "input.username" } ], "nextSteps": [ { "slug": "assign-ip-address-to-mac-address" }, { "slug": "assign-ip-address-to-mac-address", "errorCodes": [ "USERNAME_NOT_FOUND" ] } ] }, { "allowSkipExecution": false, "type": "GAIIA_START_WORKFLOW_EXECUTION", "slug": "run-update-preseem-on-pppoe-ip-change", "name": "Run Update Preseem on PPPoE IP Change", "nodeSlug": "start-workflow-execution", "inputs": [ { "key": "workflowId", "value": "string:workflow_aEcWxEsubpuSebPwajqseP", "type": "LITERAL" }, { "key": "fireAndForget", "value": "boolean:true", "type": "LITERAL" } ], "nextSteps": [ { "slug": "end" } ] } ], "trigger": { "nextSteps": [ { "slug": "should-assign-ip-address" } ], "type": "WEBHOOK", "inputJsonSchema": { "type": "object", "required": [ "bound", "callingstationid", "framedip", "username" ], "properties": { "bound": { "type": "string" }, "framedip": { "type": "string" }, "username": { "type": "string" }, "callingstationid": { "type": "string" } } } } } }, "Get Config for FreeRADIUS": { "id": "workflow_1vgYQa82YuccZmRvzW5T3P", "description": null, "definition": { "trigger": { "nextSteps": [ { "slug": "get-free-radius-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => {\n const { instances } = state.steps[\"get-free-radius-config\"].output;\n const groupDetails = state.steps[\"get-attributes-and-fall-through\"].output;\n const groupDetailsMap = new Map();\n for (const detail of groupDetails) {\n groupDetailsMap.set(`${detail.instanceName}:${detail.groupName}`, detail);\n }\n const enrichedInstances = instances.map((instance) => ({\n ...instance,\n groups: instance.groups.map((group) => {\n const key = `${instance.name}:${group.groupName}`;\n const details = groupDetailsMap.get(key);\n return {\n ...group,\n attributes: details?.attributes || [],\n fallThrough: details?.fallThrough ?? false\n };\n })\n }));\n return { instances: enrichedInstances };\n }", "type": "JSON" }, "steps": [ { "name": "get-free-radius-config", "slug": "get-free-radius-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ utils }) => {\n const instancesString = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesString) {\n return { instances: [] };\n }\n const instanceIds = instancesString.split(\",\").filter((id) => id.trim());\n const instances = [];\n for (const id of instanceIds) {\n const name = await utils.integration.datastore.getItem(`instances.${id}.name`);\n const [databaseHost, databasePort, databaseName, databaseUsername, radiusServerHost, coaPort, groups] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${id}.databaseHost`),\n utils.integration.datastore.getItem(`instances.${id}.databasePort`),\n utils.integration.datastore.getItem(`instances.${id}.databaseName`),\n utils.integration.datastore.getItem(`instances.${id}.databaseUsername`),\n utils.integration.datastore.getItem(`instances.${id}.radiusServerHost`),\n utils.integration.datastore.getItem(`instances.${id}.coaPort`),\n utils.integration.datastore.getItem(`instances.${id}.groups`)\n ]);\n instances.push({\n id,\n name: name || \"\",\n databaseHost: databaseHost || \"\",\n databasePort: databasePort ? Number(databasePort) : 0,\n databaseName: databaseName || \"\",\n databaseUsername: databaseUsername || \"\",\n radiusServerHost: radiusServerHost || \"\",\n coaPort: coaPort ? Number(coaPort) : 0,\n groups: groups ? JSON.parse(groups) : []\n });\n }\n return { instances };\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-attributes-and-fall-through" } ], "outputMapper": "({ output }) => output" }, { "name": "get-attributes-and-fall-through", "slug": "get-attributes-and-fall-through", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const { instances } = state.steps[\"get-free-radius-config\"].output;\n return instances.flatMap(\n (instance) => instance.groups.map((group) => ({\n instanceName: instance.name,\n groupName: group.groupName\n }))\n );\n }" } ], "initialSteps": [ { "slug": "query-group-details" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ state }) => state.steps[\"query-group-details\"].output" }, { "name": "query-group-details", "slug": "query-group-details", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"get-attributes-and-fall-through\"].instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { groupName } = state.currentParallelLoopIterations[\"get-attributes-and-fall-through\"];\n return [\n {\n query: `SELECT attribute, op, value FROM radgroupreply WHERE groupname = ?`,\n values: [groupName]\n }\n ];\n }", "key": "queries" } ], "nextSteps": [ { "slug": "get-attributes-and-fall-through-end" } ], "outputMapper": "({ state, output }) => {\n const loopItem = state.currentParallelLoopIterations[\"get-attributes-and-fall-through\"];\n const allResults = output.results[0] || [];\n const fallThroughRow = allResults.find((row) => row.attribute === \"Fall-Through\");\n const attributes = allResults.filter((row) => row.attribute !== \"Fall-Through\").map((row) => ({\n name: row.attribute,\n operator: row.op,\n value: row.value\n }));\n const fallThrough = !fallThroughRow || fallThroughRow.value === \"Yes\";\n return {\n groupName: loopItem.groupName,\n instanceName: loopItem.instanceName,\n attributes,\n fallThrough\n };\n }", "parentParallelLoopSlug": "get-attributes-and-fall-through" } ] } }, "Get Config for NetBox": { "id": "workflow_uncWf2LVGAEGAovxgYDRD8", "description": null, "definition": { "trigger": { "nextSteps": [ { "slug": "get-netbox-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => state.steps[\"get-netbox-config\"].output", "type": "JSON" }, "steps": [ { "name": "get-netbox-config", "slug": "get-netbox-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ utils }) => {\n const instancesString = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesString) {\n return { instances: [] };\n }\n const instanceIds = instancesString.split(\",\").filter((name) => name.trim());\n const instances = [];\n for (const id of instanceIds) {\n const name = await utils.integration.datastore.getItem(`instances.${id}.name`);\n const baseUrl = await utils.integration.datastore.getItem(`instances.${id}.baseUrl`);\n const prefixSynchronizationString = await utils.integration.datastore.getItem(`instances.${id}.prefixSynchronization`);\n const networkSiteSynchronizationString = await utils.integration.datastore.getItem(`instances.${id}.networkSiteSynchronization`);\n let prefixSynchronization = { ipamRoles: [], isEnabled: false };\n if (prefixSynchronizationString) {\n try {\n prefixSynchronization = JSON.parse(prefixSynchronizationString);\n } catch {\n }\n }\n let networkSiteSynchronization = { customFieldMappings: [], isEnabled: false };\n if (networkSiteSynchronizationString) {\n try {\n networkSiteSynchronization = JSON.parse(networkSiteSynchronizationString);\n } catch {\n }\n }\n instances.push({\n id,\n baseUrl: baseUrl || \"\",\n name: name || \"\",\n prefixSynchronization,\n networkSiteSynchronization\n });\n }\n return { instances };\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Verona Networks DHCP lease": { "id": "workflow_rpEtJsMPzhNSYoM1H3jGch", "description": null, "definition": { "steps": [ { "name": "Find gaiia account by inventory item assignation", "slug": "find-gaiia-account-by-inventory-item-assignation", "outputMapper": "({ output, state }) => {\n const assignedInventoryItems = output.response.findInventoryItemsFromDeviceIdentifiers;\n if (assignedInventoryItems.length > 1) {\n workflow.fail(`Many inventory items with MAC '${state.input.mac}' or remote ID '${state.input.remoteId}' assigned to gaiia accounts.`, 'MANY_ASSIGNED_DEVICES');\n }\n return assignedInventoryItems[0]?.inventoryItem.assignation.assignee;\n }", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaWorkflowFindInventoryItemsFromDeviceIdentifiersQuery($input: FindInventoryItemsFromDeviceIdentifiersQueryInput!) {\n findInventoryItemsFromDeviceIdentifiers(input: $input) {\n identifier\n inventoryItem {\n assignation {\n assignee {\n ... on Account {\n id\n }\n }\n }\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => {\n const inventoryItemMacAddress = state.input.remoteId\n .split(':')\n .map((macAddressPart) => macAddressPart.length === 2 ? macAddressPart.toUpperCase() : `0${macAddressPart}`.toUpperCase())\n .join(':'); // a:0:3e:44:97:6 -> 0A:00:3E:44:97:06\n return {\n input: { identifiers: [inventoryItemMacAddress] }\n };\n }" } ], "nextSteps": [ { "slug": "assign-ip-address-to-mac-address-with-account" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request" }, { "name": "Assign IP address to MAC address with account", "slug": "assign-ip-address-to-mac-address-with-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowAssignIpAddressToMacAddress($input: AssignIpAddressToMacAddressInput!) {\n assignIpAddressToMacAddress(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n ipAddress: state.input.ip,\n macAddress: state.input.mac,\n accountId: state.steps['find-gaiia-account-by-inventory-item-assignation'].output?.id\n }\n })" } ], "nextSteps": [ { "slug": "end" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request" }, { "name": "Assign IP address to MAC address without account", "slug": "assign-ip-address-to-mac-address-without-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowAssignIpAddressToMacAddress($input: AssignIpAddressToMacAddressInput!) {\n assignIpAddressToMacAddress(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n ipAddress: state.input.ip,\n macAddress: state.input.mac\n }\n })" } ], "nextSteps": [ { "slug": "end" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request" }, { "name": "Has remote ID", "slug": "has-remote-id", "condition": "({ secrets, state, utils, variables }) => {\n return state.input.remoteId;\n}", "elseSteps": [ { "slug": "assign-ip-address-to-mac-address-without-account" } ], "thenSteps": [ { "slug": "find-gaiia-account-by-inventory-item-assignation" } ], "type": "CONDITION", "nodeSlug": "condition" }, { "name": "Is IP address bound", "slug": "is-ip-address-bound", "condition": "({ secrets, state, utils, variables }) => {\n return state.input.bound === '1';\n}", "elseSteps": [ { "slug": "end" } ], "thenSteps": [ { "slug": "has-remote-id" } ], "type": "CONDITION", "nodeSlug": "condition" } ], "trigger": { "nextSteps": [ { "slug": "is-ip-address-bound" } ], "type": "WEBHOOK", "inputJsonSchema": { "type": "object", "required": [ "bound", "mac", "ip", "server", "remoteId" ], "properties": { "ip": { "type": "string" }, "mac": { "type": "string" }, "bound": { "type": "string" }, "server": { "type": "string" }, "remoteId": { "type": "string" } } } } } }, "NetBox HTTP Request": { "id": "workflow_w57zmPhtwUGon3AtuHSL3T", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "method": { "type": "string", "enum": [ "GET", "POST", "PUT", "DELETE", "PATCH" ], "description": "The HTTP method to use for the request." }, "path": { "type": "string", "description": "The URL path for the request." }, "body": { "type": [ "object", "array" ], "description": "Optional body of the request, typically used for POST and PUT requests." }, "params": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Optional query parameters for the request." }, "instanceName": { "type": "string" } }, "required": [ "method", "path" ] }, "nextSteps": [ { "slug": "fetch-datastore-key" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => {\n if (\"output\" in state.steps[\"http-request\"]) {\n return state.steps[\"http-request\"].output.data;\n }\n return {};\n }", "type": "JSON" }, "steps": [ { "name": "fetch-datastore-key", "slug": "fetch-datastore-key", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state.input", "key": "function" } ], "nextSteps": [ { "slug": "http-request" } ], "outputMapper": "async ({ state, utils }) => {\n const { instanceName } = state.input;\n let instanceId;\n if (!instanceName) {\n const instancesString = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesString) {\n return utils.fail(\"No instances found in datastore\", \"NO_INSTANCES_FOUND\");\n }\n const instanceIds = instancesString.split(\",\").map((id) => id.trim()).filter(Boolean);\n if (instanceIds.length === 0) {\n utils.fail(\"No instance IDs found\", \"NO_INSTANCE_IDS_FOUND\");\n }\n [instanceId] = instanceIds;\n } else {\n const retrievedInstanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!retrievedInstanceId) {\n return utils.fail(\"Invalid instance name provided\", \"INVALID_INSTANCE_NAME\");\n }\n instanceId = retrievedInstanceId;\n }\n const datastoreKey = `instances.${instanceId}`;\n const [baseURL, apiKey] = await Promise.all([\n utils.integration.datastore.getItem(`${datastoreKey}.baseURL`),\n utils.integration.datastore.getItem(`${datastoreKey}.apiKey`)\n ]);\n if (!baseURL)\n return utils.fail(\"Missing baseURL\", \"MISSING_BASE_URL\");\n if (!apiKey)\n return utils.fail(\"Missing API key\", \"MISSING_API_KEY\");\n return {\n baseURL,\n apiKey\n };\n }" }, { "name": "http-request", "slug": "http-request", "type": "HTTP_REST_REQUEST", "nodeSlug": "http-rest-request", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.body", "key": "body" }, { "type": "FUNCTION", "value": "async ({ state }) => ({\n Authorization: `Token ${state.steps[\"fetch-datastore-key\"].output.apiKey}`,\n \"Content-Type\": \"application/json\"\n })", "key": "headers" }, { "type": "FUNCTION", "value": "({ state }) => state.input.method", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => state.input.params", "key": "params" }, { "type": "FUNCTION", "value": "async ({ state }) => {\n const baseUrl = state.steps[\"fetch-datastore-key\"].output.baseURL;\n const baseUrlWithoutTrailingSlash = baseUrl.replace(/\\/$/, \"\");\n const pathWithoutLeadingSlash = state.input.path.replace(/^\\//, \"\");\n return `${baseUrlWithoutTrailingSlash}/${pathWithoutLeadingSlash}`;\n }", "key": "url" } ], "nextSteps": [ { "slug": "handle-http-failure", "errorCodes": [ "HTTP_REQUEST_FAILED" ] }, { "slug": "end" } ] }, { "name": "handle-http-failure", "slug": "handle-http-failure", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state, utils }) => {\n const IP_ADDRESS_NOT_FOUND_MESSAGE = \"Not found.\";\n const ADDRESS_ALREADY_RESERVED_MESSAGE = \"Duplicate IP address found\";\n const getNetboxErrorCode = (detail) => {\n if (detail.toLowerCase() === IP_ADDRESS_NOT_FOUND_MESSAGE.toLowerCase()) {\n return \"IP_ADDRESS_NOT_FOUND\";\n }\n return \"UNEXPECTED_ERROR\";\n };\n const failure = state.steps[\"http-request\"].error;\n const payload = failure.payload;\n if (payload?.detail) {\n return utils.fail(`Netbox returned the following error: ${payload.detail}`, getNetboxErrorCode(payload.detail), payload);\n }\n if (payload?.address && payload.address[0].toLowerCase().includes(ADDRESS_ALREADY_RESERVED_MESSAGE.toLowerCase())) {\n return utils.fail(\n `Netbox returned the following error: ${payload.address.map((message) => `'${message}'`).join(\", \")}`,\n \"ADDRESS_ALREADY_RESERVED\",\n payload\n );\n }\n return utils.fail(failure.message, failure.code, failure.payload);\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ] } ] } }, "Add RADIUS Account": { "id": "workflow_mi1m5xC4nANZnbXyNgXKc4", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "accountId": { "type": "string", "description": "The account ID in gaiia" }, "username": { "type": "string", "description": "The RADIUS username", "maxLength": 64 }, "instanceName": { "type": "string", "description": "The FreeRADIUS instance name" }, "password": { "type": "string", "description": "The RADIUS user password", "maxLength": 253 }, "accountDetails": { "type": "object", "properties": { "framedIpAddress": { "type": "string", "description": "The Framed-IP-Address attribute" }, "framedRoute": { "type": "string", "description": "The Framed-Route attribute" } }, "additionalProperties": false } }, "required": [ "accountId", "username", "instanceName", "password" ], "additionalProperties": false }, "nextSteps": [ { "slug": "validate-input" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n utils.fail(state.steps[\"handle-error\"].output.message, state.steps[\"handle-error\"].output.code, state.steps[\"handle-error\"].output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "validate-input", "slug": "validate-input", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName, username, password, accountDetails } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n if (username.length > 64) {\n return utils.fail(\"Username is too long. Max length is 64\", \"USERNAME_TOO_LONG\");\n }\n const usernameRegex = /^[\\w +./:@`\\-]+$/;\n if (!usernameRegex.test(username)) {\n return utils.fail(\n \"The username contains invalid characters. Allowed characters are alphanumeric characters, plus: space, +, ., /, :, @, `, -, and _.\",\n \"USERNAME_INVALID_CHARACTERS\"\n );\n }\n if (password.length > 253) {\n return utils.fail(\"Password is too long. Max length is 253\", \"PASSWORD_TOO_LONG\");\n }\n const ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})$/;\n if (accountDetails?.framedIpAddress !== void 0 && accountDetails?.framedIpAddress !== null && accountDetails?.framedIpAddress !== \"\" && !ipv4Regex.test(accountDetails.framedIpAddress)) {\n return utils.fail(\"Framed-IP-Address must be a valid IPv4 address\", \"INVALID_FRAMED_IP_ADDRESS\");\n }\n if (accountDetails?.framedRoute !== void 0 && accountDetails?.framedRoute !== null && accountDetails?.framedRoute !== \"\") {\n const ipPart = accountDetails.framedRoute.split(\"/\")[0].trim();\n if (!ipv4Regex.test(ipPart)) {\n return utils.fail(\"Framed-Route must contain a valid IPv4 address\", \"INVALID_FRAMED_ROUTE\");\n }\n }\n return {\n instanceId,\n instanceName\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "check-username-and-attribute-availability" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "check-username-and-attribute-availability", "slug": "check-username-and-attribute-availability", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username, accountDetails } = state.input;\n const queries = [];\n queries.push({\n query: \"SELECT COUNT(*) as count FROM radcheck WHERE username = ? LIMIT 1\",\n values: [username]\n });\n if (accountDetails?.framedIpAddress) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-IP-Address\" AND value = ?',\n values: [accountDetails.framedIpAddress]\n });\n }\n if (accountDetails?.framedRoute) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-Route\" AND value = ?',\n values: [accountDetails.framedRoute]\n });\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "get-account-details" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ state, output, utils }) => {\n const { accountDetails } = state.input;\n const [[usernameCount = { count: 0 }], ...restResults] = output.results;\n if (usernameCount.count > 0) {\n return utils.fail(\"Username already exists\", \"USERNAME_ALREADY_EXISTS\");\n }\n if (accountDetails?.framedIpAddress) {\n const [framedIpAddressCount] = restResults.shift();\n if (framedIpAddressCount.count > 0) {\n return utils.fail(\"Framed-IP-Address is not available\", \"FRAMED_IP_ADDRESS_NOT_AVAILABLE\");\n }\n }\n if (accountDetails?.framedRoute) {\n const [framedRouteCount] = restResults.shift();\n if (framedRouteCount.count > 0) {\n return utils.fail(\"Framed-Route is not available\", \"FRAMED_ROUTE_NOT_AVAILABLE\");\n }\n }\n return {\n success: true\n };\n }" }, { "name": "get-account-details", "slug": "get-account-details", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n query GetAccountAndProductSubscriptions($accountId: GlobalID!) {\n account(id: $accountId) {\n status {\n type\n }\n billingSubscriptions {\n nodes {\n productVersion {\n product {\n id\n }\n }\n unassignedAt\n }\n }\n }\n }\n `", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => ({\n accountId: state.input.accountId\n })", "key": "variables" } ], "nextSteps": [ { "slug": "create-radius-account" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "async ({ state, output, utils }) => {\n const { instanceId } = state.steps[\"validate-input\"].output;\n const response = output.response;\n const accountStatus = response.account?.status.type;\n if (!response.account) {\n return utils.fail(\"Account not found\", \"ACCOUNT_NOT_FOUND\");\n }\n const productIds = response.account.billingSubscriptions?.nodes.filter((node) => !node.unassignedAt).map((node) => node.productVersion.product.id) || [];\n const groupsData = await utils.integration.datastore.getItem(`instances.${instanceId}.groups`);\n let groups = [];\n if (groupsData) {\n const allGroups = JSON.parse(groupsData);\n const groupsToAssign = allGroups.filter((group) => {\n switch (group.mappingRule) {\n case \"ALL\":\n return true;\n case \"ACTIVE\":\n return accountStatus === \"ACTIVE\";\n case \"INACTIVE\":\n return accountStatus === \"INACTIVE\";\n case \"SUSPENDED\":\n return accountStatus === \"SUSPENDED\";\n case \"MATCH_PRODUCT\":\n const hasMatchingProduct = group.mappedProductIds.some((mappedProductId) => productIds.includes(mappedProductId));\n return hasMatchingProduct;\n }\n return false;\n });\n groups = groupsToAssign.map((group) => ({\n groupName: group.groupName,\n priority: group.priority\n }));\n }\n return {\n accountStatus: response.account.status.type,\n productIds,\n groups\n };\n }" }, { "name": "create-radius-account", "slug": "create-radius-account", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username, password, accountDetails } = state.input;\n const { groups } = state.steps[\"get-account-details\"].output;\n const queries = [];\n queries.push({\n query: \"INSERT INTO radcheck (username, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [username, \"Cleartext-Password\", \":=\", password]\n });\n if (accountDetails?.framedIpAddress) {\n queries.push({\n query: \"INSERT INTO radreply (username, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [username, \"Framed-IP-Address\", \"=\", accountDetails.framedIpAddress]\n });\n }\n if (accountDetails?.framedRoute) {\n queries.push({\n query: \"INSERT INTO radreply (username, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [username, \"Framed-Route\", \"+=\", accountDetails.framedRoute]\n });\n }\n if (groups.length > 0) {\n queries.push({\n query: `INSERT INTO radusergroup (username, groupname, priority) VALUES ${groups.map(() => \"(?, ?, ?)\").join(\", \")}`,\n values: groups.flatMap((group) => [username, group.groupName, String(group.priority)])\n });\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "store-in-datastore" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "store-in-datastore", "slug": "store-in-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { accountId, username } = state.input;\n const { instanceId } = state.steps[\"validate-input\"].output;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n const accounts = existingData ? JSON.parse(existingData) : [];\n const newAccount = {\n createdAt: (new Date()).toISOString(),\n instanceId,\n username\n };\n const updatedAccounts = [...accounts, newAccount];\n return [\n {\n key: `radiusAccount.${accountId}`,\n value: JSON.stringify(updatedAccounts)\n },\n {\n key: `radiusUsername.${instanceId}.${username}`,\n value: accountId\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"check-username-and-attribute-availability\"]?.error || steps[\"get-account-details\"]?.error || steps[\"validate-input\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Update RADIUS Account": { "id": "workflow_iNFhPzVMcuMd3WhXA1KfSE", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The FreeRADIUS instance name" }, "accountId": { "type": "string", "description": "The account ID in gaiia" }, "oldUsername": { "type": "string", "description": "The current RADIUS username" }, "username": { "type": "string", "description": "The new RADIUS username", "maxLength": 64 }, "password": { "type": "string", "description": "The new password (if changing)", "maxLength": 253 }, "framedIpAddress": { "type": "string", "description": "The Framed-IP-Address attribute" }, "framedRoute": { "type": "string", "description": "The Framed-Route attribute (without gateway)" } }, "required": [ "instanceName", "accountId", "oldUsername", "username" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-instance-coa-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n const error = state.steps[\"handle-error\"];\n utils.fail(error.output.message, error.output.code, error.output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-instance-coa-config", "slug": "get-instance-coa-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName, username, password, framedIpAddress, framedRoute } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n const [radiusServerHost, coaPort, coaSecret] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${instanceId}.radiusServerHost`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaPort`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaSecret`)\n ]);\n if (username.length > 64) {\n return utils.fail(\"Username is too long. Max length is 64\", \"USERNAME_TOO_LONG\");\n }\n const usernameRegex = /^[\\w +./:@`\\-]+$/;\n if (!usernameRegex.test(username)) {\n return utils.fail(\n \"The username contains invalid characters. Allowed characters are alphanumeric characters, plus: space, +, ., /, :, @, `, -, and _.\",\n \"USERNAME_INVALID_CHARACTERS\"\n );\n }\n if (password !== void 0 && password !== null && password.length > 253) {\n return utils.fail(\"Password is too long. Max length is 253\", \"PASSWORD_TOO_LONG\");\n }\n const ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})$/;\n if (framedIpAddress !== void 0 && framedIpAddress !== null && framedIpAddress !== \"\" && !ipv4Regex.test(framedIpAddress)) {\n return utils.fail(\"Framed-IP-Address must be a valid IPv4 address\", \"INVALID_FRAMED_IP_ADDRESS\");\n }\n if (framedRoute !== void 0 && framedRoute !== null && framedRoute !== \"\") {\n const ipPart = framedRoute.split(\"/\")[0].trim();\n if (!ipv4Regex.test(ipPart)) {\n return utils.fail(\"Framed-Route must contain a valid IPv4 address\", \"INVALID_FRAMED_ROUTE\");\n }\n }\n return {\n instanceId,\n instanceName,\n radiusServerHost: radiusServerHost || \"\",\n coaPort: coaPort ? Number(coaPort) : 0,\n coaSecret: coaSecret || \"\"\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "fetch-account-state-and-validate-transaction" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "fetch-account-state-and-validate-transaction", "slug": "fetch-account-state-and-validate-transaction", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const queries = [\n {\n query: `SELECT rc.attribute, rc.value FROM radcheck rc WHERE rc.username = ? AND rc.attribute LIKE ? AND rc.value IS NOT NULL;`,\n values: [state.input.oldUsername, \"%-Password\"]\n },\n {\n query: `SELECT rr.attribute, rr.value FROM radreply rr WHERE rr.username = ? AND rr.value IS NOT NULL;`,\n values: [state.input.oldUsername]\n }\n ];\n if (state.input.oldUsername !== state.input.username) {\n queries.push({\n query: \"SELECT COUNT(*) as count FROM radcheck WHERE username = ? LIMIT 1\",\n values: [state.input.username]\n });\n }\n if (state.input.framedIpAddress) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-IP-Address\" AND value = ? AND username <> ?',\n values: [state.input.framedIpAddress, state.input.oldUsername]\n });\n }\n if (state.input.framedRoute) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-Route\" AND value = ? AND username <> ?',\n values: [`${state.input.framedRoute} 0.0.0.0 1`, state.input.oldUsername]\n });\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "update-account-details" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ state, output, utils }) => {\n const { oldUsername, username, framedIpAddress, framedRoute } = state.input;\n const [oldPasswordResults, oldUsernameRadreplyResults, ...restResults] = output.results;\n const oldPasswordRow = oldPasswordResults?.find((r) => r.attribute && r.attribute.toLowerCase() === \"cleartext-password\");\n const oldPassword = oldPasswordRow?.value || null;\n const oldFramedIpAddressRow = oldUsernameRadreplyResults?.find((r) => r.attribute === \"Framed-IP-Address\");\n const oldFramedRouteRow = oldUsernameRadreplyResults?.find((r) => r.attribute === \"Framed-Route\");\n const oldFramedIpAddress = oldFramedIpAddressRow?.value || null;\n const oldFramedRoute = oldFramedRouteRow?.value || null;\n if (oldUsername !== username) {\n const [newUsernameExists] = restResults.shift();\n if (newUsernameExists.count > 0) {\n return utils.fail(\"Username already exists\", \"USERNAME_ALREADY_EXISTS\");\n }\n }\n if (framedIpAddress) {\n const [framedIpAddressExists] = restResults.shift();\n if (framedIpAddressExists.count > 0) {\n return utils.fail(\"Framed-IP-Address is not available\", \"FRAMED_IP_ADDRESS_NOT_AVAILABLE\");\n }\n }\n if (framedRoute) {\n const [framedRouteExists] = restResults.shift();\n if (framedRouteExists.count > 0) {\n return utils.fail(\"Framed-Route is not available\", \"FRAMED_ROUTE_NOT_AVAILABLE\");\n }\n }\n return {\n password: oldPassword,\n framedIpAddress: oldFramedIpAddress,\n framedRoute: oldFramedRoute\n };\n }" }, { "name": "update-account-details", "slug": "update-account-details", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { oldUsername, username, password, framedIpAddress, framedRoute } = state.input;\n const currentPassword = state.steps[\"fetch-account-state-and-validate-transaction\"].output.password;\n const currentFramedIpAddress = state.steps[\"fetch-account-state-and-validate-transaction\"].output.framedIpAddress;\n const currentFramedRoute = state.steps[\"fetch-account-state-and-validate-transaction\"].output.framedRoute;\n const currentFramedRouteWithGateway = currentFramedRoute ? `${currentFramedRoute} 0.0.0.0 1` : null;\n const queries = [];\n if (oldUsername !== username) {\n queries.push({\n query: `UPDATE radcheck rc\n LEFT JOIN radreply rr ON rc.username = rr.username\n LEFT JOIN radusergroup rug ON rc.username = rug.username\n SET\n rc.username = ?,\n rr.username = ?,\n rug.username = ?\n WHERE\n rc.username = ?\n AND rc.attribute LIKE ?`,\n values: [username, username, username, oldUsername, \"%-Password\"]\n });\n }\n if (password && password !== currentPassword) {\n queries.push({\n query: `UPDATE radcheck rc\n SET\n rc.value = ?,\n rc.attribute = ?\n WHERE\n rc.username = ?\n AND rc.attribute LIKE ?`,\n values: [password, \"Cleartext-Password\", username, \"%-Password\"]\n });\n }\n if (framedIpAddress !== void 0 && framedIpAddress !== currentFramedIpAddress) {\n if (currentFramedIpAddress && !framedIpAddress) {\n queries.push({\n query: `DELETE FROM radreply\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [username, \"Framed-IP-Address\"]\n });\n } else if (framedIpAddress) {\n const hasPreviousValue = currentFramedIpAddress !== null;\n if (hasPreviousValue) {\n queries.push({\n query: `UPDATE radreply\n SET value = ?\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [framedIpAddress, username, \"Framed-IP-Address\"]\n });\n } else {\n queries.push({\n query: `INSERT INTO radreply(username, attribute, op, value)\n VALUES (?, ?, ?, ?)`,\n values: [username, \"Framed-IP-Address\", \"=\", framedIpAddress]\n });\n }\n }\n }\n if (framedRoute !== void 0 && framedRoute !== currentFramedRoute) {\n const framedRouteWithGateway = framedRoute ? `${framedRoute} 0.0.0.0 1` : null;\n if (currentFramedRouteWithGateway && !framedRouteWithGateway) {\n queries.push({\n query: `DELETE FROM radreply\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [username, \"Framed-Route\"]\n });\n } else if (framedRouteWithGateway) {\n const hasPreviousValue = currentFramedRouteWithGateway !== null;\n if (hasPreviousValue) {\n queries.push({\n query: `UPDATE radreply\n SET value = ?\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [framedRouteWithGateway, username, \"Framed-Route\"]\n });\n } else {\n queries.push({\n query: `INSERT INTO radreply(username, attribute, op, value)\n VALUES (?, ?, ?, ?)`,\n values: [username, \"Framed-Route\", \"+=\", framedRouteWithGateway]\n });\n }\n }\n }\n queries.push({\n query: `\n SELECT\n acctsessionid as accountSessionId,\n nasipaddress as nasIpAddress,\n callingstationid as callingStationId\n FROM\n radacct\n WHERE\n username = ? AND acctstoptime IS NULL\n LIMIT 1;\n `,\n values: [oldUsername, oldUsername, oldUsername, oldUsername]\n });\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "check-active-session" } ], "outputMapper": "({ output }) => {\n const sessionResults = output.results[output.results.length - 1];\n const [session] = sessionResults || [];\n const oldUsername = session?.oldUsername || null;\n const attributes = {\n \"User-Name\": oldUsername || \"\",\n ...session?.nasIpAddress ? { \"NAS-IP-Address\": session.nasIpAddress } : {},\n ...session?.accountSessionId ? { \"Acct-Session-Id\": session.accountSessionId } : {},\n ...session?.callingStationId ? { \"Calling-Station-Id\": session.callingStationId } : {}\n };\n return {\n hasActiveSession: !!session,\n attributes,\n oldUsername\n };\n }" }, { "name": "check-active-session", "slug": "check-active-session", "nodeSlug": "condition", "type": "CONDITION", "condition": "async ({ state }) => state.steps[\"update-account-details\"].output.hasActiveSession", "thenSteps": [ { "slug": "send-coa-packet" } ], "elseSteps": [ { "slug": "update-datastore" } ] }, { "name": "send-coa-packet", "slug": "send-coa-packet", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-send-coa-packet", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"update-account-details\"].output.attributes", "key": "attributes" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "update-datastore" } ], "outputMapper": "({ output }) => ({ success: output.success })" }, { "name": "update-datastore", "slug": "update-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { accountId, oldUsername, username } = state.input;\n const { instanceId } = state.steps[\"get-instance-coa-config\"].output;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n if (!existingData) {\n return utils.fail(\"Account not found in datastore\", \"ACCOUNT_NOT_FOUND\");\n }\n const accounts = JSON.parse(existingData);\n const account = accounts.find((account2) => account2.instanceId === instanceId && account2.username === oldUsername);\n if (!account) {\n return utils.fail(\"Account not found in datastore\", \"ACCOUNT_NOT_FOUND\");\n }\n const updatedAccount = {\n ...account,\n username\n };\n const updatedAccounts = accounts.map((a) => a.instanceId === instanceId && a.username === oldUsername ? updatedAccount : a);\n const updates = [\n {\n key: `radiusAccount.${accountId}`,\n value: JSON.stringify(updatedAccounts)\n }\n ];\n if (oldUsername !== username) {\n updates.push(\n {\n key: `radiusUsername.${instanceId}.${oldUsername}`,\n value: \"\"\n // Empty string to delete the reverse mapping\n },\n {\n key: `radiusUsername.${instanceId}.${username}`,\n value: accountId\n }\n );\n }\n return updates;\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"fetch-account-state-and-validate-transaction\"]?.error || steps[\"get-instance-coa-config\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Verona Networks activation": { "id": "workflow_dK9EZgPKnUMQa6RmnqtFny", "description": null, "definition": { "steps": [ { "name": "Find gaiia account", "nextSteps": [ { "slug": "find-account-cpes" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "find-gaiia-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaActivationWorkflowAccountQuery($id: ID!) {\n account(id: $id) {\n gaiiaId\n primaryContact {\n firstName\n lastName\n }\n activeIpAddresses {\n ipAddress\n macAddress\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.accountId })" } ], "outputMapper": "({ output }) => {\n const { account } = output.response;\n if (account.activeIpAddresses.length === 0) {\n workflow.fail(`No active IP address associated to this account.`, 'NO_ACTIVE_IP_ADDRESS');\n }\n return account;\n}" }, { "name": "Find account CPEs", "nextSteps": [ { "slug": "find-account-internet-plans" } ], "nodeSlug": "find-account-cpes", "slug": "find-account-cpes", "type": "GAIIA_FIND_ACCOUNT_CPES", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Find account Internet plans", "nextSteps": [ { "slug": "preseem-create-account" } ], "nodeSlug": "find-account-internet-plans", "slug": "find-account-internet-plans", "type": "GAIIA_FIND_ACCOUNT_INTERNET_PLANS", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Preseem create account", "nextSteps": [ { "slug": "preseem-create-service" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-create-account", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/accounts/${state.steps['find-gaiia-account'].output.gaiiaId}`" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, primaryContact: { firstName, lastName } } = state.steps['find-gaiia-account'].output;\n return {\n id: String(gaiiaId),\n name: `${firstName} ${lastName}`\n };\n }" } ] }, { "name": "Preseem create service", "nextSteps": [ { "slug": "end" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-create-service", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/services/${state.steps['find-gaiia-account'].output.gaiiaId}`" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, activeIpAddresses } = state.steps['find-gaiia-account'].output;\n const [{ slug }] = state.steps['find-account-internet-plans'].output.internetPlans;\n const cpes = state.steps['find-account-cpes'].output.accountCpe;\n const macAddresses = [\n ...new Set([...activeIpAddresses.map(({ macAddress }) => macAddress), ...cpes.map(({ macAddress }) => macAddress)])\n ];\n return {\n id: String(gaiiaId),\n attachments: macAddresses.map((macAddress) => ({ cpe_mac: macAddress.toLowerCase() })),\n account: String(gaiiaId),\n package: slug\n };\n }" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-gaiia-account" } ], "type": "MANUAL", "objectType": "ACCOUNT" } } }, "Delete RADIUS Account": { "id": "workflow_3JJjHTGECV3bpfXwMEMkqc", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "username": { "type": "string", "description": "The RADIUS username" }, "instanceName": { "type": "string", "description": "The FreeRADIUS instance name" }, "accountId": { "type": "string", "description": "The account ID in gaiia" } }, "required": [ "username", "instanceName", "accountId" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-instance-coa-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n const error = state.steps[\"handle-error\"];\n utils.fail(error.output.message, error.output.code, error.output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-instance-coa-config", "slug": "get-instance-coa-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n const [radiusServerHost, coaPort, coaSecret] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${instanceId}.radiusServerHost`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaPort`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaSecret`)\n ]);\n return {\n instanceId,\n instanceName,\n radiusServerHost: radiusServerHost || \"\",\n coaPort: coaPort ? Number(coaPort) : 0,\n coaSecret: coaSecret || \"\"\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-account-details" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "get-account-details", "slug": "get-account-details", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username } = state.input;\n return [\n {\n query: `\n SELECT\n rc.username,\n rc.attribute,\n rc.value as password\n FROM\n radcheck rc\n WHERE\n rc.username = ?\n AND rc.attribute LIKE '%-Password'\n AND rc.value IS NOT NULL\n LIMIT 1;\n `,\n values: [username]\n },\n {\n query: `\n SELECT\n rug.groupname as groupName,\n rug.priority\n FROM\n radusergroup rug\n WHERE\n rug.username = ?;\n `,\n values: [username]\n },\n {\n query: `\n SELECT\n rr.attribute,\n rr.value\n FROM\n radreply rr\n WHERE\n rr.username = ?\n AND rr.attribute IN ('Framed-IP-Address', 'Framed-Route')\n AND rr.value IS NOT NULL;\n `,\n values: [username]\n }\n ];\n }", "key": "queries" } ], "nextSteps": [ { "slug": "delete-radius-account" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output, utils }) => {\n const accountDetailsResults = output.results[0];\n const passwordRow = accountDetailsResults.find((row) => row.attribute?.toLowerCase().includes(\"password\"));\n if (!passwordRow) {\n utils.fail(\"User not found in RADIUS database\", \"USERNAME_NOT_FOUND\");\n }\n const userGroupsResults = output.results[1];\n const groups = userGroupsResults.map((row) => ({\n groupName: row.groupName,\n priority: row.priority\n }));\n const radreplyResults = output.results[2]?.results || [];\n const framedIpAddress = radreplyResults.find((row) => row.attribute === \"Framed-IP-Address\")?.value;\n const framedRoute = radreplyResults.find((row) => row.attribute === \"Framed-Route\")?.value;\n return {\n groups,\n framedIpAddress: framedIpAddress || \"\",\n framedRoute: framedRoute || \"\",\n hasFramedIpAddress: !!framedIpAddress,\n hasFramedRoute: !!framedRoute\n };\n }" }, { "name": "delete-radius-account", "slug": "delete-radius-account", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username } = state.input;\n const { groups } = state.steps[\"get-account-details\"].output;\n const queries = [];\n queries.push({\n query: `\n SELECT\n acctsessionid as accountSessionId,\n nasipaddress as nasIpAddress,\n callingstationid as callingStationId\n FROM\n radacct\n WHERE\n username = ? AND acctstoptime IS NULL;\n `,\n values: [username]\n });\n if (groups.length > 0) {\n const groupPlaceholders = groups.map(() => \"?\").join(\", \");\n queries.push({\n query: `DELETE FROM radusergroup WHERE username = ? AND groupname IN (${groupPlaceholders})`,\n values: [username, ...groups.map((g) => g.groupName)]\n });\n }\n queries.push(\n // Delete only Framed-IP-Address and Framed-Route from radreply for this username\n {\n query: `DELETE FROM radreply WHERE username = ? AND attribute IN ('Framed-IP-Address', 'Framed-Route')`,\n values: [username]\n },\n // Delete from radcheck (LIMIT 1, matches radiusDeleteRadcheckCredentials)\n {\n query: \"DELETE FROM radcheck WHERE username = ? LIMIT 1\",\n values: [username]\n }\n );\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "check-active-session" } ], "outputMapper": "({ output, state }) => {\n const [sessions] = output.results;\n const hasActiveSession2 = sessions && sessions.length > 0;\n const sessionsAttributes = (sessions || []).map((session) => ({\n \"User-Name\": state.input.username,\n \"NAS-IP-Address\": session.nasIpAddress,\n \"Acct-Session-Id\": session.accountSessionId,\n \"Calling-Station-Id\": session.callingStationId\n }));\n return {\n hasActiveSession: hasActiveSession2,\n sessions: sessions || [],\n sessionsAttributes\n };\n }" }, { "name": "check-active-session", "slug": "check-active-session", "nodeSlug": "condition", "type": "CONDITION", "condition": "async ({ state }) => state.steps[\"delete-radius-account\"].output.hasActiveSession", "thenSteps": [ { "slug": "send-coa-packets" } ], "elseSteps": [ { "slug": "delete-from-datastore" } ] }, { "name": "send-coa-packets", "slug": "send-coa-packets", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const { sessionsAttributes } = state.steps[\"delete-radius-account\"].output;\n return sessionsAttributes;\n }" } ], "initialSteps": [ { "slug": "send-coa-disconnect" } ], "nextSteps": [ { "slug": "delete-from-datastore" } ], "outputMapper": "() => ({ success: true })" }, { "name": "send-coa-disconnect", "slug": "send-coa-disconnect", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-send-coa-packet", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"send-coa-packets\"]", "key": "attributes" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "send-coa-packets-end" } ], "outputMapper": "({ output }) => ({ success: output.success })", "parentParallelLoopSlug": "send-coa-packets" }, { "name": "delete-from-datastore", "slug": "delete-from-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { accountId, username } = state.input;\n const { instanceId } = state.steps[\"get-instance-coa-config\"].output;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n if (!existingData) {\n return utils.fail(\"Account not found in datastore\", \"ACCOUNT_NOT_FOUND\");\n }\n const accounts = JSON.parse(existingData);\n const updatedAccounts = accounts.filter((account) => !(account.instanceId === instanceId && account.username === username));\n return [\n {\n key: `radiusAccount.${accountId}`,\n value: JSON.stringify(updatedAccounts)\n },\n {\n key: `radiusUsername.${instanceId}.${username}`,\n value: \"\"\n // Empty string to delete the reverse mapping\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"delete-from-datastore\"]?.error || steps[\"check-active-session\"]?.error || steps[\"send-coa-disconnect\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Verona Networks cancellation": { "id": "workflow_t9iyLCe6xTJUrJALiBjpsL", "description": null, "definition": { "steps": [ { "name": "Preseem delete account", "nextSteps": [ { "slug": "create-cancellation-ticket" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-delete-account", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:DELETE" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/accounts/${state.steps['find-gaiia-account'].output.response.account.gaiiaId}`" } ] }, { "name": "Create cancellation ticket", "nextSteps": [ { "slug": "end" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "create-cancellation-ticket", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowCreateTicketMutation($input: CreateTicketInput!) {\n createTicket(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n title: `Account ${state.steps['find-gaiia-account'].output.response.account.gaiiaId} | Service Cancellation - Equipment Recovery`,\n typeId: 'e6d7b178-11f1-4ea6-bb20-a5bab438b64f', // Update for e6d7b178-11f1-4ea6-bb20-a5bab438b64f in prod\n priority: 'LOW',\n description: `
Account ${state.steps['find-gaiia-account'].output.response.account.gaiiaId} has cancelled service, please schedule time to go onsite to recover the equipments assigned to the linked account.

Once the equipment has been recovered, please navigate to the account page and unassign the equipment from the account.

-Field Service Team
`,\n entities: [\n {\n entityId: state.accountId,\n type: 'ACCOUNT'\n }\n ]\n }\n })" } ] }, { "name": "Find gaiia account", "nextSteps": [ { "slug": "preseem-delete-service" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "find-gaiia-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaCancellationWorkflowAccountQuery($id: ID!) {\n account(id: $id) {\n gaiiaId\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.accountId })" } ] }, { "name": "Preseem delete service", "nextSteps": [ { "slug": "preseem-delete-account" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-delete-service", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:DELETE" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/services/${state.steps['find-gaiia-account'].output.response.account.gaiiaId}`" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-gaiia-account" } ], "type": "MANUAL", "objectType": "ACCOUNT" } } }, "Get RADIUS Accounts": { "id": "workflow_qFHYnp5iEN4XNT9NYs7CSa", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "accountId": { "type": "string", "description": "The account ID in gaiia" } }, "required": [ "accountId" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-account-references" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => {\n const accounts = state.steps[\"merge-results\"].output;\n return {\n accounts\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-account-references", "slug": "get-account-references", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { accountId } = state.input;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n const accounts = existingData ? JSON.parse(existingData) : [];\n const accountsByInstance = new Map();\n for (const account of accounts) {\n const instanceAccounts = accountsByInstance.get(account.instanceId) || [];\n instanceAccounts.push(account);\n accountsByInstance.set(account.instanceId, instanceAccounts);\n }\n const instancesWithAccounts = [];\n for (const [instanceId, instanceAccounts] of accountsByInstance.entries()) {\n const instanceName = await utils.integration.datastore.getItem(`instances.${instanceId}.name`);\n if (!instanceName) {\n continue;\n }\n instancesWithAccounts.push({\n accounts: instanceAccounts,\n instanceId,\n instanceName\n });\n }\n return instancesWithAccounts;\n }", "key": "function" } ], "nextSteps": [ { "slug": "fetch-account-details" } ], "outputMapper": "({ output }) => output" }, { "name": "fetch-account-details", "slug": "fetch-account-details", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-account-references\"].output" } ], "initialSteps": [ { "slug": "query-account-details" } ], "nextSteps": [ { "slug": "merge-results" } ], "outputMapper": "({ state }) => state.steps[\"process-account-details\"].output" }, { "name": "query-account-details", "slug": "query-account-details", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"fetch-account-details\"].instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const usernames = state.currentParallelLoopIterations[\"fetch-account-details\"].accounts.map((a) => a.username);\n const placeholders = usernames.map(() => \"?\").join(\",\");\n return [\n {\n query: `SELECT username, attribute, value FROM radcheck WHERE username IN (${placeholders})`,\n values: usernames\n },\n {\n query: `SELECT username, groupname FROM radusergroup WHERE username IN (${placeholders}) ORDER BY username, priority`,\n values: usernames\n },\n {\n query: `SELECT username, attribute, value FROM radreply WHERE username IN (${placeholders}) AND attribute IN ('Framed-IP-Address', 'Framed-Route') AND value IS NOT NULL`,\n values: usernames\n }\n ];\n }", "key": "queries" } ], "nextSteps": [ { "slug": "process-account-details" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "fetch-account-details" }, { "name": "process-account-details", "slug": "process-account-details", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const { instanceName, accounts } = state.currentParallelLoopIterations[\"fetch-account-details\"];\n const [radcheckResults, radusergroupResults, radreplyResults] = state.steps[\"query-account-details\"].output.results;\n const radcheckByUsername = new Map();\n for (const row of radcheckResults || []) {\n const existing = radcheckByUsername.get(row.username) || [];\n existing.push({ attribute: row.attribute, value: row.value });\n radcheckByUsername.set(row.username, existing);\n }\n const groupsByUsername = new Map();\n for (const row of radusergroupResults || []) {\n const existing = groupsByUsername.get(row.username) || [];\n existing.push(row.groupname);\n groupsByUsername.set(row.username, existing);\n }\n const radreplyByUsername = new Map();\n for (const row of radreplyResults || []) {\n const existing = radreplyByUsername.get(row.username) || [];\n existing.push({ attribute: row.attribute, value: row.value });\n radreplyByUsername.set(row.username, existing);\n }\n const accountDetails = [];\n for (const account of accounts) {\n const radcheckRows = radcheckByUsername.get(account.username) || [];\n const radreplyRows = radreplyByUsername.get(account.username) || [];\n const passwordRow = radcheckRows.find((row) => row.attribute.toLowerCase().includes(\"password\"));\n if (!passwordRow) {\n continue;\n }\n const framedIpAddressRow = radreplyRows.find((row) => row.attribute === \"Framed-IP-Address\");\n const framedRouteRow = radreplyRows.find((row) => row.attribute === \"Framed-Route\");\n const groups = groupsByUsername.get(account.username) || [];\n accountDetails.push({\n ...account,\n instanceName,\n framedIpAddress: framedIpAddressRow?.value || \"\",\n framedRoute: framedRouteRow?.value?.split(\" \")[0] || \"\",\n groups\n });\n }\n return accountDetails;\n }", "key": "function" } ], "nextSteps": [ { "slug": "fetch-account-details-end" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "fetch-account-details" }, { "name": "merge-results", "slug": "merge-results", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const accountDetails = state.steps[\"fetch-account-details\"].output.flat();\n return accountDetails;\n }", "key": "function" } ], "nextSteps": [ { "slug": "cleanup-orphaned-records" } ], "outputMapper": "({ output }) => output" }, { "name": "cleanup-orphaned-records", "slug": "cleanup-orphaned-records", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state }) => {\n const { accountId } = state.input;\n const instancesWithAccounts = state.steps[\"get-account-references\"].output;\n const accountDetails = state.steps[\"merge-results\"].output;\n const accounts = instancesWithAccounts.flatMap((instance) => instance.accounts.map((account) => ({ ...account, instanceName: instance.instanceName })));\n const orphanedRecords = accounts.filter(\n (account) => !accountDetails.some((detail) => detail.instanceName === account.instanceName && detail.username === account.username)\n );\n if (orphanedRecords.length === 0) {\n return [];\n }\n const updatedAccounts = accounts.filter(\n (account) => !orphanedRecords.some((orphaned) => orphaned.instanceId === account.instanceId && orphaned.username === account.username)\n );\n return [\n {\n key: `radiusAccount.${accountId}`,\n value: JSON.stringify(updatedAccounts)\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n cleaned: true\n })" } ] } }, "Verona Networks reactivation": { "id": "workflow_2XT3DrAtHusRgzfTAky51c", "description": null, "definition": { "steps": [ { "name": "Find gaiia account", "nextSteps": [ { "slug": "find-account-cpes" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "find-gaiia-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaReactivationWorkflowAccountQuery($id: ID!) {\n account(id: $id) {\n gaiiaId\n activeIpAddresses {\n ipAddress\n macAddress\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.accountId })" } ], "outputMapper": "({ output }) => {\n const { account } = output.response;\n if (account.activeIpAddresses.length === 0) {\n workflow.fail(`No active IP address associated to this account.`, 'NO_ACTIVE_IP_ADDRESS');\n }\n return account;\n}" }, { "name": "Find account CPEs", "nextSteps": [ { "slug": "find-account-internet-plans" } ], "nodeSlug": "find-account-cpes", "slug": "find-account-cpes", "type": "GAIIA_FIND_ACCOUNT_CPES", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Find account Internet plans", "nextSteps": [ { "slug": "preseem-update-service" } ], "nodeSlug": "find-account-internet-plans", "slug": "find-account-internet-plans", "type": "GAIIA_FIND_ACCOUNT_INTERNET_PLANS", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Preseem update service", "nextSteps": [ { "slug": "end" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-update-service", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/services/${state.steps['find-gaiia-account'].output.gaiiaId}`" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, activeIpAddresses } = state.steps['find-gaiia-account'].output;\n const [{ slug }] = state.steps['find-account-internet-plans'].output.internetPlans;\n const cpes = state.steps['find-account-cpes'].output.accountCpe;\n const macAddresses = [\n ...new Set([...activeIpAddresses.map(({ macAddress }) => macAddress), ...cpes.map(({ macAddress }) => macAddress)])\n ];\n return {\n id: String(gaiiaId),\n attachments: macAddresses.map((macAddress) => ({ cpe_mac: macAddress.toLowerCase() })),\n account: String(gaiiaId),\n package: slug\n };\n }" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-gaiia-account" } ], "type": "MANUAL", "objectType": "ACCOUNT" } } }, "Get IPAM Roles from NetBox": { "id": "workflow_hteQnmBpUeVK6tuqYuHmnL", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "get-first-page-of-roles" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => ({\n roles: state.steps[\"merge-paginated-roles\"].output.roles.map((role) => ({ ...role }))\n })", "type": "JSON" }, "steps": [ { "name": "get-first-page-of-roles", "slug": "get-first-page-of-roles", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "() => `/ipam/roles?limit=100&offset=0`", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-paginated-roles" } ], "outputMapper": "({ output }) => {\n const response = output;\n const numberOfRemainingPages = Math.ceil(response.count / 100) - 1;\n return {\n pagesToFetch: Array.from({ length: numberOfRemainingPages }, (_, index) => index + 1),\n results: response.results\n };\n }" }, { "name": "get-paginated-roles", "slug": "get-paginated-roles", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-first-page-of-roles\"].output.pagesToFetch" } ], "initialSteps": [ { "slug": "get-page-of-roles" } ], "nextSteps": [ { "slug": "merge-paginated-roles" } ], "outputMapper": "({ state }) => state.steps[\"get-page-of-roles\"].output" }, { "name": "get-page-of-roles", "slug": "get-page-of-roles", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => `/ipam/roles?limit=100&offset=${state.currentParallelLoopIterations[\"get-paginated-roles\"] * 100}`", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-paginated-roles-end" } ], "outputMapper": "({ output }) => {\n const response = output;\n return response.results;\n }", "parentParallelLoopSlug": "get-paginated-roles" }, { "name": "merge-paginated-roles", "slug": "merge-paginated-roles", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ state }) => {\n const { results } = state.steps[\"get-first-page-of-roles\"].output;\n const paginatedResults = state.steps[\"get-paginated-roles\"].output.flat();\n return {\n roles: [...results, ...paginatedResults]\n };\n }" } ] } }, "Get RADIUS Account Password": { "id": "workflow_fBY1cE4WKFJf2ag2LarQQZ", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "username": { "type": "string", "description": "The username to fetch the password for" }, "instanceName": { "type": "string", "description": "The name of the FreeRADIUS instance" } }, "required": [ "username", "instanceName" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-password" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n const [passwordRow] = state.steps[\"get-password\"].output;\n if (!passwordRow || !passwordRow.value) {\n utils.fail(\"Password not found for the specified username\", \"PASSWORD_NOT_FOUND\");\n }\n return {\n value: passwordRow.value\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-password", "slug": "get-password", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-query", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "() => `SELECT rc.username, rc.attribute, rc.value FROM radcheck rc WHERE rc.username = ? AND rc.attribute LIKE '%-Password' AND rc.value IS NOT NULL LIMIT 1`", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => [state.input.username]", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output.results" } ] } }, "Import Network Site Prefixes": { "id": "workflow_wzxmp5TSvk3FgkNqUN73Bh", "description": null, "definition": { "trigger": { "skipExecutionMapper": "async ({ state, utils }) => {\n const { instanceId } = state.trigger.queryParameters;\n if (!instanceId) {\n return true;\n }\n const prefixSyncString = await utils.integration.datastore.getItem(`instances.${instanceId}.prefixSynchronization`);\n if (!prefixSyncString) {\n return true;\n }\n let prefixSync;\n try {\n prefixSync = JSON.parse(prefixSyncString);\n } catch (error) {\n return utils.fail(\"Failed to parse prefix synchronization settings\", \"INVALID_JSON_FORMAT\", { error });\n }\n if (!prefixSync.isEnabled) {\n return true;\n }\n if (state.input.model === \"site\") {\n return state.input.event !== \"deleted\";\n }\n if (prefixSync.ipamRoles.length === 0) {\n return false;\n }\n const prefixRoleId = state.input.data?.role?.id;\n if (prefixRoleId != null && prefixSync.ipamRoles.includes(String(prefixRoleId))) {\n return false;\n }\n return true;\n }", "inputJsonSchema": { "type": "object", "required": [ "model", "event", "data" ], "properties": { "model": { "type": "string", "enum": [ "site", "prefix" ] }, "event": { "type": "string", "enum": [ "created", "updated", "deleted" ] }, "data": { "type": "object", "properties": { "prefix": { "type": "string" }, "description": { "type": "string" }, "name": { "type": "string" }, "slug": { "type": "string" }, "role": { "type": [ "object", "null" ], "properties": { "id": { "type": "number" } } }, "site": { "type": [ "object", "null" ], "properties": { "id": { "type": "number" }, "name": { "type": "string" } } }, "scope_type": { "type": "string" }, "scope_id": { "type": "number" }, "scope": { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" } } } } }, "snapshots": { "type": "object", "properties": { "prechange": { "type": [ "object", "null" ], "properties": { "prefix": { "type": "string" }, "site": { "type": [ "number", "null" ] }, "scope_type": { "type": [ "number", "string", "null" ] }, "scope_id": { "type": [ "number", "null" ] }, "_site": { "type": [ "number", "null" ] } } }, "postchange": { "type": [ "object", "null" ], "properties": { "prefix": { "type": "string" }, "site": { "type": [ "number", "null" ] }, "scope_type": { "type": [ "number", "string", "null" ] }, "scope_id": { "type": [ "number", "null" ] }, "_site": { "type": [ "number", "null" ] } } } } } }, "allOf": [ { "if": { "properties": { "model": { "const": "prefix" } } }, "then": { "properties": { "data": { "required": [ "prefix" ] } } } }, { "if": { "properties": { "model": { "const": "site" } } }, "then": { "properties": { "data": { "required": [ "name" ] } } } }, { "if": { "properties": { "model": { "const": "prefix" }, "event": { "const": "updated" } } }, "then": { "required": [ "snapshots" ], "properties": { "snapshots": { "properties": { "prechange": { "required": [ "prefix" ] }, "postchange": { "required": [ "prefix" ] } } } } } } ] }, "nextSteps": [ { "slug": "map-input" } ], "type": "WEBHOOK" }, "steps": [ { "name": "map-input", "slug": "map-input", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceId } = state.trigger.queryParameters;\n if (!instanceId) {\n return utils.fail(\"Missing required instanceId query parameter\", \"NO_INSTANCE_ID\");\n }\n const prechange = state.input.snapshots?.prechange;\n const postchange = state.input.snapshots?.postchange;\n return {\n instanceId,\n instanceName: await utils.integration.datastore.getItem(`instances.${instanceId}.name`),\n prechangeSite: prechange?.site ?? prechange?._site ?? null,\n postchangeSite: postchange?.site ?? postchange?._site ?? null\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-prefix-synchronization-settings" } ], "outputMapper": "({ output }) => output" }, { "name": "get-prefix-synchronization-settings", "slug": "get-prefix-synchronization-settings", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-get-prefix-synchronization-settings", "inputs": [ { "type": "FUNCTION", "value": "async ({ state }) => state.steps[\"map-input\"].output.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "has-prefix-site-changed" } ], "outputMapper": "({ output, utils }) => {\n if (!output.isEnabled) {\n return utils.fail(\"NetBox prefix synchronization is disabled for this instance.\", \"PREFIX_SYNCHRONIZATION_DISABLED\");\n }\n return output;\n }" }, { "name": "has-prefix-site-changed", "slug": "has-prefix-site-changed", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => {\n const { prechangeSite, postchangeSite } = state.steps[\"map-input\"].output;\n return state.input.model === \"prefix\" && state.input.event === \"updated\" && prechangeSite !== null && prechangeSite !== postchangeSite;\n }", "thenSteps": [ { "slug": "fetch-previous-site" } ], "elseSteps": [ { "slug": "get-network-sites" } ] }, { "name": "fetch-previous-site", "slug": "fetch-previous-site", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => `/dcim/sites/${state.steps[\"map-input\"].output.prechangeSite}`", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"map-input\"].output.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-network-sites" } ], "outputMapper": "({ output }) => output" }, { "name": "get-network-sites", "slug": "get-network-sites", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n query NetworkSites($filters: NetworkSiteFilters, $firstIpBlocks: Int!) {\n networkSites(filters: $filters) {\n totalCount\n edges {\n node {\n id\n name\n ipBlocks(first: $firstIpBlocks) {\n totalCount\n nodes {\n id\n block\n description\n integrationId\n }\n }\n }\n }\n }\n }`", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { site, scope, name } = state.input.data;\n const fetchedPreviousSite = \"fetch-previous-site\" in state.steps ? state.steps[\"fetch-previous-site\"]?.output : null;\n const siteNames = [site?.name, scope?.name, fetchedPreviousSite?.name];\n if (state.input.model === \"site\") {\n siteNames.push(name);\n }\n return {\n filters: {\n name: {\n in: siteNames.filter((name2) => !!name2)\n }\n },\n firstIpBlocks: 250\n };\n }", "key": "variables" } ], "nextSteps": [ { "slug": "prepare-mutation-payloads" } ], "outputMapper": "({ output }) => {\n const response = output.response;\n return {\n networkSites: response.networkSites.edges.map((edge) => edge.node)\n };\n }" }, { "name": "prepare-mutation-payloads", "slug": "prepare-mutation-payloads", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "execute-mutations" } ], "outputMapper": "async ({ state, utils }) => {\n const { model, event, data, snapshots } = state.input;\n const { ipamRoles } = state.steps[\"get-prefix-synchronization-settings\"].output;\n const integrationId = await utils.integration.datastore.getItem(\"integrationId\");\n const fetchedPreviousSite = \"fetch-previous-site\" in state.steps ? state.steps[\"fetch-previous-site\"]?.output : null;\n const gaiiaNetworkSites = state.steps[\"get-network-sites\"].output.networkSites;\n const provisioningServiceMutations = [];\n if (model === \"prefix\" && (ipamRoles.length === 0 || data.role?.id != null && ipamRoles.includes(String(data.role.id)))) {\n const { prefix, site, scope, description } = data;\n const networkSite = site ?? scope;\n const newNetworkSite = gaiiaNetworkSites.find((ns) => ns.name === networkSite?.name);\n const findIpBlock = (networkSite2, block) => networkSite2?.ipBlocks.nodes.find((ipBlock) => ipBlock.block === block);\n switch (event) {\n case \"updated\": {\n const prechangeSite = fetchedPreviousSite ?? networkSite;\n const prechangePrefix = snapshots?.prechange?.prefix || prefix;\n const previousNetworkSite = gaiiaNetworkSites.find((ns) => ns.name === prechangeSite?.name);\n const oldGaiiaIpBlock = findIpBlock(previousNetworkSite, prechangePrefix);\n const isDifferentSite = previousNetworkSite?.id !== newNetworkSite?.id;\n const wasCreatedByWorkflow = oldGaiiaIpBlock?.integrationId === integrationId;\n const existsOnNewSite = !!findIpBlock(newNetworkSite, prefix);\n if (isDifferentSite && oldGaiiaIpBlock && wasCreatedByWorkflow) {\n provisioningServiceMutations.push({\n operation: \"remove\",\n variables: {\n input: {\n networkSiteId: previousNetworkSite?.id,\n ipBlockIds: [oldGaiiaIpBlock.id]\n }\n }\n });\n }\n if (newNetworkSite && (!oldGaiiaIpBlock || isDifferentSite || !wasCreatedByWorkflow) && !existsOnNewSite && !!prefix) {\n provisioningServiceMutations.push({\n operation: \"add\",\n variables: {\n input: {\n integrationId,\n networkSiteId: newNetworkSite.id,\n ipBlocks: [{ block: prefix, description }]\n }\n }\n });\n }\n if (oldGaiiaIpBlock && previousNetworkSite?.id === newNetworkSite?.id && wasCreatedByWorkflow && (oldGaiiaIpBlock.description !== description || oldGaiiaIpBlock.block !== prefix)) {\n provisioningServiceMutations.push({\n operation: \"update\",\n variables: {\n input: {\n id: oldGaiiaIpBlock.id,\n block: prefix,\n description\n }\n }\n });\n }\n break;\n }\n case \"created\": {\n if (!newNetworkSite) {\n break;\n }\n if (!!prefix && !findIpBlock(newNetworkSite, prefix)) {\n provisioningServiceMutations.push({\n operation: \"add\",\n variables: {\n input: {\n integrationId,\n networkSiteId: newNetworkSite.id,\n ipBlocks: [{ block: prefix, description }]\n }\n }\n });\n }\n break;\n }\n case \"deleted\": {\n const gaiiaIpBlock = findIpBlock(newNetworkSite, prefix);\n if (gaiiaIpBlock && gaiiaIpBlock.integrationId === integrationId) {\n provisioningServiceMutations.push({\n operation: \"remove\",\n variables: { input: { networkSiteId: newNetworkSite?.id, ipBlockIds: [gaiiaIpBlock.id] } }\n });\n }\n break;\n }\n }\n } else if (model === \"site\" && event === \"deleted\") {\n const networkSite = gaiiaNetworkSites.find((ns) => ns.name === data.name);\n if (networkSite) {\n const ipBlockIds = networkSite.ipBlocks.nodes.filter((ipBlock) => ipBlock.integrationId === integrationId).map((ipBlock) => ipBlock.id);\n provisioningServiceMutations.push({\n operation: \"remove\",\n variables: { input: { networkSiteId: networkSite.id, ipBlockIds } }\n });\n }\n }\n return provisioningServiceMutations;\n }" }, { "name": "execute-mutations", "slug": "execute-mutations", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"prepare-mutation-payloads\"].output" } ], "initialSteps": [ { "slug": "if-is-addition" } ], "nextSteps": [ { "slug": "end" } ] }, { "name": "if-is-addition", "slug": "if-is-addition", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => state.currentParallelLoopIterations[\"execute-mutations\"].operation === \"add\"", "thenSteps": [ { "slug": "add-ip-blocks-to-network-site" } ], "elseSteps": [ { "slug": "if-is-update" } ], "parentParallelLoopSlug": "execute-mutations" }, { "name": "add-ip-blocks-to-network-site", "slug": "add-ip-blocks-to-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n mutation AddIpBlocksToNetworkSite($input: AddIpBlocksToNetworkSiteInput!) {\n addIpBlocksToNetworkSite(input: $input) {\n networkSite {\n id\n }\n errors {\n code\n message\n }\n }\n }`", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"execute-mutations\"].variables", "key": "variables" } ], "nextSteps": [ { "slug": "execute-mutations-end" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.addIpBlocksToNetworkSite.errors?.length) {\n return utils.fail(\"Failed to add IP blocks to Network Site.\", \"ADD_IP_BLOCKS_FAILED\", response.addIpBlocksToNetworkSite.errors);\n }\n return response.addIpBlocksToNetworkSite.networkSite;\n }", "parentParallelLoopSlug": "execute-mutations" }, { "name": "if-is-update", "slug": "if-is-update", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => state.currentParallelLoopIterations[\"execute-mutations\"].operation === \"update\"", "thenSteps": [ { "slug": "update-ip-block-on-network-site" } ], "elseSteps": [ { "slug": "remove-ip-blocks-from-network-site" } ], "parentParallelLoopSlug": "execute-mutations" }, { "name": "update-ip-block-on-network-site", "slug": "update-ip-block-on-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n mutation UpdateNetworkSiteIpBlock($input: UpdateNetworkSiteIpBlockInput!) {\n updateNetworkSiteIpBlock(input: $input) {\n networkSiteIpBlock {\n id\n }\n errors {\n code\n message\n }\n }\n }`", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"execute-mutations\"].variables", "key": "variables" } ], "nextSteps": [ { "slug": "execute-mutations-end" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.updateNetworkSiteIpBlock.errors?.length) {\n return utils.fail(\"Failed to update IP blocks on Network Site.\", \"UPDATE_IP_BLOCKS_FAILED\", response.updateNetworkSiteIpBlock.errors);\n }\n return response.updateNetworkSiteIpBlock.networkSiteIpBlock;\n }", "parentParallelLoopSlug": "execute-mutations" }, { "name": "remove-ip-blocks-from-network-site", "slug": "remove-ip-blocks-from-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n mutation removeIpBlockFromNetworkSite($input: RemoveIpBlocksFromNetworkSiteInput!) {\n removeIpBlocksFromNetworkSite(input: $input) {\n networkSite {\n id\n }\n errors {\n code\n message\n }\n }\n }`", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"execute-mutations\"].variables", "key": "variables" } ], "nextSteps": [ { "slug": "execute-mutations-end" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.removeIpBlocksFromNetworkSite.errors?.length) {\n return utils.fail(\"Failed to remove IP blocks from Network Site.\", \"REMOVE_IP_BLOCKS_FAILED\", response.removeIpBlocksFromNetworkSite.errors);\n }\n return response.removeIpBlocksFromNetworkSite.networkSite;\n }", "parentParallelLoopSlug": "execute-mutations" } ] } }, "Verona Networks speed modification": { "id": "workflow_36ncWKmYofXn6q1bpm1HJp", "description": null, "definition": { "steps": [ { "name": "Find gaiia account", "nextSteps": [ { "slug": "find-account-cpes" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "find-gaiia-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaSpeedModificationWorkflowAccountQuery($id: ID!) {\n account(id: $id) {\n gaiiaId\n activeIpAddresses {\n ipAddress\n macAddress\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.accountId })" } ], "outputMapper": "({ output }) => {\n const { account } = output.response;\n if (account.activeIpAddresses.length === 0) {\n workflow.fail(`No active IP address associated to this account.`, 'NO_ACTIVE_IP_ADDRESS');\n }\n return account;\n}" }, { "name": "Find account CPEs", "nextSteps": [ { "slug": "find-new-gaiia-product" } ], "nodeSlug": "find-account-cpes", "slug": "find-account-cpes", "type": "GAIIA_FIND_ACCOUNT_CPES", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Find new gaiia product", "nextSteps": [ { "slug": "preseem-update-service" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "find-new-gaiia-product", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query ProductByIdWorkflowQuery($id: String!) {\n productById(id: $id) {\n slug\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.input.productId })" } ] }, { "name": "Preseem update service", "nextSteps": [ { "slug": "end" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-update-service", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/services/${state.steps['find-gaiia-account'].output.gaiiaId}`" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, activeIpAddresses } = state.steps['find-gaiia-account'].output;\n const { slug } = state.steps['find-new-gaiia-product'].output.response.productById;\n const cpes = state.steps['find-account-cpes'].output.accountCpe;\n const macAddresses = [\n ...new Set([...activeIpAddresses.map(({ macAddress }) => macAddress), ...cpes.map(({ macAddress }) => macAddress)])\n ];\n return {\n id: String(gaiiaId),\n attachments: macAddresses.map((macAddress) => ({ cpe_mac: macAddress.toLowerCase() })),\n account: String(gaiiaId),\n package: slug\n };\n }" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-gaiia-account" } ], "type": "MANUAL", "objectType": "ACCOUNT", "inputJsonSchema": { "type": "object", "required": [ "productId" ], "properties": { "productId": { "type": "string" } } } } } }, "Add RADIUS Group": { "id": "workflow_4o9WMZDJjipHE6SpdWiy9L", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The name of the FreeRADIUS instance" }, "groupName": { "type": "string", "description": "The name of the RADIUS group" }, "priority": { "type": "number", "description": "The priority of the group" }, "mappingRule": { "type": "string", "enum": [ "ALL", "MATCH_PRODUCT", "INACTIVE", "SUSPENDED", "ACTIVE" ], "description": "The mapping rule for automatic group assignment" }, "mappedProductIds": { "type": "array", "items": { "type": "string" }, "description": "Product IDs for MATCH_PRODUCT mapping rule" }, "attributes": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "operator": { "type": "string" }, "value": { "type": "string" } } }, "description": "RADIUS attributes for the group" }, "fallThrough": { "type": "boolean", "description": "Whether to continue evaluating other groups" } }, "required": [ "instanceName", "groupName", "priority", "mappingRule", "mappedProductIds", "attributes", "fallThrough" ] }, "nextSteps": [ { "slug": "validate-group-input" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n utils.fail(state.steps[\"handle-error\"].output.message, state.steps[\"handle-error\"].output.code, state.steps[\"handle-error\"].output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "validate-group-input", "slug": "validate-group-input", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state, utils }) => {\n const { groupName, priority, attributes, mappingRule, mappedProductIds } = state.input;\n if (groupName.length > 64) {\n return utils.fail(\"Group name is too long. Max length is 64\", \"GROUP_NAME_TOO_LONG\");\n }\n if (priority < 0) {\n return utils.fail(\"Priority must be greater than or equal to 0\", \"PRIORITY_INVALID\");\n }\n if (mappingRule === \"MATCH_PRODUCT\" && (!mappedProductIds || mappedProductIds.length === 0)) {\n return utils.fail(\"MATCH_PRODUCT mapping rule requires at least one mapped product ID\", \"MAPPING_RULE_INVALID\");\n }\n const RADUSERGROUP_ATTRIBUTE_NAME_MAX_LENGTH = 64;\n const RADUSERGROUP_ATTRIBUTE_OPERATOR_MAX_LENGTH = 2;\n const RADUSERGROUP_ATTRIBUTE_VALUE_MAX_LENGTH = 253;\n for (const attribute of attributes) {\n if (!attribute.name || attribute.name.length > RADUSERGROUP_ATTRIBUTE_NAME_MAX_LENGTH) {\n return utils.fail(`Attribute name is too long. Max length is ${RADUSERGROUP_ATTRIBUTE_NAME_MAX_LENGTH}`, \"ATTRIBUTE_NAME_TOO_LONG\");\n }\n if (!attribute.operator || attribute.operator.length > RADUSERGROUP_ATTRIBUTE_OPERATOR_MAX_LENGTH) {\n return utils.fail(`Attribute operator is too long. Max length is ${RADUSERGROUP_ATTRIBUTE_OPERATOR_MAX_LENGTH}`, \"ATTRIBUTE_OPERATOR_TOO_LONG\");\n }\n if (!attribute.value || attribute.value.length > RADUSERGROUP_ATTRIBUTE_VALUE_MAX_LENGTH) {\n return utils.fail(`Attribute value is too long. Max length is ${RADUSERGROUP_ATTRIBUTE_VALUE_MAX_LENGTH}`, \"ATTRIBUTE_VALUE_TOO_LONG\");\n }\n }\n return {\n validated: true\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "insert-group-with-attributes" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "insert-group-with-attributes", "slug": "insert-group-with-attributes", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { groupName, attributes, fallThrough } = state.input;\n const queries = [];\n for (const attribute of attributes) {\n queries.push({\n query: \"INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [groupName, attribute.name, attribute.operator, attribute.value]\n });\n }\n queries.push({\n query: \"INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [groupName, \"Fall-Through\", \"=\", fallThrough ? \"Yes\" : \"No\"]\n });\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "store-in-datastore" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => ({\n created: true,\n affectedRows: output.results?.length || 0\n })" }, { "name": "store-in-datastore", "slug": "store-in-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { instanceName, groupName, priority, mappingRule, mappedProductIds } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n const existingData = await utils.integration.datastore.getItem(`instances.${instanceId}.groups`);\n const groups = existingData ? JSON.parse(existingData) : [];\n const groupData = {\n groupName,\n mappedProductIds: mappedProductIds || [],\n mappingRule,\n priority\n };\n const updatedGroups = [...groups, groupData];\n return [\n {\n key: `instances.${instanceId}.groups`,\n value: JSON.stringify(updatedGroups)\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "handle-error", "errorCodes": [ "*" ] }, { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"store-in-datastore\"]?.error || steps[\"insert-group-with-attributes\"]?.error || steps[\"validate-group-input\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Import Network Site Prefixes - Full Sync": { "id": "workflow_wx3LgtSVmosH2DtrC41B5q", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "get-prefix-synchronization-settings" } ], "type": "MANUAL" }, "steps": [ { "name": "get-prefix-synchronization-settings", "slug": "get-prefix-synchronization-settings", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-get-prefix-synchronization-settings", "inputs": [ { "type": "FUNCTION", "value": "async ({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-first-page-of-prefixes" } ], "outputMapper": "({ output, utils }) => {\n if (!output.isEnabled) {\n return utils.fail(\"NetBox prefix synchronization is disabled for this instance.\", \"PREFIX_SYNCHRONIZATION_DISABLED\");\n }\n return output;\n }" }, { "name": "get-first-page-of-prefixes", "slug": "get-first-page-of-prefixes", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { ipamRoles } = state.steps[\"get-prefix-synchronization-settings\"].output;\n const filter = ipamRoles.length > 0 ? `&role_id=${ipamRoles.join(\"&role_id=\")}` : \"\";\n return `/ipam/prefixes?site__n=null&limit=100&offset=0${filter}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-paginated-prefixes" } ], "outputMapper": "({ output }) => {\n const response = output;\n const numberOfRemainingPages = Math.ceil(response.count / 100) - 1;\n return {\n pagesToFetch: Array.from({ length: numberOfRemainingPages }, (_, index) => index + 1),\n results: response.results\n };\n }" }, { "name": "get-paginated-prefixes", "slug": "get-paginated-prefixes", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-first-page-of-prefixes\"].output.pagesToFetch" } ], "initialSteps": [ { "slug": "get-page-of-prefixes" } ], "nextSteps": [ { "slug": "merge-paginated-prefixes" } ], "outputMapper": "({ state }) => state.steps[\"get-page-of-prefixes\"].output" }, { "name": "get-page-of-prefixes", "slug": "get-page-of-prefixes", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { ipamRoles } = state.steps[\"get-prefix-synchronization-settings\"].output;\n const filter = ipamRoles?.length > 0 ? `&role_id=${ipamRoles.join(\"&role_id=\")}` : \"\";\n return `/ipam/prefixes?site__n=null&limit=100&offset=${state.currentParallelLoopIterations[\"get-paginated-prefixes\"] * 100}${filter}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-paginated-prefixes-end" } ], "outputMapper": "({ output }) => {\n const response = output;\n return response.results;\n }", "parentParallelLoopSlug": "get-paginated-prefixes" }, { "name": "merge-paginated-prefixes", "slug": "merge-paginated-prefixes", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "get-all-network-sites" } ], "outputMapper": "({ state }) => {\n const { results } = state.steps[\"get-first-page-of-prefixes\"].output;\n const paginatedResults = state.steps[\"get-paginated-prefixes\"].output.flat();\n return {\n prefixes: [...results, ...paginatedResults]\n };\n }" }, { "name": "get-all-network-sites", "slug": "get-all-network-sites", "nodeSlug": "while-loop", "type": "WHILE_LOOP", "initialSteps": [ { "slug": "get-network-sites" } ], "nextSteps": [ { "slug": "map-prefixes-to-network-sites" } ], "condition": "({ state }) => {\n const whileLoop = state.currentWhileLoopIterations[\"get-all-network-sites\"];\n return whileLoop.isFirstIteration || whileLoop.previousIteration.steps[\"get-network-sites\"].output.networkSites.pageInfo.hasNextPage;\n }", "iterationOutputMapper": "({ state }) => state.steps[\"get-network-sites\"].output", "outputMapper": "({ output }) => {\n const responses = output;\n return {\n networkSites: responses.flatMap((response) => response.networkSites.edges.map((edge) => edge.node))\n };\n }" }, { "name": "get-network-sites", "slug": "get-network-sites", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n query NetworkSites($first: Int, $after: String, $filters: NetworkSiteFilters, $firstIpBlocks: Int) {\n networkSites(first: $first, after: $after, filters: $filters) {\n totalCount\n edges {\n node {\n id\n name\n ipBlocks(first: $firstIpBlocks) {\n totalCount\n nodes {\n id\n block\n description\n integrationId\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }`", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => {\n const previousResponse = state.currentWhileLoopIterations[\"get-all-network-sites\"].previousIteration?.output;\n return {\n first: 250,\n after: previousResponse?.networkSites.pageInfo.endCursor,\n firstIpBlocks: 250\n };\n }", "key": "variables" } ], "nextSteps": [ { "slug": "get-all-network-sites-end" } ], "outputMapper": "({ output }) => {\n const response = output.response;\n return response;\n }", "parentWhileLoopSlug": "get-all-network-sites" }, { "name": "map-prefixes-to-network-sites", "slug": "map-prefixes-to-network-sites", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "add-ip-blocks-to-network-sites" } ], "outputMapper": "async ({ state, utils }) => {\n const { prefixes } = state.steps[\"merge-paginated-prefixes\"].output;\n const { networkSites } = state.steps[\"get-all-network-sites\"].output;\n const integrationId = await utils.integration.datastore.getItem(\"integrationId\");\n return networkSites.map((site) => {\n const existingBlocksOnSite = new Set(site.ipBlocks.nodes.map((block) => block.block));\n return {\n networkSiteId: site.id,\n integrationId,\n ipBlocks: prefixes.filter(\n (prefix) => (\n // support for both v4.1.x (site.name) and v4.2.x (scope.name) of NetBox\n (prefix.site?.name === site.name || prefix.scope?.name === site.name) && !existingBlocksOnSite.has(prefix.prefix)\n )\n ).map((block) => ({\n block: block.prefix,\n description: block.description\n }))\n };\n }).filter((site) => site.ipBlocks.length > 0);\n }" }, { "name": "add-ip-blocks-to-network-sites", "slug": "add-ip-blocks-to-network-sites", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"map-prefixes-to-network-sites\"].output" } ], "initialSteps": [ { "slug": "add-ip-blocks-to-network-site" } ], "nextSteps": [ { "slug": "end" } ] }, { "name": "add-ip-blocks-to-network-site", "slug": "add-ip-blocks-to-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n mutation AddIpBlocksToNetworkSite($input: AddIpBlocksToNetworkSiteInput!) {\n addIpBlocksToNetworkSite(input: $input) {\n networkSite {\n id\n }\n errors {\n code\n message\n }\n }\n }`", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => ({\n input: state.currentParallelLoopIterations[\"add-ip-blocks-to-network-sites\"]\n })", "key": "variables" } ], "nextSteps": [ { "slug": "add-ip-blocks-to-network-sites-end" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.addIpBlocksToNetworkSite.errors?.length) {\n return utils.fail(\"Failed to add IP blocks to Network Site.\", \"ADD_IP_BLOCKS_FAILED\", response.addIpBlocksToNetworkSite.errors);\n }\n return response.addIpBlocksToNetworkSite.networkSite;\n }", "parentParallelLoopSlug": "add-ip-blocks-to-network-sites" } ] } }, "Verona Networks suspension": { "id": "workflow_bcdU9jvaxRXbYJW2S7DHHH", "description": null, "definition": { "steps": [ { "name": "Find gaiia account", "nextSteps": [ { "slug": "find-account-cpes" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "find-gaiia-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaSuspensionWorkflowAccountQuery($id: ID!) {\n account(id: $id) {\n gaiiaId\n activeIpAddresses {\n ipAddress\n macAddress\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.accountId })" } ], "outputMapper": "({ output }) => {\n const { account } = output.response;\n if (account.activeIpAddresses.length === 0) {\n workflow.fail(`No active IP address associated to this account.`, 'NO_ACTIVE_IP_ADDRESS');\n }\n return account;\n}" }, { "name": "Find account CPEs", "nextSteps": [ { "slug": "preseem-update-service" } ], "nodeSlug": "find-account-cpes", "slug": "find-account-cpes", "type": "GAIIA_FIND_ACCOUNT_CPES", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Preseem update service", "nextSteps": [ { "slug": "end" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-update-service", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/services/${state.steps['find-gaiia-account'].output.gaiiaId}`" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, activeIpAddresses } = state.steps['find-gaiia-account'].output;\n const cpes = state.steps['find-account-cpes'].output.accountCpe;\n const macAddresses = [\n ...new Set([...activeIpAddresses.map(({ macAddress }) => macAddress), ...cpes.map(({ macAddress }) => macAddress)])\n ];\n return {\n id: String(gaiiaId),\n attachments: macAddresses.map((macAddress) => ({ cpe_mac: macAddress.toLowerCase() })),\n account: String(gaiiaId),\n package: 'suspended'\n };\n }" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-gaiia-account" } ], "type": "MANUAL", "objectType": "ACCOUNT" } } }, "Update RADIUS Group": { "id": "workflow_jxJRrNcmd6DuZVX9EBN8Q9", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The name of the FreeRADIUS instance" }, "oldGroupName": { "type": "string", "description": "The current name of the RADIUS group" }, "groupName": { "type": "string", "description": "The new name of the RADIUS group" }, "priority": { "type": "number", "description": "The priority of the group" }, "mappingRule": { "type": "string", "enum": [ "ALL", "MATCH_PRODUCT", "INACTIVE", "SUSPENDED", "ACTIVE" ], "description": "The mapping rule for automatic group assignment" }, "mappedProductIds": { "type": "array", "items": { "type": "string" }, "description": "Product IDs for MATCH_PRODUCT mapping rule" }, "attributes": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "operator": { "type": "string" }, "value": { "type": "string" } }, "required": [ "name", "operator", "value" ] }, "description": "RADIUS attributes for the group" }, "fallThrough": { "type": "boolean", "description": "Whether to continue evaluating other groups" } }, "required": [ "instanceName", "oldGroupName", "groupName", "priority", "mappingRule", "mappedProductIds", "attributes", "fallThrough" ] }, "nextSteps": [ { "slug": "validate-group-input" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n utils.fail(state.steps[\"handle-error\"].output.message, state.steps[\"handle-error\"].output.code, state.steps[\"handle-error\"].output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "validate-group-input", "slug": "validate-group-input", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName, oldGroupName, groupName, priority, attributes, mappingRule, mappedProductIds } = state.input;\n if (groupName.length > 64) {\n return utils.fail(\"Group name is too long. Max length is 64\", \"GROUP_NAME_TOO_LONG\");\n }\n if (priority < 0) {\n return utils.fail(\"Priority must be greater than or equal to 0\", \"PRIORITY_INVALID\");\n }\n if (mappingRule === \"MATCH_PRODUCT\" && (!mappedProductIds || mappedProductIds.length === 0)) {\n return utils.fail(\"MATCH_PRODUCT mapping rule requires at least one mapped product ID\", \"MAPPING_RULE_INVALID\");\n }\n const RADUSERGROUP_ATTRIBUTE_NAME_MAX_LENGTH = 64;\n const RADUSERGROUP_ATTRIBUTE_OPERATOR_MAX_LENGTH = 2;\n const RADUSERGROUP_ATTRIBUTE_VALUE_MAX_LENGTH = 253;\n for (const attribute of attributes) {\n if (!attribute.name || attribute.name.length > RADUSERGROUP_ATTRIBUTE_NAME_MAX_LENGTH) {\n return utils.fail(`Attribute name is too long. Max length is ${RADUSERGROUP_ATTRIBUTE_NAME_MAX_LENGTH}`, \"ATTRIBUTE_NAME_TOO_LONG\");\n }\n if (!attribute.operator || attribute.operator.length > RADUSERGROUP_ATTRIBUTE_OPERATOR_MAX_LENGTH) {\n return utils.fail(`Attribute operator is too long. Max length is ${RADUSERGROUP_ATTRIBUTE_OPERATOR_MAX_LENGTH}`, \"ATTRIBUTE_OPERATOR_TOO_LONG\");\n }\n if (!attribute.value || attribute.value.length > RADUSERGROUP_ATTRIBUTE_VALUE_MAX_LENGTH) {\n return utils.fail(`Attribute value is too long. Max length is ${RADUSERGROUP_ATTRIBUTE_VALUE_MAX_LENGTH}`, \"ATTRIBUTE_VALUE_TOO_LONG\");\n }\n }\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n const existingData = await utils.integration.datastore.getItem(`instances.${instanceId}.groups`);\n if (!existingData) {\n return utils.fail(\"Groups not found in datastore\", \"GROUPS_NOT_FOUND\");\n }\n const groups = JSON.parse(existingData);\n const group = groups.find((g) => g.groupName === oldGroupName);\n if (!group) {\n return utils.fail(\"Group not found in datastore\", \"GROUP_NOT_FOUND\");\n }\n return {\n group: {\n groupName: group.groupName,\n mappedProductIds: group.mappedProductIds,\n mappingRule: group.mappingRule,\n priority: group.priority\n },\n groups,\n instanceId,\n validated: true\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "update-group-attributes-only" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "update-group-attributes-only", "slug": "update-group-attributes-only", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { oldGroupName, groupName, attributes, fallThrough } = state.input;\n const queries = [];\n queries.push({\n query: \"DELETE FROM radgroupreply WHERE groupname = ?\",\n values: [oldGroupName]\n });\n if (oldGroupName !== groupName) {\n queries.push({\n query: \"UPDATE radusergroup SET groupname = ? WHERE groupname = ?\",\n values: [groupName, oldGroupName]\n });\n }\n for (const attribute of attributes) {\n queries.push({\n query: \"INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [groupName, attribute.name, attribute.operator, attribute.value]\n });\n }\n queries.push({\n query: \"INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [groupName, \"Fall-Through\", \"=\", fallThrough === false ? \"No\" : \"Yes\"]\n });\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "update-datastore" } ], "outputMapper": "({ output }) => ({\n updated: true,\n renamed: false,\n affectedRows: output.results?.length || 0\n })" }, { "name": "update-datastore", "slug": "update-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => {\n const { groupName, priority, mappingRule, mappedProductIds } = state.input;\n const { instanceId, groups, group } = state.steps[\"validate-group-input\"].output;\n const updatedGroup = {\n ...group,\n groupName,\n priority,\n mappingRule,\n mappedProductIds\n };\n const updatedGroups = groups.map((g) => g.groupName === group.groupName ? updatedGroup : g);\n return [\n {\n key: `instances.${instanceId}.groups`,\n value: JSON.stringify(updatedGroups)\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"validate-group-input\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Create NetBox Prefix Update Webhook": { "id": "workflow_ioh3osUxKLwCZZZZBtvQyG", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "list-custom-fields" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => state.steps[\"create-event-rule\"].output", "type": "JSON" }, "steps": [ { "name": "list-custom-fields", "slug": "list-custom-fields", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/custom-fields/\"", "key": "path" }, { "type": "FUNCTION", "value": "() => ({\n name: [\"gaiia_tenant_id\", \"gaiia_instance_id\"]\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "check-tenant-id-field-exists" } ], "outputMapper": "({ output }) => {\n const results = output.results;\n return results.map((field) => field.name);\n }" }, { "name": "check-tenant-id-field-exists", "slug": "check-tenant-id-field-exists", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => state.steps[\"list-custom-fields\"].output.includes(\"gaiia_tenant_id\")", "thenSteps": [ { "slug": "check-instance-id-field-exists" } ], "elseSteps": [ { "slug": "create-tenant-id-custom-field" } ] }, { "name": "create-tenant-id-custom-field", "slug": "create-tenant-id-custom-field", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/custom-fields/\"", "key": "path" }, { "type": "FUNCTION", "value": "() => ({\n label: \"gaiia tenant ID\",\n name: \"gaiia_tenant_id\",\n type: \"text\",\n object_types: [\"extras.webhook\", \"extras.eventrule\"],\n required: false\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "check-instance-id-field-exists" } ], "outputMapper": "({ output }) => output" }, { "name": "check-instance-id-field-exists", "slug": "check-instance-id-field-exists", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => state.steps[\"list-custom-fields\"].output.includes(\"gaiia_instance_id\")", "thenSteps": [ { "slug": "get-webhook-workflow" } ], "elseSteps": [ { "slug": "create-instance-id-custom-field" } ] }, { "name": "create-instance-id-custom-field", "slug": "create-instance-id-custom-field", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/custom-fields/\"", "key": "path" }, { "type": "FUNCTION", "value": "() => ({\n label: \"gaiia instance ID\",\n name: \"gaiia_instance_id\",\n type: \"text\",\n object_types: [\"extras.webhook\", \"extras.eventrule\"],\n required: false\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-webhook-workflow" } ], "outputMapper": "({ output }) => output" }, { "name": "get-webhook-workflow", "slug": "get-webhook-workflow", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // -----------------\n // WorkflowsDocument\n // -----------------\n const WorkflowsDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"Workflows\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filter\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"WorkflowFilter\"}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"workflows\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"filter\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filter\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"latestDefinition\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"webhookUrl\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":168}};\n // ------------------------------------------------------------\n\n return WorkflowsDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "() => ({\n filter: {\n name: {\n equals: \"Import Network Site Prefixes\"\n }\n }\n })", "key": "variables" } ], "nextSteps": [ { "slug": "create-webhook" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.workflows.nodes.length === 0) {\n return utils.fail('No workflow found with the name \"Import Network Site Prefixes\"', \"NO_WORKFLOW_FOUND\");\n }\n if (response.workflows.nodes.length > 1) {\n return utils.fail('Multiple workflows found with the name \"Import Network Site Prefixes\"', \"MULTIPLE_WORKFLOWS_FOUND\");\n }\n const webhookUrl = response.workflows.nodes[0].latestDefinition?.webhookUrl;\n if (!webhookUrl) {\n return utils.fail('The workflow \"Import Network Site Prefixes\" does not have a webhook URL defined', \"NO_WEBHOOK_URL_DEFINED\");\n }\n return response.workflows.nodes[0];\n }" }, { "name": "create-webhook", "slug": "create-webhook", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/webhooks/\"", "key": "path" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n return {\n name: `${state.context.tenant.name} - Prefix Update Webhook`,\n payload_url: `${state.steps[\"get-webhook-workflow\"].output.latestDefinition?.webhookUrl}?instanceId=${instanceId}`,\n custom_fields: {\n gaiia_tenant_id: state.context.tenant.id,\n gaiia_instance_id: instanceId\n }\n };\n }", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "create-event-rule" } ], "outputMapper": "({ output }) => output" }, { "name": "create-event-rule", "slug": "create-event-rule", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/event-rules/\"", "key": "path" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => ({\n name: `${state.context.tenant.name} - Prefix Update Event Rule`,\n event_types: [\"object_updated\", \"object_created\", \"object_deleted\"],\n object_types: [\"dcim.site\", \"ipam.prefix\"],\n type_create: true,\n type_update: true,\n type_delete: true,\n action_type: \"webhook\",\n action_object_type: \"extras.webhook\",\n action_object_id: state.steps[\"create-webhook\"].output.id,\n custom_fields: {\n gaiia_tenant_id: state.context.tenant.id,\n gaiia_instance_id: await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`)\n }\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Update Preseem on IP Change": { "id": "workflow_3PWGkYj2un5VAQUGB3pamU", "description": null, "definition": { "steps": [ { "name": "Find inventory item", "slug": "find-inventory-item", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "outputMapper": "({ output }) => {\n const { account } = output.response.inventoryItem;\n if (!account) {\n workflow.fail('Inventory item is not assigned to an account.', 'NO_ACCOUNT');\n }\n return account;\n}", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query IpAssignmentWorkflowInventoryItemQuery($id: ID!) {\n inventoryItem(id: $id) {\n id\n account {\n id\n gaiiaId\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.input.objectId })" } ], "nextSteps": [ { "slug": "find-account-cpes" } ] }, { "name": "Find account CPEs", "slug": "find-account-cpes", "type": "GAIIA_FIND_ACCOUNT_CPES", "nodeSlug": "find-account-cpes", "allowSkipExecution": false, "inputs": [ { "key": "accountId", "type": "FUNCTION", "value": "({ state }) => state.steps['find-inventory-item'].output.id" } ], "nextSteps": [ { "slug": "find-account-internet-plans" } ] }, { "name": "Find account Internet plans", "slug": "find-account-internet-plans", "type": "GAIIA_FIND_ACCOUNT_INTERNET_PLANS", "nodeSlug": "find-account-internet-plans", "allowSkipExecution": false, "inputs": [ { "key": "accountId", "type": "FUNCTION", "value": "({ state }) => state.steps['find-inventory-item'].output.id" } ], "nextSteps": [ { "slug": "preseem-update-service" } ] }, { "name": "Preseem update service", "slug": "preseem-update-service", "type": "PRESEEM_REST_REQUEST", "nodeSlug": "preseem-rest-request", "allowSkipExecution": false, "inputs": [ { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId } = state.steps['find-inventory-item'].output;\n const [{ slug }] = state.steps['find-account-internet-plans'].output.internetPlans;\n const cpes = state.steps['find-account-cpes'].output.accountCpe;\n const eventMac = state.input.payload.macAddress;\n const macAddresses = [\n ...new Set([\n ...cpes.map(({ macAddress }) => macAddress),\n eventMac\n ])\n ];\n return {\n id: String(gaiiaId),\n attachments: macAddresses\n .filter(Boolean)\n .map((macAddress) => ({ cpe_mac: macAddress.toLowerCase() })),\n account: String(gaiiaId),\n package: slug\n };\n }" }, { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => /services/${state.steps['find-inventory-item'].output.gaiiaId}" } ], "nextSteps": [ { "slug": "end" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-inventory-item" } ], "type": "EVENT", "events": [ "inventory_item.ip_address_assigned", "inventory_item.ip_address_unassigned" ] } } }, "Delete RADIUS Group": { "id": "workflow_n3M2RXK57eKaiZMsrHfEtD", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The name of the FreeRADIUS instance" }, "groupName": { "type": "string", "description": "The name of the RADIUS group to delete" } }, "required": [ "instanceName", "groupName" ] }, "nextSteps": [ { "slug": "validate-instance-name" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n const error = state.steps[\"handle-error\"].output;\n utils.fail(error.message, error.code, error.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "validate-instance-name", "slug": "validate-instance-name", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n return {\n instanceId,\n instanceName\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "delete-group-transaction" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "delete-group-transaction", "slug": "delete-group-transaction", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { groupName } = state.input;\n return [\n {\n query: \"DELETE FROM radusergroup WHERE groupname = ?\",\n values: [groupName]\n },\n {\n query: \"DELETE FROM radgroupreply WHERE groupname = ?\",\n values: [groupName]\n }\n ];\n }", "key": "queries" } ], "nextSteps": [ { "slug": "delete-from-datastore" } ], "outputMapper": "({ output }) => ({\n deleted: true,\n affectedRows: output.results?.length || 0\n })" }, { "name": "delete-from-datastore", "slug": "delete-from-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { groupName } = state.input;\n const { instanceId } = state.steps[\"validate-instance-name\"].output;\n const existingData = await utils.integration.datastore.getItem(`instances.${instanceId}.groups`);\n const groups = JSON.parse(existingData);\n const updatedGroups = groups.filter((g) => g.groupName !== groupName);\n return [\n {\n key: `instances.${instanceId}.groups`,\n value: JSON.stringify(updatedGroups)\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"validate-instance-name\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Remove NetBox Prefix Update Webhook": { "id": "workflow_w7atgmGFDwaEnthsSg7WGk", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "list-event-rules" } ], "type": "MANUAL" }, "steps": [ { "name": "list-event-rules", "slug": "list-event-rules", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n return `/extras/event-rules?cf_gaiia_tenant_id=${state.context.tenant.id}&cf_gaiia_instance_id=${instanceId}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-event-rules" } ], "outputMapper": "({ output }) => {\n const response = output;\n return response;\n }" }, { "name": "delete-event-rules", "slug": "delete-event-rules", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const eventRuleName = `${state.context.tenant.name} - Prefix Update Event Rule`;\n return state.steps[\"list-event-rules\"].output.results.filter((rule) => rule.name === eventRuleName);\n }" } ], "initialSteps": [ { "slug": "delete-event-rule" } ], "nextSteps": [ { "slug": "list-webhooks" } ] }, { "name": "delete-event-rule", "slug": "delete-event-rule", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"DELETE\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => `extras/event-rules/${state.currentParallelLoopIterations[\"delete-event-rules\"].id}`", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-event-rules-end" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "delete-event-rules" }, { "name": "list-webhooks", "slug": "list-webhooks", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n return `/extras/webhooks?cf_gaiia_tenant_id=${state.context.tenant.id}&cf_gaiia_instance_id=${instanceId}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-webhooks" } ], "outputMapper": "({ output }) => {\n const response = output;\n return response;\n }" }, { "name": "delete-webhooks", "slug": "delete-webhooks", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const webhookName = `${state.context.tenant.name} - Prefix Update Webhook`;\n return state.steps[\"list-webhooks\"].output.results.filter((webhook) => webhook.name === webhookName);\n }" } ], "initialSteps": [ { "slug": "delete-webhook" } ], "nextSteps": [ { "slug": "end" } ] }, { "name": "delete-webhook", "slug": "delete-webhook", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"DELETE\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => `extras/webhooks/${state.currentParallelLoopIterations[\"delete-webhooks\"].id}`", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-webhooks-end" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "delete-webhooks" } ] } }, "FreeRADIUS MySQL Query": { "id": "workflow_aG62TB7ci2bVVEKkvv6vpp", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The name of the FreeRADIUS instance to use. If not provided, uses the first instance." }, "query": { "type": "string", "description": "The SQL query to execute" }, "values": { "type": "array", "description": "The values to use in the prepared statement", "items": { "type": [ "string", "number", "boolean", "null" ] } } }, "required": [ "query" ], "additionalProperties": false }, "nextSteps": [ { "slug": "fetch-instance-database-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => state.steps[\"execute-mysql-query\"].output", "type": "JSON" }, "steps": [ { "name": "fetch-instance-database-config", "slug": "fetch-instance-database-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName } = state.input;\n let instanceId;\n if (!instanceName) {\n const instancesId = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesId) {\n return utils.fail(\"No instances found in datastore\", \"NO_INSTANCES_FOUND\");\n }\n const instanceIds = instancesId.split(\",\");\n if (instanceIds.length === 0) {\n return utils.fail(\"No instance ids found\", \"NO_INSTANCE_IDS_FOUND\");\n }\n instanceId = instanceIds[0]?.trim();\n } else {\n instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Invalid instance name provided\", \"INVALID_INSTANCE_NAME\");\n }\n }\n const [databaseHost, databasePort, databaseName, databaseUsername, databasePassword] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${instanceId}.databaseHost`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databasePort`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databaseName`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databaseUsername`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databasePassword`)\n ]);\n if (!databaseHost || !databasePort || !databaseName || !databaseUsername || !databasePassword) {\n return utils.fail(\"Incomplete instance configuration\", \"INCOMPLETE_INSTANCE_CONFIG\");\n }\n return {\n databaseHost,\n databasePort: Number(databasePort),\n databaseName,\n databaseUsername,\n databasePassword\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "execute-mysql-query" } ], "outputMapper": "({ output }) => output" }, { "name": "execute-mysql-query", "slug": "execute-mysql-query", "type": "MYSQL_QUERY", "nodeSlug": "mysql-query", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databaseHost", "key": "host" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databasePort", "key": "port" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databaseName", "key": "database" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databaseUsername", "key": "user" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databasePassword", "key": "password" }, { "type": "FUNCTION", "value": "({ state }) => state.input.query", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => state.input.values", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Update Preseem on PPPoE IP Change": { "id": "workflow_aEcWxEsubpuSebPwajqseP", "description": null, "definition": { "steps": [ { "name": "Find gaiia account", "slug": "find-gaiia-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "outputMapper": "({ output }) => output.response.account", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query AccountQuery($id: ID!) {\n account(id: $id) {\n gaiiaId\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.accountId })" } ], "nextSteps": [ { "slug": "find-account-cpes" } ] }, { "name": "Find account CPEs", "slug": "find-account-cpes", "type": "GAIIA_FIND_ACCOUNT_CPES", "nodeSlug": "find-account-cpes", "allowSkipExecution": false, "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ], "nextSteps": [ { "slug": "find-account-internet-plans" } ] }, { "name": "Find account Internet plans", "slug": "find-account-internet-plans", "type": "GAIIA_FIND_ACCOUNT_INTERNET_PLANS", "nodeSlug": "find-account-internet-plans", "allowSkipExecution": false, "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ], "nextSteps": [ { "slug": "preseem-update-service" } ] }, { "name": "Preseem update service", "slug": "preseem-update-service", "type": "PRESEEM_REST_REQUEST", "nodeSlug": "preseem-rest-request", "allowSkipExecution": false, "inputs": [ { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId } = state.steps['find-gaiia-account'].output;\n const [{ slug }] = state.steps['find-account-internet-plans'].output.internetPlans;\n const cpes = state.steps['find-account-cpes'].output.accountCpe;\n const macAddresses = [\n ...new Set([\n ...cpes.map(({ macAddress }) => macAddress),\n state.macAddress\n ])\n ];\n return {\n id: String(gaiiaId),\n attachments: macAddresses\n .filter(Boolean)\n .map((m) => ({ cpe_mac: m.toLowerCase() })),\n account: String(gaiiaId),\n package: slug\n };\n}" }, { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => /services/${state.steps['find-gaiia-account'].output.gaiiaId}" } ], "nextSteps": [ { "slug": "end" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-gaiia-account" } ], "type": "MANUAL", "inputJsonSchema": { "type": "object", "required": [ "accountId", "macAddress" ], "properties": { "accountId": { "type": "string" }, "macAddress": { "type": "string" } } }, "objectType": "ACCOUNT" } } }, "freeRADIUS IP Sync": { "id": "workflow_v8dpzZ88Pj1roMCmF9YFwT", "description": "This workflow will walk the RADIUS radacct table and identify active/inactive sessions and learn the Framed IP and sync to the corresponding gaiia Account.", "definition": { "steps": [ { "name": "Assign IP address to devices", "slug": "assign-ip-address-to-devices", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "parentParallelLoopSlug": "assign-ips-to-macs", "allowSkipExecution": false, "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation UpsertMacAddressIpAddressAssignations($input: UpsertMacAddressIpAddressAssignationsInput!) {\n upsertMacAddressIpAddressAssignations(input: $input) {\n assignations {\n macAddress\n ipAddress\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => {\n const assignation = state.currentParallelLoopIterations['assign-ips-to-macs'];\n const rawMac = assignation.callingstationid;\n // Remove \"1:\" prefix if present. Example \"1:00:31:92:47:71:dc\" -> \"00:31:92:47:71:dc\"\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n // Pad each segment to 2 digits. Example \"0:31:92:47:71:dc\" -> \"00:31:92:47:71:dc\"\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n\n return {\n input: {\n assignations: {\n macAddress: formattedMac,\n ipAddress: assignation.framedipaddress,\n expired: assignation.expired,\n \"requireMacMatch\": true\n }\n }\n };\n}" } ], "nextSteps": [ { "slug": "assign-ips-to-macs-end" } ] }, { "name": "Assign IPs to MACs", "slug": "assign-ips-to-macs", "type": "PARALLEL_LOOP", "nodeSlug": "parallel-loop", "parentParallelLoopSlug": "process-radius-sessions", "initialSteps": [ { "slug": "assign-ip-address-to-devices" } ], "nextSteps": [ { "slug": "process-radius-sessions-end" } ], "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n const devices = state.steps[\"find-inventory-by-mac\"].output;\n return [...devices.byodDevicesToCreate, ...devices.devicesToSync]\n}" } ] }, { "name": "Create BYOD Devices", "slug": "create-byod-devices", "type": "PARALLEL_LOOP", "nodeSlug": "parallel-loop", "parentParallelLoopSlug": "process-radius-sessions", "initialSteps": [ { "slug": "get-account-id-from-radius-username" } ], "nextSteps": [ { "slug": "assign-ips-to-macs" } ], "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n return state.steps[\"find-inventory-by-mac\"].output.byodDevicesToCreate;\n}" } ] }, { "name": "Create BYOD using MAC", "slug": "create-byod-using-mac", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "parentParallelLoopSlug": "create-byod-devices", "allowSkipExecution": false, "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation AddInventoryItems($input: CreateInventoryItemsInput!) {\n addInventoryItems(input: $input) {\n inventoryItemsCreationResult {\n addedItems {\n id\n }\n }\n errors {\n code\n message\n payload {\n item {\n fields {\n data\n modelFieldId\n }\n }\n validationFailures {\n errorCode\n message\n modelFieldId\n }\n }\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n const { accountId } = state.steps['get-account-id-from-radius-username'].output.accounts[0];\n const model = state.steps['find-customer-provided-router-model'].output;\n const macAddressField = model.fields.nodes.find((field) => field.name === 'MAC Address');\n const radiusSession = state.currentParallelLoopIterations['create-byod-devices'];\n const macAddress = radiusSession.callingstationid\n const cleanedMac = macAddress.startsWith('1:') ? macAddress.slice(2) : macAddress;\n // Pad each segment to 2 digits. Example \"0:31:92:47:71:dc\" -> \"00:31:92:47:71:dc\"\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':');\n\n return {\n input: {\n items: [\n {\n fields: [\n {\n modelFieldId: macAddressField.id,\n data: formattedMac\n }\n ]\n }\n ],\n modelId: model.id,\n equipmentConditionType: 'NEW',\n purchasePriceInCents: 0,\n status: 'FUNCTIONAL',\n assignation: {\n assigneeId: accountId,\n assigneeType: 'ACCOUNT'\n }\n }\n };\n}" } ], "nextSteps": [ { "slug": "create-byod-devices-end" } ] }, { "name": "Find all Active RADIUS Sessions", "slug": "find-all-active-radius-sessions", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-query", "allowSkipExecution": false, "outputMapper": "({ output, secrets, state, utils, variables }) => {\n return output.results;\n}", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:SELECT username, callingstationid, framedipaddress FROM radacct WHERE callingstationid != '' AND acctstoptime IS NULL AND username != '' GROUP BY username, callingstationid, framedipaddress" }, { "key": "values", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n return [];\n}" } ], "nextSteps": [ { "slug": "find-all-expired-radius-sessions" } ] }, { "name": "Find all Expired RADIUS sessions", "slug": "find-all-expired-radius-sessions", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-query", "allowSkipExecution": false, "outputMapper": "({ output, secrets, state, utils, variables }) => {\n return output.results;\n}", "inputs": [ { "key": "instanceName", "type": "LITERAL", "value": "string:Default" }, { "key": "query", "type": "LITERAL", "value": "string:SELECT username, callingstationid, framedipaddress FROM radacct WHERE callingstationid != '' AND acctstoptime > DATE_SUB(NOW(),INTERVAL 1 DAY) AND username != '' GROUP BY username,callingstationid,framedipaddress" }, { "key": "values", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n return [];\n}" } ], "nextSteps": [ { "slug": "process-radius-sessions" } ] }, { "name": "Find Customer Provided Router Model", "slug": "find-customer-provided-router-model", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "parentParallelLoopSlug": "process-radius-sessions", "allowSkipExecution": false, "outputMapper": "({ output, secrets, state, utils, variables }) => {\n return output.response.inventoryModels.nodes[0];\n}", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query GetInventoryModels($filters: InventoryModelQueryFilters) {\n inventoryModels(filters: $filters) {\n nodes {\n id\n name\n fields {\n nodes {\n id\n name\n }\n }\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n return {\n filters: {\n name: {\n equals: \"Customer Provided Router\"\n }\n }\n };\n}" } ], "nextSteps": [ { "slug": "create-byod-devices" } ] }, { "name": "Find Inventory by MAC", "slug": "find-inventory-by-mac", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "parentParallelLoopSlug": "process-radius-sessions", "allowSkipExecution": false, "outputMapper": "({ output, secrets, state, utils, variables }) => {\n const radiusSessions = state.currentParallelLoopIterations['process-radius-sessions'];\n const normalizeMac = (callingstationid: string): string =>\n callingstationid\n .replace(/^1:/, '')\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n\n const inventoryItems = output.response.inventoryItems.nodes.map((item) => {\n const fields = item.fields.nodes;\n const serialNumber = fields.find((f) => f.modelField.name.toUpperCase() === 'S/N')?.data ?? '';\n const macAddress = fields.find((f) => f.modelField.name.toUpperCase() === 'PORT 1 MAC')?.data ?? '';\n\n return {\n modelName: item.model.name,\n manufacturerName: item.model.manufacturer.name,\n assignation: item.assignation.assignee,\n serialNumber,\n macAddress\n };\n });\n //const assignedInventoryItems = inventoryItems.filter((item) => item.assignation.__typename.toLowerCase() === 'account');\n const macAddresses = new Set(inventoryItems.map((item) => item.macAddress.toUpperCase()));\n\n // Get the list of BYOD Devices from the current chunk\n const byodDevicesToCreateWithDuplicates = radiusSessions.filter((session) => {\n return !macAddresses.has(normalizeMac(session.callingstationid));\n });\n const byodDevicesToCreate = [\n ...new Map(byodDevicesToCreateWithDuplicates.map((device) => [`${device.username}-${device.callingstationid}-${device.framedipaddress}`, device])).values()\n ];\n\n // Get the list of pre-existing devices in Gaiia\n const devicesToSyncWithDuplicates = radiusSessions\n .filter((session) => {\n return macAddresses.has(normalizeMac(session.callingstationid));\n })\n .map((session) => {\n const inventoryItem = inventoryItems.find((item) => item.macAddress === normalizeMac(session.callingstationid));\n if (!inventoryItem) {\n utils.fail(`No inventory item found for MAC: ${session.callingstationid}`, 'INVENTORY_ITEM_NOT_FOUND');\n}\n return {\n callingstationid: inventoryItem?.macAddress,\n //accountId: inventoryItem?.assignation['id'],\n framedipaddress: session.framedipaddress,\n expired: session.expired\n };\n });\n const devicesToSync = [...new Map(devicesToSyncWithDuplicates.map((device) => [`${device.callingstationid}-${device.framedipaddress}`, device])).values()];\n\n return {\n byodDevicesToCreate,\n devicesToSync\n };\n}", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query Nodes($filters: InventoryItemsQueryFilters) {\n inventoryItems(filters: $filters) {\n nodes {\n id\n model {\n name\n category {\n name\n }\n manufacturer {\n name\n }\n }\n fields {\n nodes {\n data\n modelField {\n name\n }\n id\n }\n }\n assignation {\n id\n assignee {\n __typename\n ... on Account {\n id\n name\n }\n }\n }\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n const macAddresses = state.currentParallelLoopIterations['process-radius-sessions'].map((account) => account.username);;\n return {\n filters: {\n fieldData: {\n in: macAddresses\n }\n }\n };\n}" } ], "nextSteps": [ { "slug": "if-there-are-byod-devices-to-create" } ] }, { "name": "Get Account ID from RADIUS Username", "slug": "get-account-id-from-radius-username", "type": "CUSTOM_WORKFLOW", "nodeSlug": "get-account-id-from-radius-username", "parentParallelLoopSlug": "create-byod-devices", "allowSkipExecution": false, "inputs": [ { "key": "username", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n return state.currentParallelLoopIterations['create-byod-devices'].username;\n}" } ], "nextSteps": [ { "slug": "create-byod-devices-end", "errorCodes": [ "USERNAME_NOT_FOUND" ] }, { "slug": "create-byod-using-mac" } ] }, { "name": "If there are BYOD devices to create", "slug": "if-there-are-byod-devices-to-create", "type": "CONDITION", "nodeSlug": "condition", "parentParallelLoopSlug": "process-radius-sessions", "condition": "({ secrets, state, utils, variables }) => {\n return state.steps['find-inventory-by-mac'].output.byodDevicesToCreate.length > 0;\n}", "elseSteps": [ { "slug": "assign-ips-to-macs" } ], "thenSteps": [ { "slug": "find-customer-provided-router-model" } ] }, { "name": "Process RADIUS sessions", "slug": "process-radius-sessions", "type": "PARALLEL_LOOP", "nodeSlug": "parallel-loop", "initialSteps": [ { "slug": "find-inventory-by-mac" } ], "maxConcurrentIterations": 5, "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ state }) => state.steps['find-inventory-by-mac'].output", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ secrets, state, utils, variables }) => {\n const CHUNK_SIZE = 1000;\n const activeSessions = state.steps['find-all-active-radius-sessions'].output;\n const expiredSessions = state.steps['find-all-expired-radius-sessions'].output;\n // remove expired sessions that are still active for the same MAC/IP/username\n const expiredInactiveSessions = expiredSessions.filter(\n (expiredSession) =>\n !activeSessions.some(\n (activeSession) =>\n activeSession.callingstationid === expiredSession.callingstationid &&\n activeSession.framedipaddress === expiredSession.framedipaddress &&\n activeSession.username === expiredSession.username\n )\n );\n const allSessions = [\n ...activeSessions.map((session) => ({\n ...session,\n expired: false\n })),\n ...expiredInactiveSessions.map((session) => ({\n ...session,\n expired: true\n }))\n ];\n const chunkedSessions: {\n username: string;\n callingstationid: string;\n framedipaddress: string;\n expired: boolean;\n }[][] = [];\n for (let index = 0; index < allSessions.length; index += CHUNK_SIZE) {\n chunkedSessions.push(allSessions.slice(index, index + CHUNK_SIZE));\n }\n return chunkedSessions;\n}" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-all-active-radius-sessions" } ], "type": "MANUAL" } } }, "Get NetBox prefix synchronization settings": { "id": "workflow_vWK4ZwVfC2DVfrNdvdGVdk", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "get-prefix-synchronization-settings" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => ({ ...state.steps[\"get-prefix-synchronization-settings\"].output })", "type": "JSON" }, "steps": [ { "name": "get-prefix-synchronization-settings", "slug": "get-prefix-synchronization-settings", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n const prefixSynchronizationSettingsString = await utils.integration.datastore.getItem(`instances.${instanceId}.prefixSynchronization`);\n if (prefixSynchronizationSettingsString) {\n try {\n return JSON.parse(prefixSynchronizationSettingsString);\n } catch (error) {\n return utils.fail(\"Failed to parse prefix synchronization settings\", \"INVALID_JSON_FORMAT\", { error });\n }\n }\n return { ipamRoles: [], isEnabled: false };\n }" } ] } }, "FreeRADIUS MySQL Transaction": { "id": "workflow_i8Y7d5CWeVzmuEANTatCDe", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The name of the FreeRADIUS instance to use. If not provided, uses the first instance." }, "queries": { "type": "array", "description": "Array of queries to execute in the transaction", "items": { "type": "object", "properties": { "query": { "type": "string", "description": "The SQL query to execute" }, "values": { "type": "array", "description": "The values to use in the prepared statement", "items": { "type": [ "string", "number", "boolean", "null" ] } } }, "required": [ "query" ], "additionalProperties": false } } }, "required": [ "queries" ], "additionalProperties": false }, "nextSteps": [ { "slug": "fetch-instance-database-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => state.steps[\"execute-mysql-transaction\"].output", "type": "JSON" }, "steps": [ { "name": "fetch-instance-database-config", "slug": "fetch-instance-database-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName } = state.input;\n let instanceId;\n if (!instanceName) {\n const instancesId = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesId) {\n return utils.fail(\"No instances found in datastore\", \"NO_INSTANCES_FOUND\");\n }\n const instanceIds = instancesId.split(\",\");\n if (instanceIds.length === 0) {\n return utils.fail(\"No instance ids found\", \"NO_INSTANCE_IDS_FOUND\");\n }\n instanceId = instanceIds[0]?.trim();\n } else {\n instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Invalid instance name provided\", \"INVALID_INSTANCE_NAME\");\n }\n }\n const [databaseHost, databasePort, databaseName, databaseUsername, databasePassword] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${instanceId}.databaseHost`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databasePort`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databaseName`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databaseUsername`),\n utils.integration.datastore.getItem(`instances.${instanceId}.databasePassword`)\n ]);\n if (!databaseHost || !databasePort || !databaseName || !databaseUsername || !databasePassword) {\n return utils.fail(\"Incomplete instance configuration\", \"INCOMPLETE_INSTANCE_CONFIG\");\n }\n return {\n databaseHost,\n databasePort: Number(databasePort),\n databaseName,\n databaseUsername,\n databasePassword\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "execute-mysql-transaction" } ], "outputMapper": "({ output }) => output" }, { "name": "execute-mysql-transaction", "slug": "execute-mysql-transaction", "type": "MYSQL_TRANSACTION", "nodeSlug": "mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databaseHost", "key": "host" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databasePort", "key": "port" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databaseName", "key": "database" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databaseUsername", "key": "user" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"fetch-instance-database-config\"].output.databasePassword", "key": "password" }, { "type": "FUNCTION", "value": "({ state }) => state.input.queries", "key": "queries" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Test Workflow": { "id": "workflow_jxqCqU1D4Uwr2HgnRm7Bbt", "description": null, "definition": { "steps": [ { "name": "Get Account ID from RADIUS Username", "slug": "get-account-id-from-radius-username", "type": "CUSTOM_WORKFLOW", "nodeSlug": "get-account-id-from-radius-username", "allowSkipExecution": false, "inputs": [ { "key": "username", "type": "LITERAL", "value": "string:jenniferb" } ], "nextSteps": [ { "slug": "end" } ] } ], "trigger": { "nextSteps": [ { "slug": "get-account-id-from-radius-username" } ], "type": "MANUAL" } } }, "FreeRADIUS Send COA Packet": { "id": "workflow_ai7uktpC7Q7JA4BHnQvq7P", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "attributes": { "type": "object", "description": "The RADIUS attributes to include in the packet", "additionalProperties": { "type": "string" } }, "instanceName": { "type": "string", "description": "The name of the FreeRADIUS instance to use" } }, "required": [ "attributes", "instanceName" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-instance-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => ({\n success: state.steps[\"send-coa-packet\"].output.success\n })", "type": "JSON" }, "steps": [ { "name": "get-instance-config", "slug": "get-instance-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName } = state.input;\n let instanceId;\n if (!instanceName) {\n const instancesId = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesId) {\n return utils.fail(\"No instances found in datastore\", \"NO_INSTANCES_FOUND\");\n }\n const instanceIds = instancesId.split(\",\");\n if (instanceIds.length === 0) {\n return utils.fail(\"No instance ids found\", \"NO_INSTANCE_IDS_FOUND\");\n }\n instanceId = instanceIds[0]?.trim();\n } else {\n instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Invalid instance name provided\", \"INVALID_INSTANCE_NAME\");\n }\n }\n const [radiusServerHost, coaPort, coaSecret] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${instanceId}.radiusServerHost`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaPort`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaSecret`)\n ]);\n if (!radiusServerHost || !coaPort || !coaSecret) {\n return utils.fail(\"Incomplete COA configuration\", \"INCOMPLETE_COA_CONFIG\");\n }\n return {\n radiusServerHost,\n coaPort: Number(coaPort),\n coaSecret\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "send-coa-packet" } ], "outputMapper": "({ output }) => output" }, { "name": "send-coa-packet", "slug": "send-coa-packet", "type": "UDP4_REQUEST", "nodeSlug": "udp4-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"base64\"", "key": "encoding" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-config\"].output.radiusServerHost", "key": "host" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-config\"].output.coaPort", "key": "port" }, { "type": "FUNCTION", "value": "({ state }) => {\n const attributes = Object.entries(state.input.attributes);\n const { coaSecret: secret } = state.steps[\"get-instance-config\"].output;\n const DISCONNECT_REQUEST_CODE = 40;\n const AUTH_LENGTH = 16;\n const ATTRIBUTES = {\n \"User-Name\": { id: 1, name: \"User-Name\", type: \"string\" },\n \"NAS-IP-Address\": { id: 4, name: \"NAS-IP-Address\", type: \"ipaddr\" },\n \"NAS-Port\": { id: 5, name: \"NAS-Port\", type: \"integer\" },\n \"Framed-IP-Address\": { id: 8, name: \"Framed-IP-Address\", type: \"ipaddr\" },\n \"Called-Station-Id\": { id: 30, name: \"Called-Station-Id\", type: \"string\" },\n \"Calling-Station-Id\": { id: 31, name: \"Calling-Station-Id\", type: \"string\" },\n \"NAS-Identifier\": { id: 32, name: \"NAS-Identifier\", type: \"string\" },\n \"Acct-Session-Id\": { id: 44, name: \"Acct-Session-Id\", type: \"string\" },\n \"Acct-Multi-Session-Id\": { id: 50, name: \"Acct-Multi-Session-Id\", type: \"string\" },\n \"Event-Timestamp\": { id: 55, name: \"Event-Timestamp\", type: \"date\" },\n \"NAS-Port-Type\": {\n id: 61,\n name: \"NAS-Port-Type\",\n type: \"integer\",\n enumValues: {\n Async: 0,\n Sync: 1,\n ISDN: 2,\n \"ISDN-V120\": 3,\n \"ISDN-V110\": 4,\n Virtual: 5,\n PIAFS: 6,\n \"HDLC-Clear-Channel\": 7,\n \"X.25\": 8,\n \"X.75\": 9,\n \"G.3-Fax\": 10,\n SDSL: 11,\n \"ADSL-CAP\": 12,\n \"ADSL-DMT\": 13,\n IDSL: 14,\n Ethernet: 15,\n xDSL: 16,\n Cable: 17,\n \"Wireless-Other\": 18,\n \"Wireless-802.11\": 19,\n \"Token-Ring\": 20,\n FDDI: 21\n }\n },\n \"Message-Authenticator\": { id: 80, name: \"Message-Authenticator\", type: \"octets\" },\n \"NAS-Port-Id\": { id: 87, name: \"NAS-Port-Id\", type: \"string\" },\n \"Chargeable-User-Identity\": { id: 89, name: \"Chargeable-User-Identity\", type: \"string\" }\n };\n const stringToUint8Array = (string_) => new TextEncoder().encode(string_);\n const concatUint8Arrays = (...arrays) => {\n const totalLength = arrays.reduce((accumulator, array) => accumulator + array.length, 0);\n const result2 = new Uint8Array(totalLength);\n let offset2 = 0;\n for (const array of arrays) {\n result2.set(array, offset2);\n offset2 += array.length;\n }\n return result2;\n };\n const md5 = (data) => {\n const K = new Uint32Array([\n 3614090360,\n 3905402710,\n 606105819,\n 3250441966,\n 4118548399,\n 1200080426,\n 2821735955,\n 4249261313,\n 1770035416,\n 2336552879,\n 4294925233,\n 2304563134,\n 1804603682,\n 4254626195,\n 2792965006,\n 1236535329,\n 4129170786,\n 3225465664,\n 643717713,\n 3921069994,\n 3593408605,\n 38016083,\n 3634488961,\n 3889429448,\n 568446438,\n 3275163606,\n 4107603335,\n 1163531501,\n 2850285829,\n 4243563512,\n 1735328473,\n 2368359562,\n 4294588738,\n 2272392833,\n 1839030562,\n 4259657740,\n 2763975236,\n 1272893353,\n 4139469664,\n 3200236656,\n 681279174,\n 3936430074,\n 3572445317,\n 76029189,\n 3654602809,\n 3873151461,\n 530742520,\n 3299628645,\n 4096336452,\n 1126891415,\n 2878612391,\n 4237533241,\n 1700485571,\n 2399980690,\n 4293915773,\n 2240044497,\n 1873313359,\n 4264355552,\n 2734768916,\n 1309151649,\n 4149444226,\n 3174756917,\n 718787259,\n 3951481745\n ]);\n const S = [\n 7,\n 12,\n 17,\n 22,\n 7,\n 12,\n 17,\n 22,\n 7,\n 12,\n 17,\n 22,\n 7,\n 12,\n 17,\n 22,\n 5,\n 9,\n 14,\n 20,\n 5,\n 9,\n 14,\n 20,\n 5,\n 9,\n 14,\n 20,\n 5,\n 9,\n 14,\n 20,\n 4,\n 11,\n 16,\n 23,\n 4,\n 11,\n 16,\n 23,\n 4,\n 11,\n 16,\n 23,\n 4,\n 11,\n 16,\n 23,\n 6,\n 10,\n 15,\n 21,\n 6,\n 10,\n 15,\n 21,\n 6,\n 10,\n 15,\n 21,\n 6,\n 10,\n 15,\n 21\n ];\n const originalLength = data.length;\n const bitLength = BigInt(originalLength * 8);\n let paddingLength = 64 - (originalLength + 8) % 64;\n if (paddingLength === 0)\n paddingLength = 64;\n const paddedData = new Uint8Array(originalLength + paddingLength + 8);\n paddedData.set(data);\n paddedData[originalLength] = 128;\n const view = new DataView(paddedData.buffer);\n view.setUint32(paddedData.length - 8, Number(bitLength & BigInt(4294967295)), true);\n view.setUint32(paddedData.length - 4, Number(bitLength >> BigInt(32) & BigInt(4294967295)), true);\n let a0 = 1732584193;\n let b0 = 4023233417;\n let c0 = 2562383102;\n let d0 = 271733878;\n const rotateLeft = (x, n) => (x << n | x >>> 32 - n) >>> 0;\n for (let index = 0; index < paddedData.length; index += 64) {\n const M = new Uint32Array(16);\n for (let index_ = 0; index_ < 16; index_++) {\n M[index_] = view.getUint32(index + index_ * 4, true);\n }\n let A = a0, B = b0, C = c0, D = d0;\n for (let index2 = 0; index2 < 64; index2++) {\n let F, g;\n if (index2 < 16) {\n F = B & C | ~B & D;\n g = index2;\n } else if (index2 < 32) {\n F = D & B | ~D & C;\n g = (5 * index2 + 1) % 16;\n } else if (index2 < 48) {\n F = B ^ C ^ D;\n g = (3 * index2 + 5) % 16;\n } else {\n F = C ^ (B | ~D);\n g = 7 * index2 % 16;\n }\n F = F + A + K[index2] + M[g] >>> 0;\n A = D;\n D = C;\n C = B;\n B = B + rotateLeft(F, S[index2]) >>> 0;\n }\n a0 = a0 + A >>> 0;\n b0 = b0 + B >>> 0;\n c0 = c0 + C >>> 0;\n d0 = d0 + D >>> 0;\n }\n const result2 = new Uint8Array(16);\n const resultView = new DataView(result2.buffer);\n resultView.setUint32(0, a0, true);\n resultView.setUint32(4, b0, true);\n resultView.setUint32(8, c0, true);\n resultView.setUint32(12, d0, true);\n return result2;\n };\n const hmacMd5 = (key, message) => {\n const blockSize = 64;\n if (key.length > blockSize) {\n key = md5(key);\n }\n const paddedKey = new Uint8Array(blockSize);\n paddedKey.set(key);\n const ipad = new Uint8Array(blockSize);\n const opad = new Uint8Array(blockSize);\n for (let index = 0; index < blockSize; index++) {\n ipad[index] = paddedKey[index] ^ 54;\n opad[index] = paddedKey[index] ^ 92;\n }\n const innerHash = md5(concatUint8Arrays(ipad, message));\n return md5(concatUint8Arrays(opad, innerHash));\n };\n const encodeAttribute = (attribute) => {\n const [nameOrId, value] = attribute;\n if (typeof nameOrId === \"number\") {\n const rawValue = value;\n const result3 = new Uint8Array(2 + rawValue.length);\n result3[0] = nameOrId;\n result3[1] = 2 + rawValue.length;\n result3.set(rawValue, 2);\n return result3;\n }\n const attrDef = ATTRIBUTES[nameOrId];\n if (!attrDef) {\n throw new Error(`Unknown attribute: ${nameOrId}`);\n }\n let encodedValue;\n switch (attrDef.type) {\n case \"string\":\n case \"text\":\n encodedValue = stringToUint8Array(value);\n break;\n case \"ipaddr\": {\n const parts = value.split(\".\").map(Number);\n if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) {\n throw new Error(`Invalid IP address: ${value}`);\n }\n encodedValue = new Uint8Array(parts);\n break;\n }\n case \"integer\": {\n let numberValue;\n numberValue = typeof value === \"string\" && attrDef.enumValues?.[value] !== void 0 ? attrDef.enumValues[value] : value;\n encodedValue = new Uint8Array(4);\n new DataView(encodedValue.buffer).setUint32(0, numberValue, false);\n break;\n }\n case \"date\": {\n const timestamp = Math.floor(value.getTime() / 1e3);\n encodedValue = new Uint8Array(4);\n new DataView(encodedValue.buffer).setUint32(0, timestamp, false);\n break;\n }\n case \"octets\":\n encodedValue = value;\n break;\n default:\n throw new Error(`Unsupported attribute type: ${attrDef.type}`);\n }\n const result2 = new Uint8Array(2 + encodedValue.length);\n result2[0] = attrDef.id;\n result2[1] = 2 + encodedValue.length;\n result2.set(encodedValue, 2);\n return result2;\n };\n const packet = new Uint8Array(4096);\n let offset = 0;\n packet[offset++] = DISCONNECT_REQUEST_CODE;\n const identifier = Math.floor(Math.random() * 256);\n if (identifier > 255) {\n throw new Error(\"Identifier must be 0-255\");\n }\n packet[offset++] = identifier;\n offset += 2;\n const authenticatorOffset = offset;\n offset += AUTH_LENGTH;\n let messageAuthenticatorOffset = -1;\n for (const attribute of attributes) {\n if (attribute[0] === \"Message-Authenticator\" || attribute[0] === 80) {\n messageAuthenticatorOffset = offset + 2;\n }\n const encoded = encodeAttribute(attribute);\n packet.set(encoded, offset);\n offset += encoded.length;\n }\n new DataView(packet.buffer).setUint16(2, offset, false);\n const result = packet.slice(0, offset);\n const secretBytes = stringToUint8Array(secret);\n const authenticator = md5(concatUint8Arrays(result, secretBytes));\n for (const [index, byte] of authenticator.entries()) {\n result[authenticatorOffset + index] = byte;\n }\n if (messageAuthenticatorOffset !== -1) {\n const messageAuthenticator = hmacMd5(secretBytes, result);\n for (const [index, byte] of messageAuthenticator.entries()) {\n result[messageAuthenticatorOffset + index] = byte;\n }\n }\n return Buffer.from(result).toString(\"base64\");\n }", "key": "message" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" } ] } }, "Update RADIUS Account Fields": { "id": "workflow_4NGHRz53T8XpEDPxCuCvrb", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The FreeRADIUS instance name" }, "username": { "type": "string", "description": "The RADIUS username" }, "framedIpAddress": { "type": [ "string", "null" ], "description": "The Framed-IP-Address attribute (IPv4 address). Set to null to remove." }, "framedRoute": { "type": [ "string", "null" ], "description": "The Framed-Route attribute (without gateway). Set to null to remove." } }, "required": [ "instanceName", "username" ], "additionalProperties": false }, "nextSteps": [ { "slug": "validate-input" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n const error = state.steps[\"handle-error\"];\n utils.fail(error.output.message, error.output.code, error.output.payload);\n } else if (\"update-account-fields\" in state.steps) {\n const {\n output: { updatedFields }\n } = state.steps[\"update-account-fields\"];\n return {\n success: true,\n updatedFields\n };\n }\n return {\n success: true,\n updatedFields: []\n };\n }", "type": "JSON" }, "steps": [ { "name": "validate-input", "slug": "validate-input", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state, utils }) => {\n const { framedIpAddress, framedRoute } = state.input;\n const ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})$/;\n if (framedIpAddress !== void 0 && framedIpAddress !== null && framedIpAddress !== \"\" && !ipv4Regex.test(framedIpAddress)) {\n return utils.fail(\"Framed-IP-Address must be a valid IPv4 address\", \"INVALID_FRAMED_IP_ADDRESS\");\n }\n if (framedRoute !== void 0 && framedRoute !== null && framedRoute !== \"\") {\n const ipPart = framedRoute.split(\"/\")[0].trim();\n if (!ipv4Regex.test(ipPart)) {\n return utils.fail(\"Framed-Route must contain a valid IPv4 address\", \"INVALID_FRAMED_ROUTE\");\n }\n }\n return {\n validated: true\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "fetch-current-and-check-availability" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "fetch-current-and-check-availability", "slug": "fetch-current-and-check-availability", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username, framedIpAddress, framedRoute } = state.input;\n const queries = [\n {\n query: `SELECT rr.attribute, rr.value FROM radreply rr WHERE rr.username = ? AND rr.attribute IN ('Framed-IP-Address', 'Framed-Route') AND rr.value IS NOT NULL;`,\n values: [username]\n }\n ];\n if (framedIpAddress !== void 0 && framedIpAddress !== null && framedIpAddress !== \"\") {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-IP-Address\" AND value = ? AND username <> ?',\n values: [framedIpAddress, username]\n });\n }\n if (framedRoute !== void 0 && framedRoute !== null && framedRoute !== \"\") {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-Route\" AND value = ? AND username <> ?',\n values: [`${framedRoute} 0.0.0.0 1`, username]\n });\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "update-account-fields" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ state, output, utils }) => {\n const { framedIpAddress, framedRoute } = state.input;\n const [radreplyResults, ...availabilityResults] = output.results;\n const currentFramedIpAddressRow = radreplyResults?.find((r) => r.attribute === \"Framed-IP-Address\");\n const currentFramedRouteRow = radreplyResults?.find((r) => r.attribute === \"Framed-Route\");\n const currentFramedIpAddress = currentFramedIpAddressRow?.value || null;\n const currentFramedRoute = currentFramedRouteRow?.value || null;\n const currentFramedRouteWithoutGateway = currentFramedRoute ? currentFramedRoute.split(\" \")[0] : null;\n if (framedIpAddress !== void 0 && framedIpAddress !== null && framedIpAddress !== \"\") {\n const [framedIpAddressExists] = availabilityResults.shift();\n if (framedIpAddressExists?.count > 0) {\n return utils.fail(\"Framed-IP-Address is not available\", \"FRAMED_IP_ADDRESS_NOT_AVAILABLE\");\n }\n }\n if (framedRoute !== void 0 && framedRoute !== null && framedRoute !== \"\") {\n const [framedRouteExists] = availabilityResults.shift();\n if (framedRouteExists?.count > 0) {\n return utils.fail(\"Framed-Route is not available\", \"FRAMED_ROUTE_NOT_AVAILABLE\");\n }\n }\n return {\n currentFramedIpAddress,\n currentFramedRoute: currentFramedRouteWithoutGateway\n };\n }" }, { "name": "update-account-fields", "slug": "update-account-fields", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username, framedIpAddress, framedRoute } = state.input;\n const {\n output: { currentFramedIpAddress, currentFramedRoute }\n } = state.steps[\"fetch-current-and-check-availability\"];\n const currentFramedRouteWithGateway = currentFramedRoute ? `${currentFramedRoute} 0.0.0.0 1` : null;\n const queries = [];\n if (framedIpAddress !== void 0 && framedIpAddress !== currentFramedIpAddress) {\n if (currentFramedIpAddress && (framedIpAddress === null || framedIpAddress === \"\")) {\n queries.push({\n query: `DELETE FROM radreply\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [username, \"Framed-IP-Address\"]\n });\n } else if (framedIpAddress && framedIpAddress !== \"\") {\n if (currentFramedIpAddress !== null) {\n queries.push({\n query: `UPDATE radreply\n SET value = ?\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [framedIpAddress, username, \"Framed-IP-Address\"]\n });\n } else {\n queries.push({\n query: `INSERT INTO radreply(username, attribute, op, value)\n VALUES (?, ?, ?, ?)`,\n values: [username, \"Framed-IP-Address\", \"=\", framedIpAddress]\n });\n }\n }\n }\n if (framedRoute !== void 0 && framedRoute !== currentFramedRoute) {\n const framedRouteWithGateway = framedRoute && framedRoute !== \"\" ? `${framedRoute} 0.0.0.0 1` : null;\n if (currentFramedRouteWithGateway && !framedRouteWithGateway) {\n queries.push({\n query: `DELETE FROM radreply\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [username, \"Framed-Route\"]\n });\n } else if (framedRouteWithGateway) {\n if (currentFramedRouteWithGateway !== null) {\n queries.push({\n query: `UPDATE radreply\n SET value = ?\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [framedRouteWithGateway, username, \"Framed-Route\"]\n });\n } else {\n queries.push({\n query: `INSERT INTO radreply(username, attribute, op, value)\n VALUES (?, ?, ?, ?)`,\n values: [username, \"Framed-Route\", \"+=\", framedRouteWithGateway]\n });\n }\n }\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ state }) => {\n const { framedIpAddress, framedRoute } = state.input;\n const {\n output: { currentFramedIpAddress, currentFramedRoute }\n } = state.steps[\"fetch-current-and-check-availability\"];\n const updatedFields = [];\n if (framedIpAddress !== void 0 && framedIpAddress !== currentFramedIpAddress) {\n updatedFields.push(\"framedIpAddress\");\n }\n if (framedRoute !== void 0 && framedRoute !== currentFramedRoute) {\n updatedFields.push(\"framedRoute\");\n }\n return {\n updatedFields\n };\n }" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"fetch-current-and-check-availability\"]?.error || steps[\"validate-input\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Import Network Sites": { "id": "workflow_a2yvCkM3eAEiEfYjaWmjLk", "description": null, "definition": { "trigger": { "skipExecutionMapper": "async ({ state, utils }) => {\n const { instanceId } = state.trigger.queryParameters;\n if (!instanceId) {\n return true;\n }\n const networkSiteSyncString = await utils.integration.datastore.getItem(`instances.${instanceId}.networkSiteSynchronization`);\n if (!networkSiteSyncString) {\n return true;\n }\n try {\n const networkSiteSync = JSON.parse(networkSiteSyncString);\n if (!networkSiteSync.isEnabled) {\n return true;\n }\n } catch (error) {\n return utils.fail(\"Failed to parse network site synchronization settings\", \"INVALID_JSON_FORMAT\", { error });\n }\n if (state.input.event === \"deleted\") {\n return true;\n }\n return false;\n }", "inputJsonSchema": { "type": "object", "required": [ "model", "event", "data" ], "properties": { "model": { "type": "string", "const": "site" }, "event": { "type": "string", "enum": [ "created", "updated", "deleted" ] }, "data": { "type": "object", "required": [ "id", "name", "slug", "url", "status", "custom_fields" ], "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "slug": { "type": "string" }, "url": { "type": "string" }, "custom_fields": { "type": "object" }, "physical_address": { "type": [ "string", "null" ] }, "latitude": { "type": [ "number", "null" ] }, "longitude": { "type": [ "number", "null" ] }, "status": { "type": "object", "required": [ "label", "value" ], "properties": { "label": { "type": "string" }, "value": { "type": "string" } } } } } } }, "nextSteps": [ { "slug": "get-instance-name-from-id" } ], "type": "WEBHOOK" }, "steps": [ { "name": "get-instance-name-from-id", "slug": "get-instance-name-from-id", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceId } = state.trigger.queryParameters;\n const instanceName = await utils.integration.datastore.getItem(`instances.${instanceId}.name`);\n if (!instanceName) {\n return utils.fail(`Instance with ID ${instanceId} not found`, \"INSTANCE_NOT_FOUND\");\n }\n return instanceName;\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-network-site-synchronization-settings" } ], "outputMapper": "({ output }) => output" }, { "name": "get-network-site-synchronization-settings", "slug": "get-network-site-synchronization-settings", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-get-network-site-synchronization-settings", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-name-from-id\"].output", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-network-site-custom-field-definitions" } ], "outputMapper": "({ output, utils }) => {\n if (!output.isEnabled) {\n return utils.fail(\"NetBox network site synchronization is disabled for this instance.\", \"NETWORK_SITE_SYNC_DISABLED\");\n }\n return output;\n }" }, { "name": "get-network-site-custom-field-definitions", "slug": "get-network-site-custom-field-definitions", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // --------------------------------------------------\n // PaginatedNetworkSiteCustomFieldDefinitionsDocument\n // --------------------------------------------------\n const PaginatedNetworkSiteCustomFieldDefinitionsDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"PaginatedNetworkSiteCustomFieldDefinitions\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"String\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSiteCustomFieldDefinitionFilters\"}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSiteCustomFieldDefinitions\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"pageInfo\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"hasNextPage\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"endCursor\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"name\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"required\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"type\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"createdAt\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"archivedAt\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"options\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"option\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":449}};\n // ------------------------------------------------------------\n\n return PaginatedNetworkSiteCustomFieldDefinitionsDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "() => ({\n first: 250,\n filters: {\n isArchived: { equals: false }\n }\n })", "key": "variables" } ], "nextSteps": [ { "slug": "get-network-site-custom-statuses" } ], "outputMapper": "({ output }) => {\n const response = output.response;\n return {\n customFieldDefinitions: response.networkSiteCustomFieldDefinitions.nodes\n };\n }" }, { "name": "get-network-site-custom-statuses", "slug": "get-network-site-custom-statuses", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // ---------------------------------\n // NetworkSiteCustomStatusesDocument\n // ---------------------------------\n const NetworkSiteCustomStatusesDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSiteCustomStatuses\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSiteCustomStatuses\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"name\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":146}};\n // ------------------------------------------------------------\n\n return NetworkSiteCustomStatusesDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "() => ({\n first: 250\n })", "key": "variables" } ], "nextSteps": [ { "slug": "check-datastore-for-site-mapping" } ], "outputMapper": "({ output }) => {\n const response = output.response;\n const statusMap = response.networkSiteCustomStatuses.nodes.reduce(\n (map, status) => ({\n ...map,\n [status.name.toLowerCase()]: status.id\n }),\n {}\n );\n return { statusMap };\n }" }, { "name": "check-datastore-for-site-mapping", "slug": "check-datastore-for-site-mapping", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "check-has-mapping" } ], "outputMapper": "async ({ state, utils }) => {\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const netboxSiteId = state.input.data.id;\n const gaiiaSiteId = await utils.integration.datastore.getItem(`netbox-site-to-gaiia.${integrationId}.${netboxSiteId}`);\n return { gaiiaSiteId };\n }" }, { "name": "check-has-mapping", "slug": "check-has-mapping", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => !!state.steps[\"check-datastore-for-site-mapping\"].output.gaiiaSiteId", "thenSteps": [ { "slug": "get-gaiia-network-site-by-id" } ], "elseSteps": [ { "slug": "get-gaiia-network-site-by-name" } ] }, { "name": "get-gaiia-network-site-by-id", "slug": "get-gaiia-network-site-by-id", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // -------------------\n // NetworkSiteDocument\n // -------------------\n const NetworkSiteDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSite\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"GlobalID\"}}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"firstFields\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSite\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"customStatus\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"externalSourceLink\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"externalSourceId\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"integration\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"fields\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"firstFields\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"data\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"definition\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"name\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"address\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"country\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"line1\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"locality\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"postalCode\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"region\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"latitude\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"longitude\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":480}};\n // ------------------------------------------------------------\n\n return NetworkSiteDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => ({\n id: state.steps[\"check-datastore-for-site-mapping\"].output.gaiiaSiteId,\n firstFields: 250\n })", "key": "variables" } ], "nextSteps": [ { "slug": "get-gaiia-network-site-by-name", "errorCodes": [ "STALE_MAPPING" ] }, { "slug": "merge-network-site-lookup" } ], "outputMapper": "({ output, state, utils }) => {\n const response = output.response;\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const netboxSiteId = String(state.input.data.id);\n if (response.networkSite?.externalSourceLink?.integration.id === integrationId && response.networkSite.externalSourceLink.externalSourceId === netboxSiteId) {\n return { existingSite: response.networkSite };\n }\n return utils.fail(\"Datastore mapping exists but site external link does not match\", \"STALE_MAPPING\");\n }" }, { "name": "get-gaiia-network-site-by-name", "slug": "get-gaiia-network-site-by-name", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // ------------------------------------\n // NetworkSitesWithExternalLinkDocument\n // ------------------------------------\n const NetworkSitesWithExternalLinkDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSitesWithExternalLink\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"String\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSiteFilters\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"firstFields\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSites\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"edges\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"node\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"customStatus\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"externalSourceLink\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"externalSourceId\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"integration\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"fields\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"firstFields\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"data\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"definition\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"name\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"address\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"country\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"line1\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"locality\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"postalCode\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"region\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"latitude\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"longitude\"},\"arguments\":[],\"directives\":[]}]}}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"pageInfo\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"endCursor\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"hasNextPage\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"totalCount\"},\"arguments\":[],\"directives\":[]}]}}]}}],\"loc\":{\"start\":0,\"end\":802}};\n // ------------------------------------------------------------\n\n return NetworkSitesWithExternalLinkDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => ({\n filters: {\n name: { equals: state.input.data.name }\n },\n first: 10,\n firstFields: 250\n })", "key": "variables" } ], "nextSteps": [ { "slug": "merge-network-site-lookup" } ], "outputMapper": "({ output, state }) => {\n const response = output.response;\n const netboxSiteName = state.input.data.name;\n const existingSite = response.networkSites.edges.find(\n (edge) => !edge.node.externalSourceLink && edge.node.name.toLowerCase() === netboxSiteName.toLowerCase()\n )?.node;\n return {\n existingSite: existingSite || null\n };\n }" }, { "name": "merge-network-site-lookup", "slug": "merge-network-site-lookup", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "check-site-exists" } ], "outputMapper": "({ state }) => {\n const { steps } = state;\n if (\"get-gaiia-network-site-by-id\" in steps && \"output\" in steps[\"get-gaiia-network-site-by-id\"]) {\n return { existingSite: steps[\"get-gaiia-network-site-by-id\"].output.existingSite };\n }\n if (\"get-gaiia-network-site-by-name\" in steps) {\n return { existingSite: steps[\"get-gaiia-network-site-by-name\"].output.existingSite };\n }\n return {\n existingSite: null\n };\n }" }, { "name": "check-site-exists", "slug": "check-site-exists", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => !!state.steps[\"merge-network-site-lookup\"].output.existingSite", "thenSteps": [ { "slug": "prepare-network-site-data-for-update" } ], "elseSteps": [ { "slug": "prepare-network-site-data-for-create" } ] }, { "name": "prepare-network-site-data-for-create", "slug": "prepare-network-site-data-for-create", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "create-network-site" } ], "outputMapper": "({ state, utils }) => {\n // ---------------\n // parseUSAddress\n // ---------------\n const parseUSAddress = (netboxSite) => {\n const { physical_address, latitude, longitude } = netboxSite;\n if (!physical_address) {\n return null;\n }\n const postalCodeMatch = physical_address.match(/\\b(\\d{5})(?:-\\d{4})?\\b/);\n if (!postalCodeMatch) {\n return null;\n }\n const stateMatch = physical_address.match(\n /\\b(AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|DC|AS|GU|MP|PR|VI)\\b(?=\\s*\\d{5}|\\s*$)/\n );\n if (!stateMatch) {\n return null;\n }\n const cityMatch = physical_address.match(/,\\s*([^,]+?)\\s*,?\\s*[A-Z]{2}\\b/);\n if (!cityMatch?.[1]?.trim()) {\n return null;\n }\n const line1 = physical_address.slice(0, physical_address.indexOf(\",\")).trim();\n if (!line1) {\n return null;\n }\n return {\n country: \"US\",\n line1,\n locality: cityMatch[1].trim(),\n region: stateMatch[1],\n postalCode: postalCodeMatch[1],\n ...typeof latitude === \"number\" && { latitude },\n ...typeof longitude === \"number\" && { longitude }\n };\n };\n\n // ---------------------\n // mapNetBoxCustomFields\n // ---------------------\n const mapNetBoxCustomFields = (netBoxCustomFields, customFieldMappings, customFieldDefinitions, parseGlobalId) => customFieldMappings.map((mapping) => {\n const value = netBoxCustomFields[mapping.netBoxCustomFieldName];\n if (value === void 0) {\n return null;\n }\n const customFieldDefinition = customFieldDefinitions.find((definition) => definition.id === mapping.networkSiteCustomFieldDefinitionId);\n if (!customFieldDefinition) {\n return null;\n }\n switch (customFieldDefinition.type) {\n case \"DROPDOWN\": {\n const optionGlobalId = customFieldDefinition.options?.find((option) => option.option === value)?.id;\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: optionGlobalId ? parseGlobalId(optionGlobalId).id : null\n };\n }\n case \"NUMBER\": {\n const parsed = typeof value === \"number\" ? value : Number(value);\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: Number.isNaN(parsed) ? null : parsed\n };\n }\n case \"NUMBER_LIST\": {\n const parsed = typeof value === \"number\" ? value : Number(value);\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: Number.isNaN(parsed) ? [] : [parsed]\n };\n }\n case \"TEXT\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value != null ? String(value) : null\n };\n }\n case \"TEXT_LIST\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value != null ? [String(value)] : []\n };\n }\n case \"TOGGLE\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value === true || value === \"true\"\n };\n }\n case \"CHECKBOX\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value === true || value === \"true\"\n };\n }\n default:\n return null;\n }\n }).filter((field) => field !== null);\n // ------------------------------------------------------------\n\n const netboxSite = state.input.data;\n const { customFieldMappings } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { customFieldDefinitions } = state.steps[\"get-network-site-custom-field-definitions\"].output;\n const parsedAddress = parseUSAddress(netboxSite);\n const mappedFields = mapNetBoxCustomFields(netboxSite.custom_fields, customFieldMappings, customFieldDefinitions, utils.parseGlobalId);\n return {\n parsedAddress,\n mappedFields\n };\n}" }, { "name": "prepare-network-site-data-for-update", "slug": "prepare-network-site-data-for-update", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "update-network-site" } ], "outputMapper": "({ state, utils }) => {\n // ---------------\n // parseUSAddress\n // ---------------\n const parseUSAddress = (netboxSite) => {\n const { physical_address, latitude, longitude } = netboxSite;\n if (!physical_address) {\n return null;\n }\n const postalCodeMatch = physical_address.match(/\\b(\\d{5})(?:-\\d{4})?\\b/);\n if (!postalCodeMatch) {\n return null;\n }\n const stateMatch = physical_address.match(\n /\\b(AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|DC|AS|GU|MP|PR|VI)\\b(?=\\s*\\d{5}|\\s*$)/\n );\n if (!stateMatch) {\n return null;\n }\n const cityMatch = physical_address.match(/,\\s*([^,]+?)\\s*,?\\s*[A-Z]{2}\\b/);\n if (!cityMatch?.[1]?.trim()) {\n return null;\n }\n const line1 = physical_address.slice(0, physical_address.indexOf(\",\")).trim();\n if (!line1) {\n return null;\n }\n return {\n country: \"US\",\n line1,\n locality: cityMatch[1].trim(),\n region: stateMatch[1],\n postalCode: postalCodeMatch[1],\n ...typeof latitude === \"number\" && { latitude },\n ...typeof longitude === \"number\" && { longitude }\n };\n };\n\n // ---------------------\n // mapNetBoxCustomFields\n // ---------------------\n const mapNetBoxCustomFields = (netBoxCustomFields, customFieldMappings, customFieldDefinitions, parseGlobalId) => customFieldMappings.map((mapping) => {\n const value = netBoxCustomFields[mapping.netBoxCustomFieldName];\n if (value === void 0) {\n return null;\n }\n const customFieldDefinition = customFieldDefinitions.find((definition) => definition.id === mapping.networkSiteCustomFieldDefinitionId);\n if (!customFieldDefinition) {\n return null;\n }\n switch (customFieldDefinition.type) {\n case \"DROPDOWN\": {\n const optionGlobalId = customFieldDefinition.options?.find((option) => option.option === value)?.id;\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: optionGlobalId ? parseGlobalId(optionGlobalId).id : null\n };\n }\n case \"NUMBER\": {\n const parsed = typeof value === \"number\" ? value : Number(value);\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: Number.isNaN(parsed) ? null : parsed\n };\n }\n case \"NUMBER_LIST\": {\n const parsed = typeof value === \"number\" ? value : Number(value);\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: Number.isNaN(parsed) ? [] : [parsed]\n };\n }\n case \"TEXT\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value != null ? String(value) : null\n };\n }\n case \"TEXT_LIST\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value != null ? [String(value)] : []\n };\n }\n case \"TOGGLE\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value === true || value === \"true\"\n };\n }\n case \"CHECKBOX\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value === true || value === \"true\"\n };\n }\n default:\n return null;\n }\n }).filter((field) => field !== null);\n // ------------------------------------------------------------\n\n const netboxSite = state.input.data;\n const { customFieldMappings } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { customFieldDefinitions } = state.steps[\"get-network-site-custom-field-definitions\"].output;\n const parsedAddress = parseUSAddress(netboxSite);\n const mappedFields = mapNetBoxCustomFields(netboxSite.custom_fields, customFieldMappings, customFieldDefinitions, utils.parseGlobalId);\n return {\n parsedAddress,\n mappedFields\n };\n}" }, { "name": "create-network-site", "slug": "create-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // -------------------------\n // CreateNetworkSiteDocument\n // -------------------------\n const CreateNetworkSiteDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"mutation\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateNetworkSite\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateNetworkSiteInput\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"createNetworkSite\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"errors\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"code\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"message\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSite\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":191}};\n // ------------------------------------------------------------\n\n return CreateNetworkSiteDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n // ----------------------------\n // buildNetBoxExternalSourceUrl\n // ----------------------------\n const buildNetBoxExternalSourceUrl = async (netboxUrl, instanceName, datastore, utils) => {\n const externalSourceUrl = netboxUrl.replace(/\\/api\\//, \"/\");\n if (externalSourceUrl.startsWith(\"http://\") || externalSourceUrl.startsWith(\"https://\")) {\n return externalSourceUrl;\n }\n const instanceId = await datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Failed to retrieve instance ID\", \"MISSING_INSTANCE_ID\");\n }\n const baseUrl = await datastore.getItem(`instances.${instanceId}.baseURL`);\n if (!baseUrl) {\n return utils.fail(\"Failed to retrieve base URL for external source link\", \"MISSING_BASE_URL\");\n }\n const baseUrlWithoutTrailingSlash = baseUrl.replace(/\\/api\\/?$/, \"\").replace(/\\/$/, \"\");\n const pathWithoutLeadingSlash = externalSourceUrl.replace(/^\\//, \"\");\n return `${baseUrlWithoutTrailingSlash}/${pathWithoutLeadingSlash}`;\n };\n // ------------------------------------------------------------\n\n const netboxSite = state.input.data;\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { statusMap } = state.steps[\"get-network-site-custom-statuses\"].output;\n const { parsedAddress, mappedFields } = state.steps[\"prepare-network-site-data-for-create\"].output;\n if (!parsedAddress) {\n return utils.fail(\"Failed to parse address for site creation\", \"ADDRESS_PARSE_FAILED\");\n }\n const customStatusId = statusMap[netboxSite.status.label.toLowerCase()];\n return {\n input: {\n address: parsedAddress,\n ...customStatusId && { customStatusId },\n externalSourceLink: {\n externalSourceId: String(netboxSite.id),\n externalSourceSlug: netboxSite.slug,\n externalSourceUrl: await buildNetBoxExternalSourceUrl(\n netboxSite.url,\n state.steps[\"get-instance-name-from-id\"].output,\n utils.integration.datastore,\n utils\n ),\n integrationId\n },\n fields: mappedFields,\n name: netboxSite.name\n }\n };\n}", "key": "variables" } ], "nextSteps": [ { "slug": "store-created-site-mapping" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n const { networkSite, errors } = response.createNetworkSite;\n if (errors?.length || !networkSite) {\n return utils.fail(\"Failed to create network site\", \"CREATE_NETWORK_SITE_FAILED\", errors);\n }\n return networkSite;\n }" }, { "name": "store-created-site-mapping", "slug": "store-created-site-mapping", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => {\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const netboxSiteId = state.input.data.id;\n const networkSite = state.steps[\"create-network-site\"].output;\n return [\n {\n key: `netbox-site-to-gaiia.${integrationId}.${netboxSiteId}`,\n value: networkSite.id\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ] }, { "name": "update-network-site", "slug": "update-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // -------------------------\n // UpdateNetworkSiteDocument\n // -------------------------\n const UpdateNetworkSiteDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"mutation\",\"name\":{\"kind\":\"Name\",\"value\":\"UpdateNetworkSite\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"UpdateNetworkSiteInput\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"updateNetworkSite\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"errors\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"code\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"message\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSite\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":191}};\n // ------------------------------------------------------------\n\n return UpdateNetworkSiteDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n // ----------------------------\n // buildNetBoxExternalSourceUrl\n // ----------------------------\n const buildNetBoxExternalSourceUrl = async (netboxUrl, instanceName, datastore, utils) => {\n const externalSourceUrl = netboxUrl.replace(/\\/api\\//, \"/\");\n if (externalSourceUrl.startsWith(\"http://\") || externalSourceUrl.startsWith(\"https://\")) {\n return externalSourceUrl;\n }\n const instanceId = await datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Failed to retrieve instance ID\", \"MISSING_INSTANCE_ID\");\n }\n const baseUrl = await datastore.getItem(`instances.${instanceId}.baseURL`);\n if (!baseUrl) {\n return utils.fail(\"Failed to retrieve base URL for external source link\", \"MISSING_BASE_URL\");\n }\n const baseUrlWithoutTrailingSlash = baseUrl.replace(/\\/api\\/?$/, \"\").replace(/\\/$/, \"\");\n const pathWithoutLeadingSlash = externalSourceUrl.replace(/^\\//, \"\");\n return `${baseUrlWithoutTrailingSlash}/${pathWithoutLeadingSlash}`;\n };\n // ------------------------------------------------------------\n\n const netboxSite = state.input.data;\n const gaiiaSite = state.steps[\"merge-network-site-lookup\"].output.existingSite;\n const { integrationId, customFieldMappings } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { statusMap } = state.steps[\"get-network-site-custom-statuses\"].output;\n const { parsedAddress, mappedFields } = state.steps[\"prepare-network-site-data-for-update\"].output;\n const customStatusId = statusMap[netboxSite.status.label.toLowerCase()];\n const syncedFieldDefinitionIds = new Set(customFieldMappings.map((mapping) => mapping.networkSiteCustomFieldDefinitionId));\n const nonSyncedFields = gaiiaSite.fields.nodes.filter((field) => !syncedFieldDefinitionIds.has(field.definition.id)).map((field) => ({\n customFieldDefinitionId: field.definition.id,\n data: field.data\n }));\n const allFields = [...mappedFields, ...nonSyncedFields];\n return {\n input: {\n ...parsedAddress && { address: parsedAddress },\n ...customStatusId && { customStatusId },\n externalSourceLink: {\n externalSourceId: String(netboxSite.id),\n externalSourceSlug: netboxSite.slug,\n externalSourceUrl: await buildNetBoxExternalSourceUrl(\n netboxSite.url,\n state.steps[\"get-instance-name-from-id\"].output,\n utils.integration.datastore,\n utils\n ),\n integrationId\n },\n fields: allFields,\n id: gaiiaSite.id,\n name: netboxSite.name\n }\n };\n}", "key": "variables" } ], "nextSteps": [ { "slug": "store-updated-site-mapping" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n const { networkSite, errors } = response.updateNetworkSite;\n if (errors?.length || !networkSite) {\n return utils.fail(\"Failed to update network site\", \"UPDATE_NETWORK_SITE_FAILED\", errors);\n }\n return networkSite;\n }" }, { "name": "store-updated-site-mapping", "slug": "store-updated-site-mapping", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => {\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const netboxSiteId = state.input.data.id;\n const networkSite = state.steps[\"update-network-site\"].output;\n return [\n {\n key: `netbox-site-to-gaiia.${integrationId}.${netboxSiteId}`,\n value: networkSite.id\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ] } ] } }, "Update RADIUS Account Groups": { "id": "workflow_fLXTNCvridPqwEEXxf3CK1", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "accountId": { "type": "string", "description": "The account ID in gaiia" }, "status": { "type": "string", "enum": [ "ACTIVE", "INACTIVE", "SUSPENDED", "PENDING", "LEAD" ], "description": "The status of the account" }, "productIds": { "type": "array", "items": { "type": "string" }, "description": "The product IDs of the account" } }, "required": [ "accountId", "status", "productIds" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-account-references" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n const error = state.steps[\"handle-error\"];\n utils.fail(error.output.message, error.output.code, error.output.payload);\n }\n if (\"update-account-groups\" in state.steps) {\n const parallelResults = state.steps[\"update-account-groups\"];\n return {\n success: true,\n updatedAccounts: parallelResults.output.flat()\n };\n }\n return {\n success: true,\n updatedAccounts: []\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-account-references", "slug": "get-account-references", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { accountId } = state.input;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n const accounts = existingData ? JSON.parse(existingData) : [];\n if (accounts.length === 0) {\n return utils.fail(\"No RADIUS accounts found for this account ID\", \"NO_ACCOUNTS_FOUND\");\n }\n const instanceMap = new Map();\n for (const account of accounts) {\n const instanceName = await utils.integration.datastore.getItem(`instances.${account.instanceId}.name`);\n if (!instanceName) {\n continue;\n }\n if (!instanceMap.has(instanceName)) {\n instanceMap.set(instanceName, {\n instanceName,\n instanceId: account.instanceId,\n accounts: []\n });\n }\n instanceMap.get(instanceName).accounts.push(account);\n }\n return [...instanceMap.values()];\n }", "key": "function" } ], "nextSteps": [ { "slug": "update-account-groups" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "update-account-groups", "slug": "update-account-groups", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-account-references\"].output" } ], "initialSteps": [ { "slug": "get-current-groups-and-determine-expected" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ state }) => {\n if (\"update-account-groups-transaction\" in state.steps) {\n return state.steps[\"update-account-groups-transaction\"].output;\n }\n return [];\n }" }, { "name": "get-current-groups-and-determine-expected", "slug": "get-current-groups-and-determine-expected", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-query", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"update-account-groups\"].instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { accounts } = state.currentParallelLoopIterations[\"update-account-groups\"];\n return `SELECT groupname, priority, username FROM radusergroup WHERE username IN (${accounts.map((account) => account.username).map(() => \"?\").join(\", \")}) ORDER BY username, priority`;\n }", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { accounts } = state.currentParallelLoopIterations[\"update-account-groups\"];\n return accounts.map((account) => account.username);\n }", "key": "values" } ], "nextSteps": [ { "slug": "needs-update" } ], "outputMapper": "async ({ state, output, utils }) => {\n const instance = state.currentParallelLoopIterations[\"update-account-groups\"];\n const { instanceId, instanceName, accounts } = instance;\n const { status, productIds } = state.input;\n const currentGroupsResult = output.results;\n const currentGroupsByUsername = new Map();\n if (currentGroupsResult && currentGroupsResult.length > 0) {\n for (const row of currentGroupsResult) {\n if (!currentGroupsByUsername.has(row.username)) {\n currentGroupsByUsername.set(row.username, []);\n }\n currentGroupsByUsername.get(row.username).push({\n groupname: row.groupname,\n priority: row.priority\n });\n }\n }\n const groupsData = await utils.integration.datastore.getItem(`instances.${instanceId}.groups`);\n let allExpectedGroups = [];\n if (groupsData) {\n const allGroups = JSON.parse(groupsData);\n const groupsToAssign = allGroups.filter((group) => {\n switch (group.mappingRule) {\n case \"ALL\":\n return true;\n case \"ACTIVE\":\n return status === \"ACTIVE\";\n case \"INACTIVE\":\n return status === \"INACTIVE\";\n case \"SUSPENDED\":\n return status === \"SUSPENDED\";\n case \"MATCH_PRODUCT\":\n const hasMatchingProduct = group.mappedProductIds.some((mappedProductId) => productIds.includes(mappedProductId));\n return hasMatchingProduct;\n }\n return false;\n });\n allExpectedGroups = groupsToAssign.map((group) => ({\n groupName: group.groupName,\n priority: group.priority\n }));\n }\n const accountResults = accounts.map((account) => {\n const { username } = account;\n const currentGroups = currentGroupsByUsername.get(username) || [];\n const currentGroupsSet = new Set(currentGroups.map((g) => g.groupname));\n const expectedGroupsSet = new Set(allExpectedGroups.map((g) => g.groupName));\n const needsUpdate = currentGroups.length !== allExpectedGroups.length || allExpectedGroups.some((g) => !currentGroupsSet.has(g.groupName)) || currentGroups.some((g) => !expectedGroupsSet.has(g.groupname));\n return {\n username,\n instanceName,\n instanceId,\n currentGroups,\n expectedGroups: allExpectedGroups,\n needsUpdate\n };\n });\n return {\n instanceName,\n instanceId,\n accounts: accountResults\n };\n }", "parentParallelLoopSlug": "update-account-groups" }, { "name": "needs-update", "slug": "needs-update", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => state.steps[\"get-current-groups-and-determine-expected\"].output.accounts.some((account) => account.needsUpdate)", "thenSteps": [ { "slug": "update-account-groups-transaction" } ], "elseSteps": [ { "slug": "update-account-groups-end" } ], "parentParallelLoopSlug": "update-account-groups" }, { "name": "update-account-groups-transaction", "slug": "update-account-groups-transaction", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-current-groups-and-determine-expected\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { accounts } = state.steps[\"get-current-groups-and-determine-expected\"].output;\n const queries = [];\n for (const account of accounts) {\n const { username, expectedGroups, needsUpdate } = account;\n if (!needsUpdate) {\n continue;\n }\n queries.push({\n query: \"DELETE FROM radusergroup WHERE username = ?\",\n values: [username]\n });\n if (expectedGroups.length > 0) {\n queries.push({\n query: `INSERT INTO radusergroup (username, groupname, priority) VALUES ${expectedGroups.map(() => \"(?, ?, ?)\").join(\", \")}`,\n values: expectedGroups.flatMap((group) => [username, group.groupName, String(group.priority)])\n });\n }\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "update-account-groups-end" } ], "outputMapper": "({ state }) => {\n const { instanceName, accounts } = state.steps[\"get-current-groups-and-determine-expected\"].output;\n return accounts.map((account) => ({\n groups: account.expectedGroups.map((g) => g.groupName),\n instanceName,\n username: account.username\n }));\n }", "parentParallelLoopSlug": "update-account-groups" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"get-account-references\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Import Network Sites - Full Sync": { "id": "workflow_pGwirTKW5riKBk9GwiaY1W", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" }, "siteIds": { "type": "array", "items": { "type": "number" }, "description": "List of NetBox site IDs to sync (optional - if not provided, all sites will be synced)" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "get-network-site-synchronization-settings" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n const createErrors = state.steps[\"create-network-sites\"].output.flatMap((result) => result.errors);\n const updateErrors = state.steps[\"update-network-sites\"].output.flatMap((result) => result.errors);\n const allErrors = [...createErrors, ...updateErrors];\n if (allErrors.length > 0) {\n return utils.fail(\"Some network sites failed to sync\", \"PARTIAL_SYNC_FAILURE\", allErrors);\n }\n const { toCreate, toUpdate, skipped } = state.steps[\"match-netbox-sites-to-gaiia-sites\"].output;\n return {\n created: toCreate.length,\n updated: toUpdate.length,\n skipped: skipped.length\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-network-site-synchronization-settings", "slug": "get-network-site-synchronization-settings", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-get-network-site-synchronization-settings", "inputs": [ { "type": "FUNCTION", "value": "async ({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-network-site-custom-field-definitions" } ], "outputMapper": "({ output, utils }) => {\n if (!output.isEnabled) {\n return utils.fail(\"NetBox network site synchronization is disabled for this instance.\", \"NETWORK_SITE_SYNC_DISABLED\");\n }\n return output;\n }" }, { "name": "get-network-site-custom-field-definitions", "slug": "get-network-site-custom-field-definitions", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // --------------------------------------------------\n // PaginatedNetworkSiteCustomFieldDefinitionsDocument\n // --------------------------------------------------\n const PaginatedNetworkSiteCustomFieldDefinitionsDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"PaginatedNetworkSiteCustomFieldDefinitions\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"String\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSiteCustomFieldDefinitionFilters\"}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSiteCustomFieldDefinitions\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"pageInfo\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"hasNextPage\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"endCursor\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"name\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"required\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"type\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"createdAt\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"archivedAt\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"options\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"option\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":449}};\n // ------------------------------------------------------------\n\n return PaginatedNetworkSiteCustomFieldDefinitionsDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "() => ({\n first: 250,\n filters: {\n isArchived: { equals: false }\n }\n })", "key": "variables" } ], "nextSteps": [ { "slug": "get-network-site-custom-statuses" } ], "outputMapper": "({ output }) => {\n const response = output.response;\n return {\n customFieldDefinitions: response.networkSiteCustomFieldDefinitions.nodes\n };\n }" }, { "name": "get-network-site-custom-statuses", "slug": "get-network-site-custom-statuses", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // ---------------------------------\n // NetworkSiteCustomStatusesDocument\n // ---------------------------------\n const NetworkSiteCustomStatusesDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSiteCustomStatuses\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSiteCustomStatuses\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"name\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":146}};\n // ------------------------------------------------------------\n\n return NetworkSiteCustomStatusesDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "() => ({\n first: 250\n })", "key": "variables" } ], "nextSteps": [ { "slug": "get-first-page-of-sites" } ], "outputMapper": "({ output }) => {\n const response = output.response;\n const statusMap = response.networkSiteCustomStatuses.nodes.reduce(\n (map, status) => ({\n ...map,\n [status.name.toLowerCase()]: status.id\n }),\n {}\n );\n return { statusMap };\n }" }, { "name": "get-first-page-of-sites", "slug": "get-first-page-of-sites", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => {\n const parameters = new URLSearchParams({\n limit: \"100\",\n offset: \"0\"\n });\n if (state.input.siteIds && state.input.siteIds.length > 0) {\n parameters.set(\"id\", state.input.siteIds.join(\",\"));\n }\n return `/dcim/sites/?${parameters.toString()}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-paginated-sites" } ], "outputMapper": "({ output }) => {\n const response = output;\n const numberOfRemainingPages = Math.max(0, Math.ceil(response.count / 100) - 1);\n return {\n pagesToFetch: Array.from({ length: numberOfRemainingPages }, (_, index) => index + 1),\n results: response.results\n };\n }" }, { "name": "get-paginated-sites", "slug": "get-paginated-sites", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-first-page-of-sites\"].output.pagesToFetch" } ], "initialSteps": [ { "slug": "get-page-of-sites" } ], "nextSteps": [ { "slug": "merge-paginated-sites" } ], "outputMapper": "({ state }) => state.steps[\"get-page-of-sites\"].output" }, { "name": "get-page-of-sites", "slug": "get-page-of-sites", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => {\n const parameters = new URLSearchParams({\n limit: \"100\",\n offset: String(state.currentParallelLoopIterations[\"get-paginated-sites\"] * 100)\n });\n if (state.input.siteIds && state.input.siteIds.length > 0) {\n parameters.set(\"id\", state.input.siteIds.join(\",\"));\n }\n return `/dcim/sites/?${parameters.toString()}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-paginated-sites-end" } ], "outputMapper": "({ output }) => {\n const response = output;\n return response.results;\n }", "parentParallelLoopSlug": "get-paginated-sites" }, { "name": "merge-paginated-sites", "slug": "merge-paginated-sites", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "get-all-gaiia-network-sites" } ], "outputMapper": "({ state }) => {\n const { results } = state.steps[\"get-first-page-of-sites\"].output;\n const paginatedResults = state.steps[\"get-paginated-sites\"].output.flat();\n return {\n sites: [...results, ...paginatedResults]\n };\n }" }, { "name": "get-all-gaiia-network-sites", "slug": "get-all-gaiia-network-sites", "nodeSlug": "while-loop", "type": "WHILE_LOOP", "initialSteps": [ { "slug": "get-gaiia-network-sites" } ], "nextSteps": [ { "slug": "match-netbox-sites-to-gaiia-sites" } ], "condition": "({ state }) => {\n const whileLoop = state.currentWhileLoopIterations[\"get-all-gaiia-network-sites\"];\n return whileLoop.isFirstIteration || whileLoop.previousIteration.steps[\"get-gaiia-network-sites\"].output.networkSites.pageInfo.hasNextPage;\n }", "iterationOutputMapper": "({ state }) => state.steps[\"get-gaiia-network-sites\"].output", "outputMapper": "({ output }) => {\n const responses = output;\n return {\n networkSites: responses.flatMap((response) => response.networkSites.edges.map((edge) => edge.node))\n };\n }" }, { "name": "get-gaiia-network-sites", "slug": "get-gaiia-network-sites", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // ------------------------------------\n // NetworkSitesWithExternalLinkDocument\n // ------------------------------------\n const NetworkSitesWithExternalLinkDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSitesWithExternalLink\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"String\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"NetworkSiteFilters\"}},\"directives\":[]},{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"firstFields\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"Int\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSites\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"after\"}}},{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filters\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"edges\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"node\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"customStatus\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"externalSourceLink\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"externalSourceId\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"integration\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"fields\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"first\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"firstFields\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"data\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"definition\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"name\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"address\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"country\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"line1\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"locality\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"postalCode\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"region\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"latitude\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"longitude\"},\"arguments\":[],\"directives\":[]}]}}]}}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"pageInfo\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"endCursor\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"hasNextPage\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"totalCount\"},\"arguments\":[],\"directives\":[]}]}}]}}],\"loc\":{\"start\":0,\"end\":802}};\n // ------------------------------------------------------------\n\n return NetworkSitesWithExternalLinkDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => {\n const previousResponse = state.currentWhileLoopIterations[\"get-all-gaiia-network-sites\"].previousIteration?.output;\n return {\n first: 250,\n after: previousResponse?.networkSites.pageInfo.endCursor,\n firstFields: 250\n };\n }", "key": "variables" } ], "nextSteps": [ { "slug": "get-all-gaiia-network-sites-end" } ], "outputMapper": "({ output }) => {\n const response = output.response;\n return response;\n }", "parentWhileLoopSlug": "get-all-gaiia-network-sites" }, { "name": "match-netbox-sites-to-gaiia-sites", "slug": "match-netbox-sites-to-gaiia-sites", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "create-network-sites" } ], "outputMapper": "async ({ state, utils }) => {\n // ---------------\n // parseUSAddress\n // ---------------\n const parseUSAddress = (netboxSite) => {\n const { physical_address, latitude, longitude } = netboxSite;\n if (!physical_address) {\n return null;\n }\n const postalCodeMatch = physical_address.match(/\\b(\\d{5})(?:-\\d{4})?\\b/);\n if (!postalCodeMatch) {\n return null;\n }\n const stateMatch = physical_address.match(\n /\\b(AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|DC|AS|GU|MP|PR|VI)\\b(?=\\s*\\d{5}|\\s*$)/\n );\n if (!stateMatch) {\n return null;\n }\n const cityMatch = physical_address.match(/,\\s*([^,]+?)\\s*,?\\s*[A-Z]{2}\\b/);\n if (!cityMatch?.[1]?.trim()) {\n return null;\n }\n const line1 = physical_address.slice(0, physical_address.indexOf(\",\")).trim();\n if (!line1) {\n return null;\n }\n return {\n country: \"US\",\n line1,\n locality: cityMatch[1].trim(),\n region: stateMatch[1],\n postalCode: postalCodeMatch[1],\n ...typeof latitude === \"number\" && { latitude },\n ...typeof longitude === \"number\" && { longitude }\n };\n };\n\n // ---------------------\n // mapNetBoxCustomFields\n // ---------------------\n const mapNetBoxCustomFields = (netBoxCustomFields, customFieldMappings, customFieldDefinitions, parseGlobalId) => customFieldMappings.map((mapping) => {\n const value = netBoxCustomFields[mapping.netBoxCustomFieldName];\n if (value === void 0) {\n return null;\n }\n const customFieldDefinition = customFieldDefinitions.find((definition) => definition.id === mapping.networkSiteCustomFieldDefinitionId);\n if (!customFieldDefinition) {\n return null;\n }\n switch (customFieldDefinition.type) {\n case \"DROPDOWN\": {\n const optionGlobalId = customFieldDefinition.options?.find((option) => option.option === value)?.id;\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: optionGlobalId ? parseGlobalId(optionGlobalId).id : null\n };\n }\n case \"NUMBER\": {\n const parsed = typeof value === \"number\" ? value : Number(value);\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: Number.isNaN(parsed) ? null : parsed\n };\n }\n case \"NUMBER_LIST\": {\n const parsed = typeof value === \"number\" ? value : Number(value);\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: Number.isNaN(parsed) ? [] : [parsed]\n };\n }\n case \"TEXT\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value != null ? String(value) : null\n };\n }\n case \"TEXT_LIST\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value != null ? [String(value)] : []\n };\n }\n case \"TOGGLE\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value === true || value === \"true\"\n };\n }\n case \"CHECKBOX\": {\n return {\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId,\n data: value === true || value === \"true\"\n };\n }\n default:\n return null;\n }\n }).filter((field) => field !== null);\n // ------------------------------------------------------------\n\n const { sites } = state.steps[\"merge-paginated-sites\"].output;\n const { networkSites } = state.steps[\"get-all-gaiia-network-sites\"].output;\n const { integrationId, customFieldMappings } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { customFieldDefinitions } = state.steps[\"get-network-site-custom-field-definitions\"].output;\n const toCreate = [];\n const toUpdate = [];\n const skipped = [];\n for (const netboxSite of sites) {\n const datastoreKey = `netbox-site-to-gaiia.${integrationId}.${netboxSite.id}`;\n const gaiiaSiteId = await utils.integration.datastore.getItem(datastoreKey);\n const matchedByDatastore = gaiiaSiteId ? networkSites.find(\n (site) => site.id === gaiiaSiteId && site.externalSourceLink?.integration.id === integrationId && site.externalSourceLink.externalSourceId === String(netboxSite.id)\n ) : void 0;\n const matchedByName = networkSites.find((site) => !site.externalSourceLink && site.name.toLowerCase() === netboxSite.name.toLowerCase());\n const matchedSite = matchedByDatastore ?? matchedByName;\n const parsedAddress = parseUSAddress(netboxSite);\n const mappedFields = mapNetBoxCustomFields(netboxSite.custom_fields, customFieldMappings, customFieldDefinitions, utils.parseGlobalId);\n if (matchedSite) {\n toUpdate.push({\n gaiia: matchedSite,\n netbox: netboxSite,\n parsedAddress: parsedAddress ?? void 0,\n mappedFields\n });\n } else {\n if (!parsedAddress) {\n skipped.push({ site: netboxSite, reason: \"address_parse_failed\" });\n continue;\n }\n toCreate.push({\n site: netboxSite,\n parsedAddress,\n mappedFields\n });\n }\n }\n return {\n toCreate,\n toUpdate,\n skipped\n };\n}" }, { "name": "create-network-sites", "slug": "create-network-sites", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"match-netbox-sites-to-gaiia-sites\"].output.toCreate" } ], "initialSteps": [ { "slug": "create-network-site" } ], "nextSteps": [ { "slug": "update-network-sites" } ], "outputMapper": "({ state }) => {\n const step = state.steps[\"create-network-site\"];\n return {\n errors: \"error\" in step ? [step.error] : []\n };\n }", "maxConcurrentIterations": 1 }, { "name": "create-network-site", "slug": "create-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // -------------------------\n // CreateNetworkSiteDocument\n // -------------------------\n const CreateNetworkSiteDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"mutation\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateNetworkSite\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateNetworkSiteInput\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"createNetworkSite\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"errors\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"code\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"message\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSite\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":191}};\n // ------------------------------------------------------------\n\n return CreateNetworkSiteDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n // ----------------------------\n // buildNetBoxExternalSourceUrl\n // ----------------------------\n const buildNetBoxExternalSourceUrl = async (netboxUrl, instanceName, datastore, utils) => {\n const externalSourceUrl = netboxUrl.replace(/\\/api\\//, \"/\");\n if (externalSourceUrl.startsWith(\"http://\") || externalSourceUrl.startsWith(\"https://\")) {\n return externalSourceUrl;\n }\n const instanceId = await datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Failed to retrieve instance ID\", \"MISSING_INSTANCE_ID\");\n }\n const baseUrl = await datastore.getItem(`instances.${instanceId}.baseURL`);\n if (!baseUrl) {\n return utils.fail(\"Failed to retrieve base URL for external source link\", \"MISSING_BASE_URL\");\n }\n const baseUrlWithoutTrailingSlash = baseUrl.replace(/\\/api\\/?$/, \"\").replace(/\\/$/, \"\");\n const pathWithoutLeadingSlash = externalSourceUrl.replace(/^\\//, \"\");\n return `${baseUrlWithoutTrailingSlash}/${pathWithoutLeadingSlash}`;\n };\n // ------------------------------------------------------------\n\n const { site: netboxSite, parsedAddress, mappedFields } = state.currentParallelLoopIterations[\"create-network-sites\"];\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { statusMap } = state.steps[\"get-network-site-custom-statuses\"].output;\n const customStatusId = statusMap[netboxSite.status.label.toLowerCase()];\n return {\n input: {\n address: parsedAddress,\n ...customStatusId && { customStatusId },\n externalSourceLink: {\n externalSourceId: String(netboxSite.id),\n externalSourceSlug: netboxSite.slug,\n externalSourceUrl: await buildNetBoxExternalSourceUrl(netboxSite.url, state.input.instanceName, utils.integration.datastore, utils),\n integrationId\n },\n fields: mappedFields,\n name: netboxSite.name\n }\n };\n}", "key": "variables" } ], "nextSteps": [ { "slug": "store-created-site-mapping" }, { "slug": "create-network-sites-end", "errorCodes": [ "CREATE_NETWORK_SITE_FAILED" ] } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n const { networkSite, errors } = response.createNetworkSite;\n if (errors?.length || !networkSite) {\n return utils.fail(\"Failed to create network site\", \"CREATE_NETWORK_SITE_FAILED\", errors);\n }\n return networkSite;\n }", "parentParallelLoopSlug": "create-network-sites" }, { "name": "store-created-site-mapping", "slug": "store-created-site-mapping", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => {\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { site: netboxSite } = state.currentParallelLoopIterations[\"create-network-sites\"];\n const networkSite = state.steps[\"create-network-site\"].output;\n return [\n {\n key: `netbox-site-to-gaiia.${integrationId}.${netboxSite.id}`,\n value: networkSite.id\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "create-network-sites-end" } ], "parentParallelLoopSlug": "create-network-sites" }, { "name": "update-network-sites", "slug": "update-network-sites", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const { toUpdate } = state.steps[\"match-netbox-sites-to-gaiia-sites\"].output;\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { statusMap } = state.steps[\"get-network-site-custom-statuses\"].output;\n return toUpdate.filter(({ gaiia, netbox, parsedAddress, mappedFields }) => {\n const hasExternalSourceLink = gaiia.externalSourceLink?.integration.id === integrationId && gaiia.externalSourceLink.externalSourceId === String(netbox.id);\n const nameChanged = gaiia.name !== netbox.name;\n const netboxStatusId = statusMap[netbox.status.label.toLowerCase()];\n const statusChanged = netboxStatusId && gaiia.customStatus?.id !== netboxStatusId;\n const addressChanged = parsedAddress ? !gaiia.address || gaiia.address.country !== parsedAddress.country || gaiia.address.line1 !== parsedAddress.line1 || gaiia.address.locality !== parsedAddress.locality || gaiia.address.postalCode !== parsedAddress.postalCode || gaiia.address.region !== parsedAddress.region || gaiia.address.latitude !== parsedAddress.latitude || gaiia.address.longitude !== parsedAddress.longitude : false;\n const fieldsChanged = mappedFields.some((mappedField) => {\n const existingField = gaiia.fields.nodes.find((f) => f.definition.id === mappedField.customFieldDefinitionId);\n return !existingField || JSON.stringify(existingField.data) !== JSON.stringify(mappedField.data);\n });\n return !hasExternalSourceLink || nameChanged || statusChanged || addressChanged || fieldsChanged;\n });\n }" } ], "initialSteps": [ { "slug": "update-network-site" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ state }) => {\n const step = state.steps[\"update-network-site\"];\n return {\n errors: \"error\" in step ? [step.error] : []\n };\n }", "maxConcurrentIterations": 1 }, { "name": "update-network-site", "slug": "update-network-site", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // -------------------------\n // UpdateNetworkSiteDocument\n // -------------------------\n const UpdateNetworkSiteDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"mutation\",\"name\":{\"kind\":\"Name\",\"value\":\"UpdateNetworkSite\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"UpdateNetworkSiteInput\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"updateNetworkSite\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"errors\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"code\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"message\"},\"arguments\":[],\"directives\":[]}]}},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"networkSite\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":191}};\n // ------------------------------------------------------------\n\n return UpdateNetworkSiteDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n // ----------------------------\n // buildNetBoxExternalSourceUrl\n // ----------------------------\n const buildNetBoxExternalSourceUrl = async (netboxUrl, instanceName, datastore, utils) => {\n const externalSourceUrl = netboxUrl.replace(/\\/api\\//, \"/\");\n if (externalSourceUrl.startsWith(\"http://\") || externalSourceUrl.startsWith(\"https://\")) {\n return externalSourceUrl;\n }\n const instanceId = await datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Failed to retrieve instance ID\", \"MISSING_INSTANCE_ID\");\n }\n const baseUrl = await datastore.getItem(`instances.${instanceId}.baseURL`);\n if (!baseUrl) {\n return utils.fail(\"Failed to retrieve base URL for external source link\", \"MISSING_BASE_URL\");\n }\n const baseUrlWithoutTrailingSlash = baseUrl.replace(/\\/api\\/?$/, \"\").replace(/\\/$/, \"\");\n const pathWithoutLeadingSlash = externalSourceUrl.replace(/^\\//, \"\");\n return `${baseUrlWithoutTrailingSlash}/${pathWithoutLeadingSlash}`;\n };\n // ------------------------------------------------------------\n\n const { gaiia, netbox, parsedAddress, mappedFields } = state.currentParallelLoopIterations[\"update-network-sites\"];\n const { integrationId, customFieldMappings } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { statusMap } = state.steps[\"get-network-site-custom-statuses\"].output;\n const customStatusId = statusMap[netbox.status.label.toLowerCase()];\n const syncedFieldDefinitionIds = new Set(customFieldMappings.map((mapping) => mapping.networkSiteCustomFieldDefinitionId));\n const nonSyncedFields = gaiia.fields.nodes.filter((field) => !syncedFieldDefinitionIds.has(field.definition.id)).map((field) => ({\n customFieldDefinitionId: field.definition.id,\n data: field.data\n }));\n const allFields = [...mappedFields, ...nonSyncedFields];\n return {\n input: {\n ...parsedAddress && { address: parsedAddress },\n ...customStatusId && { customStatusId },\n externalSourceLink: {\n externalSourceId: String(netbox.id),\n externalSourceSlug: netbox.slug,\n externalSourceUrl: await buildNetBoxExternalSourceUrl(netbox.url, state.input.instanceName, utils.integration.datastore, utils),\n integrationId\n },\n fields: allFields,\n id: gaiia.id,\n name: netbox.name\n }\n };\n}", "key": "variables" } ], "nextSteps": [ { "slug": "store-updated-site-mapping" }, { "slug": "update-network-sites-end", "errorCodes": [ "UPDATE_NETWORK_SITE_FAILED" ] } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n const { networkSite, errors } = response.updateNetworkSite;\n if (errors?.length || !networkSite) {\n return utils.fail(\"Failed to update network site\", \"UPDATE_NETWORK_SITE_FAILED\", errors);\n }\n return networkSite;\n }", "parentParallelLoopSlug": "update-network-sites" }, { "name": "store-updated-site-mapping", "slug": "store-updated-site-mapping", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => {\n const { integrationId } = state.steps[\"get-network-site-synchronization-settings\"].output;\n const { netbox } = state.currentParallelLoopIterations[\"update-network-sites\"];\n const networkSite = state.steps[\"update-network-site\"].output;\n return [\n {\n key: `netbox-site-to-gaiia.${integrationId}.${netbox.id}`,\n value: networkSite.id\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "update-network-sites-end" } ], "parentParallelLoopSlug": "update-network-sites" } ] } }, "Create NetBox Site Update Webhook": { "id": "workflow_eZLp5qqnQxEKy3wDLjPvB6", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "get-network-site-synchronization-settings" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => state.steps[\"create-event-rule\"].output", "type": "JSON" }, "steps": [ { "name": "get-network-site-synchronization-settings", "slug": "get-network-site-synchronization-settings", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-get-network-site-synchronization-settings", "inputs": [ { "type": "FUNCTION", "value": "async ({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "list-custom-fields" } ], "outputMapper": "({ output, utils }) => {\n if (!output.isEnabled) {\n return utils.fail(\"NetBox network site synchronization is disabled for this instance.\", \"NETWORK_SITE_SYNC_DISABLED\");\n }\n return output;\n }" }, { "name": "list-custom-fields", "slug": "list-custom-fields", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/custom-fields/\"", "key": "path" }, { "type": "FUNCTION", "value": "() => ({\n name: [\"gaiia_tenant_id\", \"gaiia_instance_id\"]\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "check-tenant-id-field-exists" } ], "outputMapper": "({ output }) => {\n const results = output.results;\n return results.map((field) => field.name);\n }" }, { "name": "check-tenant-id-field-exists", "slug": "check-tenant-id-field-exists", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => state.steps[\"list-custom-fields\"].output.includes(\"gaiia_tenant_id\")", "thenSteps": [ { "slug": "check-instance-id-field-exists" } ], "elseSteps": [ { "slug": "create-tenant-id-custom-field" } ] }, { "name": "create-tenant-id-custom-field", "slug": "create-tenant-id-custom-field", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/custom-fields/\"", "key": "path" }, { "type": "FUNCTION", "value": "() => ({\n label: \"gaiia tenant ID\",\n name: \"gaiia_tenant_id\",\n type: \"text\",\n object_types: [\"extras.webhook\", \"extras.eventrule\"],\n required: false\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "check-instance-id-field-exists" } ], "outputMapper": "({ output }) => output" }, { "name": "check-instance-id-field-exists", "slug": "check-instance-id-field-exists", "nodeSlug": "condition", "type": "CONDITION", "condition": "({ state }) => state.steps[\"list-custom-fields\"].output.includes(\"gaiia_instance_id\")", "thenSteps": [ { "slug": "get-webhook-workflow" } ], "elseSteps": [ { "slug": "create-instance-id-custom-field" } ] }, { "name": "create-instance-id-custom-field", "slug": "create-instance-id-custom-field", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/custom-fields/\"", "key": "path" }, { "type": "FUNCTION", "value": "() => ({\n label: \"gaiia instance ID\",\n name: \"gaiia_instance_id\",\n type: \"text\",\n object_types: [\"extras.webhook\", \"extras.eventrule\"],\n required: false\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "get-webhook-workflow" } ], "outputMapper": "({ output }) => output" }, { "name": "get-webhook-workflow", "slug": "get-webhook-workflow", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // -----------------\n // WorkflowsDocument\n // -----------------\n const WorkflowsDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"query\",\"name\":{\"kind\":\"Name\",\"value\":\"Workflows\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filter\"}},\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"WorkflowFilter\"}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"workflows\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"filter\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"filter\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"nodes\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"latestDefinition\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"webhookUrl\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":168}};\n // ------------------------------------------------------------\n\n return WorkflowsDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "() => ({\n filter: {\n name: {\n equals: \"Import Network Sites\"\n }\n }\n })", "key": "variables" } ], "nextSteps": [ { "slug": "create-webhook" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.workflows.nodes.length === 0) {\n return utils.fail('No workflow found with the name \"Import Network Sites\"', \"NO_WORKFLOW_FOUND\");\n }\n if (response.workflows.nodes.length > 1) {\n return utils.fail('Multiple workflows found with the name \"Import Network Sites\"', \"MULTIPLE_WORKFLOWS_FOUND\");\n }\n const webhookUrl = response.workflows.nodes[0].latestDefinition?.webhookUrl;\n if (!webhookUrl) {\n return utils.fail('The workflow \"Import Network Sites\" does not have a webhook URL defined', \"NO_WEBHOOK_URL_DEFINED\");\n }\n return response.workflows.nodes[0];\n }" }, { "name": "create-webhook", "slug": "create-webhook", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/webhooks/\"", "key": "path" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n return {\n name: `${state.context.tenant.name} - Site Update Webhook`,\n payload_url: `${state.steps[\"get-webhook-workflow\"].output.latestDefinition?.webhookUrl}?instanceId=${instanceId}`,\n custom_fields: {\n gaiia_tenant_id: state.context.tenant.id,\n gaiia_instance_id: instanceId\n }\n };\n }", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "create-event-rule" } ], "outputMapper": "({ output }) => output" }, { "name": "create-event-rule", "slug": "create-event-rule", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"POST\"", "key": "method" }, { "type": "FUNCTION", "value": "() => \"/extras/event-rules/\"", "key": "path" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => ({\n name: `${state.context.tenant.name} - Site Update Event Rule`,\n event_types: [\"object_updated\", \"object_created\", \"object_deleted\"],\n object_types: [\"dcim.site\"],\n type_create: true,\n type_update: true,\n type_delete: true,\n action_type: \"webhook\",\n action_object_type: \"extras.webhook\",\n action_object_id: state.steps[\"create-webhook\"].output.id,\n custom_fields: {\n gaiia_tenant_id: state.context.tenant.id,\n gaiia_instance_id: await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`)\n }\n })", "key": "body" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "create-external-source-claim" } ], "outputMapper": "({ output }) => output" }, { "name": "create-external-source-claim", "slug": "create-external-source-claim", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // ----------------------------------\n // CreateExternalSourceClaimsDocument\n // ----------------------------------\n const CreateExternalSourceClaimsDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"mutation\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateExternalSourceClaims\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateExternalSourceClaimsInput\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"createExternalSourceClaims\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"errors\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"code\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"message\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":185}};\n // ------------------------------------------------------------\n\n return CreateExternalSourceClaimsDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "() => ({\n input: {\n claims: [\n {\n claimType: \"OBJECT\",\n entityType: \"NETWORK_SITE\"\n }\n ]\n }\n })", "key": "variables" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.createExternalSourceClaims.errors?.length) {\n return utils.fail(\"Failed to create external source claim\", \"CREATE_EXTERNAL_SOURCE_CLAIM_FAILED\", response.createExternalSourceClaims.errors);\n }\n return response.createExternalSourceClaims;\n }" } ] } }, "Remove NetBox Site Update Webhook": { "id": "workflow_cBNb5ana2UuvrE5XKYgrbQ", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "list-event-rules" } ], "type": "MANUAL" }, "steps": [ { "name": "list-event-rules", "slug": "list-event-rules", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n return `/extras/event-rules?cf_gaiia_tenant_id=${state.context.tenant.id}&cf_gaiia_instance_id=${instanceId}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-event-rules" } ], "outputMapper": "({ output }) => {\n const response = output;\n return response;\n }" }, { "name": "delete-event-rules", "slug": "delete-event-rules", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const eventRuleName = `${state.context.tenant.name} - Site Update Event Rule`;\n return state.steps[\"list-event-rules\"].output.results.filter((rule) => rule.name === eventRuleName);\n }" } ], "initialSteps": [ { "slug": "delete-event-rule" } ], "nextSteps": [ { "slug": "list-webhooks" } ] }, { "name": "delete-event-rule", "slug": "delete-event-rule", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"DELETE\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => `extras/event-rules/${state.currentParallelLoopIterations[\"delete-event-rules\"].id}`", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-event-rules-end" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "delete-event-rules" }, { "name": "list-webhooks", "slug": "list-webhooks", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"GET\"", "key": "method" }, { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n return `/extras/webhooks?cf_gaiia_tenant_id=${state.context.tenant.id}&cf_gaiia_instance_id=${instanceId}`;\n }", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-webhooks" } ], "outputMapper": "({ output }) => {\n const response = output;\n return response;\n }" }, { "name": "delete-webhooks", "slug": "delete-webhooks", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const webhookName = `${state.context.tenant.name} - Site Update Webhook`;\n return state.steps[\"list-webhooks\"].output.results.filter((webhook) => webhook.name === webhookName);\n }" } ], "initialSteps": [ { "slug": "delete-webhook" } ], "nextSteps": [ { "slug": "end" } ] }, { "name": "delete-webhook", "slug": "delete-webhook", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-http-request", "inputs": [ { "type": "FUNCTION", "value": "() => \"DELETE\"", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => `extras/webhooks/${state.currentParallelLoopIterations[\"delete-webhooks\"].id}`", "key": "path" }, { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "delete-webhooks-end" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "delete-webhooks" } ] } }, "Update Network Site Custom Field Claims": { "id": "workflow_dznKDNB7pHL75PAoAPh1Jd", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "get-network-site-synchronization-settings" } ], "type": "MANUAL" }, "steps": [ { "name": "get-network-site-synchronization-settings", "slug": "get-network-site-synchronization-settings", "type": "CUSTOM_WORKFLOW", "nodeSlug": "netbox-get-network-site-synchronization-settings", "inputs": [ { "type": "FUNCTION", "value": "async ({ state }) => state.input.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "update-custom-field-claims" } ], "outputMapper": "({ output }) => output" }, { "name": "update-custom-field-claims", "slug": "update-custom-field-claims", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => {\n // ----------------------------------\n // CreateExternalSourceClaimsDocument\n // ----------------------------------\n const CreateExternalSourceClaimsDocument = {\"kind\":\"Document\",\"definitions\":[{\"kind\":\"OperationDefinition\",\"operation\":\"mutation\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateExternalSourceClaims\"},\"variableDefinitions\":[{\"kind\":\"VariableDefinition\",\"variable\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}},\"type\":{\"kind\":\"NonNullType\",\"type\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"CreateExternalSourceClaimsInput\"}}},\"directives\":[]}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"createExternalSourceClaims\"},\"arguments\":[{\"kind\":\"Argument\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"},\"value\":{\"kind\":\"Variable\",\"name\":{\"kind\":\"Name\",\"value\":\"input\"}}}],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"errors\"},\"arguments\":[],\"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"code\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\",\"name\":{\"kind\":\"Name\",\"value\":\"message\"},\"arguments\":[],\"directives\":[]}]}}]}}]}}],\"loc\":{\"start\":0,\"end\":185}};\n // ------------------------------------------------------------\n\n return CreateExternalSourceClaimsDocument;\n}", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { customFieldMappings } = state.steps[\"get-network-site-synchronization-settings\"].output;\n return {\n input: {\n claims: customFieldMappings.map((mapping) => ({\n claimType: \"CUSTOM_FIELD\",\n entityType: \"NETWORK_SITE\",\n customFieldDefinitionId: mapping.networkSiteCustomFieldDefinitionId\n }))\n }\n };\n }", "key": "variables" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output, utils }) => {\n const response = output.response;\n if (response.createExternalSourceClaims.errors?.length) {\n return utils.fail(\"Failed to create custom field claims\", \"CREATE_CUSTOM_FIELD_CLAIMS_FAILED\", response.createExternalSourceClaims.errors);\n }\n return response.createExternalSourceClaims;\n }" } ] } }, "Get NetBox network site synchronization settings": { "id": "workflow_mDtdvEiFAYpNFrgmhN9xqr", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string" } }, "required": [ "instanceName" ] }, "nextSteps": [ { "slug": "get-network-site-synchronization-settings" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => ({ ...state.steps[\"get-network-site-synchronization-settings\"].output })", "type": "JSON" }, "steps": [ { "name": "get-network-site-synchronization-settings", "slug": "get-network-site-synchronization-settings", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "async ({ state, utils }) => {\n const instanceId = await utils.integration.datastore.getItem(`instances.${state.input.instanceName}.id`);\n if (!instanceId) {\n return utils.fail(`No instance found with name ${state.input.instanceName}`, \"INSTANCE_NOT_FOUND\");\n }\n const integrationId = await utils.integration.datastore.getItem(\"integrationId\");\n if (!integrationId) {\n return utils.fail(\"Integration ID not found in datastore\", \"INTEGRATION_ID_NOT_FOUND\");\n }\n const networkSiteSynchronizationSettingsString = await utils.integration.datastore.getItem(`instances.${instanceId}.networkSiteSynchronization`);\n if (networkSiteSynchronizationSettingsString) {\n try {\n const parsed = JSON.parse(networkSiteSynchronizationSettingsString);\n return {\n ...parsed,\n instanceId,\n integrationId\n };\n } catch (error) {\n return utils.fail(\"Failed to parse network site synchronization settings\", \"INVALID_JSON_FORMAT\", { error });\n }\n }\n return {\n customFieldMappings: [],\n instanceId,\n integrationId,\n isEnabled: false\n };\n }" } ] } }, "Get Account ID from RADIUS Username": { "id": "workflow_4aLJbygeEqSwVnq9h95mzp", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "username": { "type": "string", "description": "The RADIUS username to search for" } }, "required": [ "username" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-all-instances" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => ({\n accounts: state.steps[\"collect-accounts\"].output\n })", "type": "JSON" }, "steps": [ { "name": "get-all-instances", "slug": "get-all-instances", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ utils }) => {\n const instancesId = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesId) {\n return utils.fail(\"No instances found in datastore\", \"NO_INSTANCES_FOUND\");\n }\n const instanceIds = instancesId.split(\",\").map((id) => id.trim());\n if (instanceIds.length === 0) {\n return utils.fail(\"No instance ids found\", \"NO_INSTANCE_IDS_FOUND\");\n }\n const instances = [];\n for (const instanceId of instanceIds) {\n const name = await utils.integration.datastore.getItem(`instances.${instanceId}.name`);\n if (name) {\n instances.push({ instanceId, instanceName: name });\n }\n }\n if (instances.length === 0) {\n return utils.fail(\"No valid instances found\", \"NO_VALID_INSTANCES\");\n }\n return instances;\n }", "key": "function" } ], "nextSteps": [ { "slug": "check-instances" } ], "outputMapper": "({ output }) => output" }, { "name": "check-instances", "slug": "check-instances", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-all-instances\"].output" } ], "initialSteps": [ { "slug": "check-instance-mapping" } ], "nextSteps": [ { "slug": "collect-accounts" } ], "outputMapper": "({ state }) => state.steps[\"check-instance-mapping\"].output" }, { "name": "check-instance-mapping", "slug": "check-instance-mapping", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceId, instanceName } = state.currentParallelLoopIterations[\"check-instances\"];\n const { username } = state.input;\n const accountId = await utils.integration.datastore.getItem(`radiusUsername.${instanceId}.${username}`);\n if (accountId) {\n return {\n accountId,\n instanceName\n };\n }\n return false;\n }", "key": "function" } ], "nextSteps": [ { "slug": "check-instances-end" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "check-instances" }, { "name": "collect-accounts", "slug": "collect-accounts", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state, utils }) => {\n const { username } = state.input;\n const results = state.steps[\"check-instances\"].output;\n const accounts = results.filter((result) => result !== false).map((result) => ({\n accountId: result.accountId,\n instanceName: result.instanceName\n }));\n if (accounts.length === 0) {\n return utils.fail(\n `RADIUS username \"${username}\" not found in any instance. The reverse index keys \"radiusUsername.{instanceId}.${username}\" are maintained by the addRadiusAccount and deleteRadiusAccount workflows. Run the backfillRadiusUsernameMapping.ts script to create mappings for existing accounts.`,\n \"USERNAME_NOT_FOUND\"\n );\n }\n return accounts;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "List Accounts With RADIUS Accounts": { "id": "workflow_fUsK1CmtC4mcrDB38TyR1K", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "maxExpectedAccounts": { "type": "number", "description": "Maximum expected number of accounts to paginate through (default: 10000)" } }, "additionalProperties": false }, "nextSteps": [ { "slug": "prepare-page-array" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => {\n const results = state.steps[\"get-radius-accounts-loop\"].output;\n const accountsWithRadius = results.filter((result) => result.radiusUsernames.length > 0).map((result) => ({\n accountId: result.account.id,\n accountName: `${result.account.primaryContact.firstName} ${result.account.primaryContact.lastName}`.trim(),\n radiusUsernames: result.radiusUsernames\n }));\n return { accounts: accountsWithRadius };\n }", "type": "JSON" }, "steps": [ { "name": "prepare-page-array", "slug": "prepare-page-array", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const PAGE_SIZE = 250;\n const maxExpectedAccounts = state.input.maxExpectedAccounts ?? 1e4;\n const totalPages = Math.ceil(maxExpectedAccounts / PAGE_SIZE);\n return Array.from({ length: totalPages }, (_, index) => index);\n }", "key": "function" } ], "nextSteps": [ { "slug": "fetch-accounts-paginated" } ], "outputMapper": "({ output }) => output" }, { "name": "fetch-accounts-paginated", "slug": "fetch-accounts-paginated", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"prepare-page-array\"].output" } ], "initialSteps": [ { "slug": "read-cursor" } ], "nextSteps": [ { "slug": "flatten-accounts" } ], "outputMapper": "({ state }) => state.steps[\"fetch-accounts-batch\"].output.accounts", "maxConcurrentIterations": 1 }, { "name": "read-cursor", "slug": "read-cursor", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ utils }) => {\n const DATASTORE_KEY = \"_temp.listAccounts.cursor\";\n const cursor = await utils.integration.datastore.getItem(DATASTORE_KEY);\n if (cursor === \"__DONE__\") {\n return { cursor: null, done: true };\n }\n return { cursor: cursor || null, done: false };\n }", "key": "function" } ], "nextSteps": [ { "slug": "fetch-accounts-batch" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "fetch-accounts-paginated" }, { "name": "fetch-accounts-batch", "slug": "fetch-accounts-batch", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n query ListAccounts($first: Int!, $after: String) {\n accounts(first: $first, after: $after) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n primaryContact {\n firstName\n lastName\n }\n }\n }\n }\n `", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => ({\n first: 250,\n after: state.steps[\"read-cursor\"].output.done ? null : state.steps[\"read-cursor\"].output.cursor\n })", "key": "variables" } ], "nextSteps": [ { "slug": "store-cursor" } ], "outputMapper": "({ state, output }) => {\n if (state.steps[\"read-cursor\"].output.done) {\n return {\n accounts: [],\n hasNextPage: false,\n endCursor: null\n };\n }\n const response = output.response;\n return {\n accounts: response.accounts.nodes,\n hasNextPage: response.accounts.pageInfo.hasNextPage,\n endCursor: response.accounts.pageInfo.endCursor\n };\n }", "parentParallelLoopSlug": "fetch-accounts-paginated" }, { "name": "store-cursor", "slug": "store-cursor", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => {\n const DATASTORE_KEY = \"_temp.listAccounts.cursor\";\n const { done } = state.steps[\"read-cursor\"].output;\n if (done) {\n return [];\n }\n const { hasNextPage, endCursor } = state.steps[\"fetch-accounts-batch\"].output;\n return [\n {\n key: DATASTORE_KEY,\n value: hasNextPage ? endCursor ?? \"__DONE__\" : \"__DONE__\"\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "fetch-accounts-paginated-end" } ], "outputMapper": "() => ({ stored: true })", "parentParallelLoopSlug": "fetch-accounts-paginated" }, { "name": "flatten-accounts", "slug": "flatten-accounts", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const paginatedResults = state.steps[\"fetch-accounts-paginated\"].output;\n return paginatedResults.flat();\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-radius-accounts-loop" } ], "outputMapper": "({ output }) => output" }, { "name": "get-radius-accounts-loop", "slug": "get-radius-accounts-loop", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => state.steps[\"flatten-accounts\"].output" } ], "initialSteps": [ { "slug": "get-radius-accounts" } ], "nextSteps": [ { "slug": "cleanup-temp-cursor" } ], "outputMapper": "({ state }) => state.steps[\"get-radius-accounts\"].output", "maxConcurrentIterations": 5 }, { "name": "get-radius-accounts", "slug": "get-radius-accounts", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const account = state.currentParallelLoopIterations[\"get-radius-accounts-loop\"];\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${account.id}`);\n const accounts = existingData ? JSON.parse(existingData) : [];\n return {\n account,\n radiusUsernames: accounts.map((a) => a.username)\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-radius-accounts-loop-end" } ], "outputMapper": "({ output }) => output", "parentParallelLoopSlug": "get-radius-accounts-loop" }, { "name": "cleanup-temp-cursor", "slug": "cleanup-temp-cursor", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "() => [\n {\n key: \"_temp.listAccounts.cursor\",\n value: \"\"\n }\n ]", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({ cleaned: true })" } ] } }, "Get RADIUS Mappings by Product ID": { "id": "workflow_uKTVe9vpHC1ZWi4KpqZQHD", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "productId": { "type": "string", "description": "The product ID to look up RADIUS mappings for" } }, "required": [ "productId" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-mappings" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => ({\n groups: state.steps[\"get-mappings\"].output\n })", "type": "JSON" }, "steps": [ { "name": "get-mappings", "slug": "get-mappings", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "async ({ state, utils }) => {\n const { productId } = state.input;\n const instancesString = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesString) {\n return [];\n }\n const instanceIds = instancesString.split(\",\").map((id) => id.trim()).filter((id) => id.length > 0);\n const mappings = [];\n for (const instanceId of instanceIds) {\n const instanceName = await utils.integration.datastore.getItem(`instances.${instanceId}.name`);\n if (!instanceName) {\n continue;\n }\n const groupsData = await utils.integration.datastore.getItem(`instances.${instanceId}.groups`);\n if (!groupsData) {\n continue;\n }\n const groups = JSON.parse(groupsData);\n for (const group of groups) {\n if (group.mappingRule !== \"MATCH_PRODUCT\") {\n continue;\n }\n if (!group.mappedProductIds?.includes(productId)) {\n continue;\n }\n mappings.push({\n instanceId,\n instanceName,\n groupName: group.groupName,\n priority: group.priority,\n mappingRule: group.mappingRule,\n mappedProductIds: group.mappedProductIds\n });\n }\n }\n return mappings;\n }" } ] } } } }