feat: Add custom data to public API execution endpoints (#9705)

This commit is contained in:
Val
2024-06-17 12:38:10 +01:00
committed by GitHub
parent d615711728
commit a1046607bf
4 changed files with 59 additions and 6 deletions

View File

@@ -145,6 +145,7 @@ export interface IExecutionResponse extends IExecutionBase {
retryOf?: string; retryOf?: string;
retrySuccessId?: string; retrySuccessId?: string;
workflowData: IWorkflowBase | WorkflowWithSharingsAndCredentials; workflowData: IWorkflowBase | WorkflowWithSharingsAndCredentials;
customData: Record<string, string>;
} }
// Flatted data to save memory when saving in database or transferring // Flatted data to save memory when saving in database or transferring
@@ -158,6 +159,7 @@ export interface IExecutionFlattedDb extends IExecutionBase {
id: string; id: string;
data: string; data: string;
workflowData: Omit<IWorkflowBase, 'pinData'>; workflowData: Omit<IWorkflowBase, 'pinData'>;
customData: Record<string, string>;
} }
export interface IExecutionFlattedResponse extends IExecutionFlatted { export interface IExecutionFlattedResponse extends IExecutionFlatted {

View File

@@ -31,3 +31,5 @@ properties:
type: string type: string
nullable: true nullable: true
format: date-time format: date-time
customData:
type: object

View File

@@ -149,27 +149,29 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
if (!queryParams.relations) { if (!queryParams.relations) {
queryParams.relations = []; queryParams.relations = [];
} }
(queryParams.relations as string[]).push('executionData'); (queryParams.relations as string[]).push('executionData', 'metadata');
} }
const executions = await this.find(queryParams); const executions = await this.find(queryParams);
if (options?.includeData && options?.unflattenData) { if (options?.includeData && options?.unflattenData) {
return executions.map((execution) => { return executions.map((execution) => {
const { executionData, ...rest } = execution; const { executionData, metadata, ...rest } = execution;
return { return {
...rest, ...rest,
data: parse(executionData.data) as IRunExecutionData, data: parse(executionData.data) as IRunExecutionData,
workflowData: executionData.workflowData, workflowData: executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionResponse; } as IExecutionResponse;
}); });
} else if (options?.includeData) { } else if (options?.includeData) {
return executions.map((execution) => { return executions.map((execution) => {
const { executionData, ...rest } = execution; const { executionData, metadata, ...rest } = execution;
return { return {
...rest, ...rest,
data: execution.executionData.data, data: execution.executionData.data,
workflowData: execution.executionData.workflowData, workflowData: execution.executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionFlattedDb; } as IExecutionFlattedDb;
}); });
} }
@@ -219,7 +221,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
}, },
}; };
if (options?.includeData) { if (options?.includeData) {
findOptions.relations = ['executionData']; findOptions.relations = ['executionData', 'metadata'];
} }
const execution = await this.findOne(findOptions); const execution = await this.findOne(findOptions);
@@ -228,19 +230,21 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
return undefined; return undefined;
} }
const { executionData, ...rest } = execution; const { executionData, metadata, ...rest } = execution;
if (options?.includeData && options?.unflattenData) { if (options?.includeData && options?.unflattenData) {
return { return {
...rest, ...rest,
data: parse(execution.executionData.data) as IRunExecutionData, data: parse(execution.executionData.data) as IRunExecutionData,
workflowData: execution.executionData.workflowData, workflowData: execution.executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionResponse; } as IExecutionResponse;
} else if (options?.includeData) { } else if (options?.includeData) {
return { return {
...rest, ...rest,
data: execution.executionData.data, data: execution.executionData.data,
workflowData: execution.executionData.workflowData, workflowData: execution.executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionFlattedDb; } as IExecutionFlattedDb;
} }
@@ -298,7 +302,8 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
// Se isolate startedAt because it must be set when the execution starts and should never change. // Se isolate startedAt because it must be set when the execution starts and should never change.
// So we prevent updating it, if it's sent (it usually is and causes problems to executions that // So we prevent updating it, if it's sent (it usually is and causes problems to executions that
// are resumed after waiting for some time, as a new startedAt is set) // are resumed after waiting for some time, as a new startedAt is set)
const { id, data, workflowId, workflowData, startedAt, ...executionInformation } = execution; const { id, data, workflowId, workflowData, startedAt, customData, ...executionInformation } =
execution;
if (Object.keys(executionInformation).length > 0) { if (Object.keys(executionInformation).length > 0) {
await this.update({ id: executionId }, executionInformation); await this.update({ id: executionId }, executionInformation);
} }

View File

@@ -12,6 +12,7 @@ import {
} from '../shared/db/workflows'; } from '../shared/db/workflows';
import { import {
createErrorExecution, createErrorExecution,
createExecution,
createManyExecutions, createManyExecutions,
createSuccessfulExecution, createSuccessfulExecution,
createWaitingExecution, createWaitingExecution,
@@ -125,6 +126,49 @@ describe('GET /executions/:id', () => {
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
}); });
test('member should not be able to fetch custom data when includeData is not set', async () => {
const workflow = await createWorkflow({}, user1);
const execution = await createExecution(
{
finished: true,
status: 'success',
metadata: [
{ key: 'test1', value: 'value1' },
{ key: 'test2', value: 'value2' },
],
},
workflow,
);
const response = await authUser1Agent.get(`/executions/${execution.id}`);
expect(response.statusCode).toBe(200);
expect(response.body.customData).toBeUndefined();
});
test('member should be able to fetch custom data when includeData=true', async () => {
const workflow = await createWorkflow({}, user1);
const execution = await createExecution(
{
finished: true,
status: 'success',
metadata: [
{ key: 'test1', value: 'value1' },
{ key: 'test2', value: 'value2' },
],
},
workflow,
);
const response = await authUser1Agent.get(`/executions/${execution.id}?includeData=true`);
expect(response.statusCode).toBe(200);
expect(response.body.customData).toEqual({
test1: 'value1',
test2: 'value2',
});
});
test('member should not get an execution of another user without the workflow being shared', async () => { test('member should not get an execution of another user without the workflow being shared', async () => {
const workflow = await createWorkflow({}, owner); const workflow = await createWorkflow({}, owner);