Skip to content

Commit

Permalink
feat: Add custom data to public API execution endpoints (n8n-io#9705)
Browse files Browse the repository at this point in the history
  • Loading branch information
valya authored and adrian-martinez-onestic committed Jun 20, 2024
1 parent 1302911 commit a86702a
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 6 deletions.
2 changes: 2 additions & 0 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export interface IExecutionResponse extends IExecutionBase {
retryOf?: string;
retrySuccessId?: string;
workflowData: IWorkflowBase | WorkflowWithSharingsAndCredentials;
customData: Record<string, string>;
}

// Flatted data to save memory when saving in database or transferring
Expand All @@ -158,6 +159,7 @@ export interface IExecutionFlattedDb extends IExecutionBase {
id: string;
data: string;
workflowData: Omit<IWorkflowBase, 'pinData'>;
customData: Record<string, string>;
}

export interface IExecutionFlattedResponse extends IExecutionFlatted {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ properties:
type: string
nullable: true
format: date-time
customData:
type: object
17 changes: 11 additions & 6 deletions packages/cli/src/databases/repositories/execution.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,29 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
if (!queryParams.relations) {
queryParams.relations = [];
}
(queryParams.relations as string[]).push('executionData');
(queryParams.relations as string[]).push('executionData', 'metadata');
}

const executions = await this.find(queryParams);

if (options?.includeData && options?.unflattenData) {
return executions.map((execution) => {
const { executionData, ...rest } = execution;
const { executionData, metadata, ...rest } = execution;
return {
...rest,
data: parse(executionData.data) as IRunExecutionData,
workflowData: executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionResponse;
});
} else if (options?.includeData) {
return executions.map((execution) => {
const { executionData, ...rest } = execution;
const { executionData, metadata, ...rest } = execution;
return {
...rest,
data: execution.executionData.data,
workflowData: execution.executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionFlattedDb;
});
}
Expand Down Expand Up @@ -219,7 +221,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
},
};
if (options?.includeData) {
findOptions.relations = ['executionData'];
findOptions.relations = ['executionData', 'metadata'];
}

const execution = await this.findOne(findOptions);
Expand All @@ -228,19 +230,21 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
return undefined;
}

const { executionData, ...rest } = execution;
const { executionData, metadata, ...rest } = execution;

if (options?.includeData && options?.unflattenData) {
return {
...rest,
data: parse(execution.executionData.data) as IRunExecutionData,
workflowData: execution.executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionResponse;
} else if (options?.includeData) {
return {
...rest,
data: execution.executionData.data,
workflowData: execution.executionData.workflowData,
customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])),
} as IExecutionFlattedDb;
}

Expand Down Expand Up @@ -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.
// 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)
const { id, data, workflowId, workflowData, startedAt, ...executionInformation } = execution;
const { id, data, workflowId, workflowData, startedAt, customData, ...executionInformation } =
execution;
if (Object.keys(executionInformation).length > 0) {
await this.update({ id: executionId }, executionInformation);
}
Expand Down
44 changes: 44 additions & 0 deletions packages/cli/test/integration/publicApi/executions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../shared/db/workflows';
import {
createErrorExecution,
createExecution,
createManyExecutions,
createSuccessfulExecution,
createWaitingExecution,
Expand Down Expand Up @@ -125,6 +126,49 @@ describe('GET /executions/:id', () => {
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 () => {
const workflow = await createWorkflow({}, owner);

Expand Down

0 comments on commit a86702a

Please sign in to comment.