network/gaiia/workflows_backup_2026-07-10_125815.json
2026-07-19 14:42:02 -05:00

6980 lines
No EOL
405 KiB
JSON

{
"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: `<div>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.</div><div><br></div><div>Once the equipment has been recovered, please navigate to the account page and unassign the equipment from the account.</div><div><br></div><div>-Field Service Team</div>`,\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 }"
}
]
}
}
}
}