mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Expose View Execution links for erroneous sub-executions (#13185)
This commit is contained in:
31
packages/workflow/src/MetadataUtils.ts
Normal file
31
packages/workflow/src/MetadataUtils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { ITaskMetadata } from '.';
|
||||
import { hasKey } from './utils';
|
||||
|
||||
function responseHasSubworkflowData(
|
||||
response: unknown,
|
||||
): response is { executionId: string; workflowId: string } {
|
||||
return ['executionId', 'workflowId'].every(
|
||||
(x) => hasKey(response, x) && typeof response[x] === 'string',
|
||||
);
|
||||
}
|
||||
|
||||
type ISubWorkflowMetadata = Required<Pick<ITaskMetadata, 'subExecution' | 'subExecutionsCount'>>;
|
||||
|
||||
function parseErrorResponseWorkflowMetadata(response: unknown): ISubWorkflowMetadata | undefined {
|
||||
if (!responseHasSubworkflowData(response)) return undefined;
|
||||
|
||||
return {
|
||||
subExecution: {
|
||||
executionId: response.executionId,
|
||||
workflowId: response.workflowId,
|
||||
},
|
||||
subExecutionsCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseErrorMetadata(error: unknown): ISubWorkflowMetadata | undefined {
|
||||
if (hasKey(error, 'errorResponse')) {
|
||||
return parseErrorResponseWorkflowMetadata(error.errorResponse);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
IDataObject,
|
||||
IStatusCodeMessages,
|
||||
Functionality,
|
||||
RelatedExecution,
|
||||
} from '../Interfaces';
|
||||
import { removeCircularRefs } from '../utils';
|
||||
|
||||
@@ -30,6 +31,10 @@ export interface NodeOperationErrorOptions {
|
||||
messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node
|
||||
functionality?: Functionality;
|
||||
type?: string;
|
||||
metadata?: {
|
||||
subExecution?: RelatedExecution;
|
||||
parentExecution?: RelatedExecution;
|
||||
};
|
||||
}
|
||||
|
||||
interface NodeApiErrorOptions extends NodeOperationErrorOptions {
|
||||
|
||||
@@ -35,6 +35,7 @@ export class NodeOperationError extends NodeError {
|
||||
this.description = options.description;
|
||||
this.context.runIndex = options.runIndex;
|
||||
this.context.itemIndex = options.itemIndex;
|
||||
this.context.metadata = options.metadata;
|
||||
|
||||
if (this.message === this.description) {
|
||||
this.description = undefined;
|
||||
|
||||
@@ -15,6 +15,7 @@ export * from './ExecutionStatus';
|
||||
export * from './Expression';
|
||||
export * from './FromAIParseUtils';
|
||||
export * from './NodeHelpers';
|
||||
export * from './MetadataUtils';
|
||||
export * from './Workflow';
|
||||
export * from './WorkflowDataProxy';
|
||||
export * from './WorkflowDataProxyEnvProvider';
|
||||
|
||||
@@ -276,3 +276,10 @@ export function randomString(minLength: number, maxLength?: number): string {
|
||||
.map((byte) => ALPHABET[byte % ALPHABET.length])
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is an object with a specific key and provides a type guard for the key.
|
||||
*/
|
||||
export function hasKey<T extends PropertyKey>(value: unknown, key: T): value is Record<T, unknown> {
|
||||
return value !== null && typeof value === 'object' && value.hasOwnProperty(key);
|
||||
}
|
||||
|
||||
30
packages/workflow/test/MetadataUtils.test.ts
Normal file
30
packages/workflow/test/MetadataUtils.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { parseErrorMetadata } from '@/MetadataUtils';
|
||||
|
||||
describe('MetadataUtils', () => {
|
||||
describe('parseMetadataFromError', () => {
|
||||
it('should return undefined if error does not have response', () => {
|
||||
const error = { message: 'An error occurred' };
|
||||
const result = parseErrorMetadata(error);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if error response does not have subworkflow data', () => {
|
||||
const error = { errorResponse: { someKey: 'someValue' } };
|
||||
const result = parseErrorMetadata(error);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return metadata if error response has subworkflow data', () => {
|
||||
const error = { errorResponse: { executionId: '123', workflowId: '456' } };
|
||||
const expectedMetadata = {
|
||||
subExecution: {
|
||||
executionId: '123',
|
||||
workflowId: '456',
|
||||
},
|
||||
subExecutionsCount: 1,
|
||||
};
|
||||
const result = parseErrorMetadata(error);
|
||||
expect(result).toEqual(expectedMetadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
fileTypeFromMimeType,
|
||||
randomInt,
|
||||
randomString,
|
||||
hasKey,
|
||||
} from '@/utils';
|
||||
|
||||
describe('isObjectEmpty', () => {
|
||||
@@ -290,3 +291,78 @@ describe('randomString', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type Expect<T extends true> = T;
|
||||
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
|
||||
? true
|
||||
: false;
|
||||
|
||||
describe('hasKey', () => {
|
||||
it('should return false if the input is null', () => {
|
||||
const x = null;
|
||||
const result = hasKey(x, 'key');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
it('should return false if the input is undefined', () => {
|
||||
const x = undefined;
|
||||
const result = hasKey(x, 'key');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
it('should return false if the input is a number', () => {
|
||||
const x = 1;
|
||||
const result = hasKey(x, 'key');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
it('should return false if the input is an array out of bounds', () => {
|
||||
const x = [1, 2];
|
||||
const result = hasKey(x, 5);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true if the input is an array within bounds', () => {
|
||||
const x = [1, 2];
|
||||
const result = hasKey(x, 1);
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
it('should return true if the input is an array with the key `length`', () => {
|
||||
const x = [1, 2];
|
||||
const result = hasKey(x, 'length');
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
it('should return false if the input is an array with the key `toString`', () => {
|
||||
const x = [1, 2];
|
||||
const result = hasKey(x, 'toString');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
it('should return false if the input is an object without the key', () => {
|
||||
const x = { a: 3 };
|
||||
const result = hasKey(x, 'a');
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return true if the input is an object with the key', () => {
|
||||
const x = { a: 3 };
|
||||
const result = hasKey(x, 'b');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should provide a type guard', () => {
|
||||
const x: unknown = { a: 3 };
|
||||
if (hasKey(x, '0')) {
|
||||
const y: Expect<Equal<typeof x, Record<'0', unknown>>> = true;
|
||||
y;
|
||||
} else {
|
||||
const z: Expect<Equal<typeof x, unknown>> = true;
|
||||
z;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user