feat(editor): Handle pin data edge cases and unify validation (no-changelog) (#6685)
Github issue / Community forum post (link here to close automatically):
This commit is contained in:
@@ -1,17 +1,27 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeTypeDescription, IPinData } from 'n8n-workflow';
|
||||
import type { IPinData, INodeExecutionData } from 'n8n-workflow';
|
||||
import { stringSizeInBytes } from '@/utils';
|
||||
import { MAX_WORKFLOW_PINNED_DATA_SIZE, PIN_DATA_NODE_TYPES_DENYLIST } from '@/constants';
|
||||
import {
|
||||
MAX_EXPECTED_REQUEST_SIZE,
|
||||
MAX_PINNED_DATA_SIZE,
|
||||
MAX_WORKFLOW_SIZE,
|
||||
PIN_DATA_NODE_TYPES_DENYLIST,
|
||||
} from '@/constants';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useToast } from '@/composables';
|
||||
import { jsonParse, jsonStringify } from 'n8n-workflow';
|
||||
|
||||
export interface IPinDataContext {
|
||||
node: INodeUi;
|
||||
nodeType: INodeTypeDescription;
|
||||
$showError(error: Error, title: string): void;
|
||||
}
|
||||
export type PinDataSource =
|
||||
| 'pin-icon-click'
|
||||
| 'save-edit'
|
||||
| 'on-ndv-close-modal'
|
||||
| 'duplicate-node'
|
||||
| 'add-nodes';
|
||||
|
||||
export type UnpinDataSource = 'unpin-and-execute-modal';
|
||||
|
||||
export const pinData = defineComponent({
|
||||
setup() {
|
||||
@@ -20,7 +30,7 @@ export const pinData = defineComponent({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useWorkflowsStore),
|
||||
...mapStores(useWorkflowsStore, useNDVStore),
|
||||
pinData(): IPinData[string] | undefined {
|
||||
return this.node ? this.workflowsStore.pinDataByNodeName(this.node.name) : undefined;
|
||||
},
|
||||
@@ -83,13 +93,16 @@ export const pinData = defineComponent({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isValidPinDataSize(data: string | object): boolean {
|
||||
isValidPinDataSize(data: string | object, activeNodeName: string): boolean {
|
||||
if (typeof data === 'object') data = JSON.stringify(data);
|
||||
|
||||
if (
|
||||
this.workflowsStore.pinDataSize + stringSizeInBytes(data) >
|
||||
MAX_WORKFLOW_PINNED_DATA_SIZE
|
||||
) {
|
||||
const { pinData: currentPinData, ...workflow } = this.workflowsStore.getCurrentWorkflow();
|
||||
const workflowJson = jsonStringify(workflow, { replaceCircularRefs: true });
|
||||
|
||||
const newPinData = { ...currentPinData, [activeNodeName]: data };
|
||||
const newPinDataSize = this.workflowsStore.getPinDataSize(newPinData);
|
||||
|
||||
if (newPinDataSize > MAX_PINNED_DATA_SIZE) {
|
||||
this.showError(
|
||||
new Error(this.$locale.baseText('ndv.pinData.error.tooLarge.description')),
|
||||
this.$locale.baseText('ndv.pinData.error.tooLarge.title'),
|
||||
@@ -98,7 +111,83 @@ export const pinData = defineComponent({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
stringSizeInBytes(workflowJson) + newPinDataSize >
|
||||
MAX_WORKFLOW_SIZE - MAX_EXPECTED_REQUEST_SIZE
|
||||
) {
|
||||
this.showError(
|
||||
new Error(this.$locale.baseText('ndv.pinData.error.tooLargeWorkflow.description')),
|
||||
this.$locale.baseText('ndv.pinData.error.tooLargeWorkflow.title'),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
setPinData(node: INodeUi, data: string | INodeExecutionData[], source: PinDataSource): boolean {
|
||||
if (typeof data === 'string') {
|
||||
if (!this.isValidPinDataJSON(data)) {
|
||||
this.onDataPinningError({ errorType: 'invalid-json', source });
|
||||
throw new Error('Invalid JSON');
|
||||
}
|
||||
|
||||
data = jsonParse(data);
|
||||
}
|
||||
|
||||
if (!this.isValidPinDataSize(data, node.name)) {
|
||||
this.onDataPinningError({ errorType: 'data-too-large', source });
|
||||
throw new Error('Data too large');
|
||||
}
|
||||
|
||||
this.onDataPinningSuccess({ source });
|
||||
this.workflowsStore.pinData({ node, data: data as INodeExecutionData[] });
|
||||
},
|
||||
unsetPinData(node: INodeUi, source: UnpinDataSource): void {
|
||||
this.onDataUnpinning({ source });
|
||||
this.workflowsStore.unpinData({ node });
|
||||
},
|
||||
onDataPinningSuccess({ source }: { source: PinDataSource }) {
|
||||
const telemetryPayload = {
|
||||
pinning_source: source,
|
||||
node_type: this.activeNode?.type,
|
||||
session_id: this.sessionId,
|
||||
data_size: stringSizeInBytes(this.pinData),
|
||||
view: this.displayMode,
|
||||
run_index: this.runIndex,
|
||||
};
|
||||
void this.$externalHooks().run('runData.onDataPinningSuccess', telemetryPayload);
|
||||
this.$telemetry.track('Ndv data pinning success', telemetryPayload);
|
||||
},
|
||||
onDataPinningError({
|
||||
errorType,
|
||||
source,
|
||||
}: {
|
||||
errorType: 'data-too-large' | 'invalid-json';
|
||||
source: PinDataSource;
|
||||
}) {
|
||||
this.$telemetry.track('Ndv data pinning failure', {
|
||||
pinning_source: source,
|
||||
node_type: this.activeNode?.type,
|
||||
session_id: this.sessionId,
|
||||
data_size: stringSizeInBytes(this.pinData),
|
||||
view: this.displayMode,
|
||||
run_index: this.runIndex,
|
||||
error_type: errorType,
|
||||
});
|
||||
},
|
||||
onDataUnpinning({
|
||||
source,
|
||||
}: {
|
||||
source: 'banner-link' | 'pin-icon-click' | 'unpin-and-execute-modal';
|
||||
}) {
|
||||
this.$telemetry.track('User unpinned ndv data', {
|
||||
node_type: this.activeNode?.type,
|
||||
session_id: this.sessionId,
|
||||
run_index: this.runIndex,
|
||||
source,
|
||||
data_size: stringSizeInBytes(this.pinData),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user