Files
n8n-enterprise-unlocked/packages/workflow/src/TelemetryHelpers.ts
T
336fc9e2a8 feat(core): Allow credential reuse on HTTP Request node (#3228)
* ✨ Create controller

* ⚡ Mount controller

* ✏️ Add error messages

* ✨ Create scopes fetcher

* ⚡ Account for non-existent credential type

* 📘 Type scopes request

* ⚡ Adjust error message

* 🧪 Add tests

* ✨ Introduce simple node versioning

* ⚡ Add example how to read version in node-code for custom logic

* 🐛 Fix setting of parameters

* 🐛 Fix another instance where it sets the wrong parameter

* ⚡ Remove unnecessary TOODs

* ✨ Re-version HTTP Request node

* 👕 Satisfy linter

* ⚡ Retrieve node version

* ⏪ Undo Jan's changes to Set node

* 🧪 Fix CI/CD for `/oauth2-credential` tests (#3230)

* 🐛 Fix notice warning missing background color (#3231)

* 🐛 Check for generic auth in node cred types

* ⚡ Refactor credentials dropdown for HTTP Request node (#3222)

* ⚡ Discoverability flow (#3229)

* ✨ Added node credentials type proxy. Changed node credentials input order.

* ⚡ Add computed property from versioning branch

* 🐛 Fix cred ref lost and unsaved

* ⚡ Make options consistent with cred type names

* ⚡ Use prop to set component order

* ⚡ Use constant and version

* ⚡ Fix rendering for generic auth creds

* ⚡ Mark as required on first selection

* ⚡ Implement discoverability flow

* ⚡ Mark as required on subsequent selections

* ⚡ Fix marking as required after cred deletion

* ⚡ Refactor to clean up

* ⚡ Detect position automatically

* ⚡ Add i18n to option label

* ⚡ Hide subtitle for custom action

* ⚡ Detect active credential type

* ⚡ Prop drilling to re-render select

* 🔥 Remove unneeded property

* ✏️ Rename arg

* 🔥 Remove unused import

* 🔥 Remove unneeded getters

* 🔥 Remove unused import

* ⚡ Generalize cred component positioning

* ⚡ Set up request

* 🐛 Fix edge case in endpoint

* ⚡ Display scopes alert box

* ⏪ Revert "Generalize cred comp positioning"

This reverts commit 75eea89273b854110fa6d1f96c7c1d78dd3b0731.

* ⚡ Consolidate HTTPRN check

* ⚡ Fix hue percentage to degree

* 🔥 Remove unused import

* 🔥 Remove unused import

* 🔥 Remove unused class

* 🔥 Remove unused import

* 📘 Create type for HTTPRN v2 auth params

* ✏️ Rename check

* 🔥 Remove unused import

* ✏️ Add i18n to `reportUnsetCredential()`

* ⚡ Refactor Alex's spacing changes

* ⚡ Post-merge fixes

* ⚡ Add docs link

* 🔥 Exclude Notion OAuth cred

* ✏️ Update copy

* ✏️ Rename param

* 🎨 Reposition notice and simplify styling

* ✏️ Update copy

* ✏️ Update copy

* ⚡ Hide params during custom action

* ⚡ Show notice if any cred type supported

* 🐛 Prevent scopes text overflow

* 🔥 Remove superfluous check

* ✏️ Break up docstring

* 🎨 Tweak notice styling

* ⚡ Reorder cred param in Webhook node

* ✏️ Shorten cred name in scopes notice

* 🧪 Update Notice snapshots

* 🐛 Fix check when `globalRole` is `undefined`

* ⏪ Revert 3f2c4a6

* ⚡ Apply feedback from Product

* 🧪 Update snapshot

* ⚡ Adjust regex expansion pattern for singular

* 🔥 Remove unused import

* 🔥 Remove logging

* ⚡ Make `somethingElse` key more unique

* ⚡ Move something else to constants

* ⚡ Consolidate notice component

* ⚡ Apply latest feedback

* 🧪 Update tests

* 🧪 Update snapshot

* ✏️ Fix singular version

* 🧪 Finalize tests

* ✏️ Rename constant

* 🧪 Expand tests

* 🔥 Remove `truncate` prop

* 🚚 Move scopes fetching to store

* 🚚 Move method to component

* ⚡ Use constant

* ⚡ Refactor `Notice` component

* 🧪 Update tests

* 🔥 Remove unused keys

* ⚡ Inject custom API call option

* 🔥 Remove unused props

* 🎨 Use `compact` prop

* 🧪 Update snapshots

* 🚚 Move scopes to store

* 🚚 Move `nodeCredentialTypes` to parent

* ✏️ Rename cred types per branding

* 🐛 Clear scopes when none

* ⚡ Add default

* 🚚 Move `newHttpRequestNodeCredentialType` to parent

* 🔥 Remove test data

* ⚡ Separate lines for readability

* ⚡ Change reference from node to node name

* ✏️ Rename i18n keys

* ⚡ Refactor OAuth check

* 🔥 Remove unused key

* 🚚 Move `OAuth1/2 API` to i18n

* ⚡ Refactor `skipCheck`

* ⚡ Add `stopPropagation` and `preventDefault`

* 🚚 Move active credential scopes logic to store

* 🎨 Fix spacing for `NodeWebhooks` component

* ⚡ Implement feedback

* ⚡ Update HTTPRN default and issue copy

* Refactor to use `CredentialsSelect` param (#3304)

* ⚡ Refactor into cred type param

* ⚡ Componentize scopes notice

* 🔥 Remove unused data

* 🔥 Remove unused `loadOptions`

* ⚡ Componentize `NodeCredentialType`

* 🐛 Fix param validation

* 🔥 Remove dup methods

* ⚡ Refactor all references to `isHttpRequestNodeV2`

* 🎨 Fix styling

* 🔥 Remove unused import

* 🔥 Remove unused properties

* 🎨 Fix spacing for Pipedrive Trigger node

* 🎨 Undo Webhook node styling change

* 🔥 Remove unused style

* ⚡ Cover `httpHeaderAuth` edge case

* 🐛 Fix `this.node` reference

* 🚚 Rename to `credentialsSelect`

* 🐛 Fix mistaken renaming

* ⚡ Set one attribute per line

* ⚡ Move condition to instantiation site

* 🚚 Rename prop

* ⚡ Refactor away `prepareScopesNotice`

* ✏️ Rename i18n keys

* ✏️ Update i18n calls

* ✏️ Add more i18n keys

* 🔥 Remove unused props

* ✏️ Add explanatory comment

* ⚡ Adjust check in `hasProxyAuth`

* ⚡ Refactor `credentialSelected` from prop to event

* ⚡ Eventify `valueChanged`, `setFocus`, `onBlur`

* ⚡ Eventify `optionSelected`

* ⚡ Add `noDataExpression`

* 🔥 Remove logging

* 🔥 Remove URL from scopes

* ⚡ Disregard expressions for display

* 🎨 Use CSS modules

* 📘 Tigthen interface

* 🐛 Fix generic auth display

* 🐛 Fix generic auth validation

* 📘 Loosen type

* 🚚 Move event params to end

* ⚡ Generalize reference

* ⚡ Refactor generic auth as `credentialsSelect` param

* ⏪ Restore check for `httpHeaderAuth `

* 🚚 Rename `existing` to `predefined`

* Extend metrics for HTTP Request node (#3282)

* ⚡ Extend metrics

* 🧪 Add tests

* ⚡ Update param names

Co-authored-by: Alex Grozav <alex@grozav.com>

* ⚡ Update check per new branch

* ⚡ Include generic auth check

* ⚡ Adjust telemetry (#3359)

* ⚡ Filter credential types by label

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
2022-05-24 11:36:19 +02:00

222 lines
6.5 KiB
TypeScript

/* eslint-disable import/no-cycle */
import {
IConnection,
INode,
INodeNameIndex,
INodesGraph,
INodeGraphItem,
INodesGraphResult,
IWorkflowBase,
INodeTypes,
} from '.';
import { INodeType } from './Interfaces';
import { getInstance as getLoggerInstance } from './LoggerProxy';
const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined {
return workflow.nodes.find((node) => node.name === nodeName);
}
export function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
function getStickyDimensions(note: INode, stickyType: INodeType | undefined) {
const heightProperty = stickyType?.description.properties.find(
(property) => property.name === 'height',
);
const widthProperty = stickyType?.description.properties.find(
(property) => property.name === 'width',
);
const defaultHeight =
heightProperty && isNumber(heightProperty?.default) ? heightProperty.default : 0;
const defaultWidth =
widthProperty && isNumber(widthProperty?.default) ? widthProperty.default : 0;
const height: number = isNumber(note.parameters.height) ? note.parameters.height : defaultHeight;
const width: number = isNumber(note.parameters.width) ? note.parameters.width : defaultWidth;
return {
height,
width,
};
}
type XYPosition = [number, number];
function areOverlapping(
topLeft: XYPosition,
bottomRight: XYPosition,
targetPos: XYPosition,
): boolean {
return (
targetPos[0] > topLeft[0] &&
targetPos[1] > topLeft[1] &&
targetPos[0] < bottomRight[0] &&
targetPos[1] < bottomRight[1]
);
}
const URL_PARTS_REGEX = /(?<protocolPlusDomain>.*?\..*?)(?<pathname>\/.*)/;
export function getDomainBase(raw: string, urlParts = URL_PARTS_REGEX): string {
try {
const url = new URL(raw);
return [url.protocol, url.hostname].join('//');
} catch (_) {
const match = urlParts.exec(raw);
if (!match?.groups?.protocolPlusDomain) return '';
return match.groups.protocolPlusDomain;
}
}
function isSensitive(segment: string) {
if (/^v\d+$/.test(segment)) return false;
return /%40/.test(segment) || /\d/.test(segment) || /^[0-9A-F]{8}/i.test(segment);
}
export const ANONYMIZATION_CHARACTER = '*';
function sanitizeRoute(raw: string, check = isSensitive, char = ANONYMIZATION_CHARACTER) {
return raw
.split('/')
.map((segment) => (check(segment) ? char.repeat(segment.length) : segment))
.join('/');
}
/**
* Return pathname plus query string from URL, anonymizing IDs in route and query params.
*/
export function getDomainPath(raw: string, urlParts = URL_PARTS_REGEX): string {
try {
const url = new URL(raw);
if (!url.hostname) throw new Error('Malformed URL');
return sanitizeRoute(url.pathname);
} catch (_) {
const match = urlParts.exec(raw);
if (!match?.groups?.pathname) return '';
// discard query string
const route = match.groups.pathname.split('?').shift() as string;
return sanitizeRoute(route);
}
}
export function generateNodesGraph(
workflow: IWorkflowBase,
nodeTypes: INodeTypes,
): INodesGraphResult {
const nodesGraph: INodesGraph = {
node_types: [],
node_connections: [],
nodes: {},
notes: {},
};
const nodeNameAndIndex: INodeNameIndex = {};
try {
const notes = workflow.nodes.filter((node) => node.type === STICKY_NODE_TYPE);
const otherNodes = workflow.nodes.filter((node) => node.type !== STICKY_NODE_TYPE);
notes.forEach((stickyNote: INode, index: number) => {
const stickyType = nodeTypes.getByNameAndVersion(STICKY_NODE_TYPE, stickyNote.typeVersion);
const { height, width } = getStickyDimensions(stickyNote, stickyType);
const topLeft = stickyNote.position;
const bottomRight: [number, number] = [topLeft[0] + width, topLeft[1] + height];
const overlapping = Boolean(
otherNodes.find((node) => areOverlapping(topLeft, bottomRight, node.position)),
);
nodesGraph.notes[index] = {
overlapping,
position: topLeft,
height,
width,
};
});
otherNodes.forEach((node: INode, index: number) => {
nodesGraph.node_types.push(node.type);
const nodeItem: INodeGraphItem = {
type: node.type,
position: node.position,
};
if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion === 1) {
try {
nodeItem.domain = new URL(node.parameters.url as string).hostname;
} catch (_) {
nodeItem.domain = getDomainBase(node.parameters.url as string);
}
} else if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion === 2) {
const { authentication } = node.parameters as { authentication: string };
nodeItem.credential_type = {
none: 'none',
genericCredentialType: node.parameters.genericAuthType as string,
existingCredentialType: node.parameters.nodeCredentialType as string,
}[authentication];
nodeItem.credential_set = node.credentials
? Object.keys(node.credentials).length > 0
: false;
const { url } = node.parameters as { url: string };
nodeItem.domain_base = getDomainBase(url);
nodeItem.domain_path = getDomainPath(url);
nodeItem.method = node.parameters.requestMethod as string;
} else {
const nodeType = nodeTypes.getByNameAndVersion(node.type);
nodeType?.description.properties.forEach((property) => {
if (
property.name === 'operation' ||
property.name === 'resource' ||
property.name === 'mode'
) {
nodeItem[property.name] = property.default ? property.default.toString() : undefined;
}
});
nodeItem.operation = node.parameters.operation?.toString() ?? nodeItem.operation;
nodeItem.resource = node.parameters.resource?.toString() ?? nodeItem.resource;
nodeItem.mode = node.parameters.mode?.toString() ?? nodeItem.mode;
}
nodesGraph.nodes[`${index}`] = nodeItem;
nodeNameAndIndex[node.name] = index.toString();
});
const getGraphConnectionItem = (startNode: string, connectionItem: IConnection) => {
return { start: nodeNameAndIndex[startNode], end: nodeNameAndIndex[connectionItem.node] };
};
Object.keys(workflow.connections).forEach((nodeName) => {
const connections = workflow.connections[nodeName];
connections.main.forEach((element) => {
element.forEach((element2) => {
nodesGraph.node_connections.push(getGraphConnectionItem(nodeName, element2));
});
});
});
} catch (e) {
const logger = getLoggerInstance();
logger.warn(`Failed to generate nodes graph for workflowId: ${workflow.id as string | number}`);
logger.warn((e as Error).message);
logger.warn((e as Error).stack ?? '');
}
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex };
}