mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
feat(core): Initial workflow history API (#7234)
Github issue / Community forum post (link here to close automatically):
This commit is contained in:
parent
5c57e2ccc3
commit
0083a9e45d
|
@ -26,6 +26,8 @@ import {
|
|||
import { WorkflowsService } from '@/workflows/workflows.services';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
import { isWorkflowHistoryLicensed } from '@/workflows/workflowHistory/workflowHistoryHelper.ee';
|
||||
import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee';
|
||||
|
||||
export = {
|
||||
createWorkflow: [
|
||||
|
@ -177,6 +179,10 @@ export = {
|
|||
}
|
||||
}
|
||||
|
||||
if (isWorkflowHistoryLicensed()) {
|
||||
await Container.get(WorkflowHistoryService).saveVersion(req.user, sharedWorkflow.workflow);
|
||||
}
|
||||
|
||||
if (sharedWorkflow.workflow.active) {
|
||||
try {
|
||||
await workflowRunner.add(sharedWorkflow.workflowId, 'update');
|
||||
|
|
|
@ -178,6 +178,8 @@ import { JwtService } from './services/jwt.service';
|
|||
import { RoleService } from './services/role.service';
|
||||
import { UserService } from './services/user.service';
|
||||
import { OrchestrationController } from './controllers/orchestration.controller';
|
||||
import { isWorkflowHistoryEnabled } from './workflows/workflowHistory/workflowHistoryHelper.ee';
|
||||
import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee';
|
||||
|
||||
const exec = promisify(callbackExec);
|
||||
|
||||
|
@ -470,6 +472,7 @@ export class Server extends AbstractServer {
|
|||
LICENSE_FEATURES.SHOW_NON_PROD_BANNER,
|
||||
),
|
||||
debugInEditor: isDebugInEditorLicensed(),
|
||||
history: isWorkflowHistoryEnabled(),
|
||||
});
|
||||
|
||||
if (isLdapEnabled()) {
|
||||
|
@ -559,6 +562,7 @@ export class Server extends AbstractServer {
|
|||
Container.get(WorkflowStatisticsController),
|
||||
Container.get(ExternalSecretsController),
|
||||
Container.get(OrchestrationController),
|
||||
Container.get(WorkflowHistoryController),
|
||||
];
|
||||
|
||||
if (isLdapEnabled()) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { jsonColumnType } from './AbstractEntity';
|
||||
import { WithTimestamps, jsonColumnType } from './AbstractEntity';
|
||||
import { IConnections } from 'n8n-workflow';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { WorkflowEntity } from './WorkflowEntity';
|
||||
|
||||
@Entity()
|
||||
export class WorkflowHistory {
|
||||
export class WorkflowHistory extends WithTimestamps {
|
||||
@PrimaryColumn()
|
||||
versionId: string;
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import type { ExecutionData } from '../entities/ExecutionData';
|
|||
import { ExecutionEntity } from '../entities/ExecutionEntity';
|
||||
import { ExecutionMetadata } from '../entities/ExecutionMetadata';
|
||||
import { ExecutionDataRepository } from './executionData.repository';
|
||||
import { TIME } from '@/constants';
|
||||
import { TIME, inTest } from '@/constants';
|
||||
|
||||
function parseFiltersToQueryBuilder(
|
||||
qb: SelectQueryBuilder<ExecutionEntity>,
|
||||
|
@ -93,7 +93,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||
) {
|
||||
super(ExecutionEntity, dataSource.manager);
|
||||
|
||||
if (!this.isMainInstance) return;
|
||||
if (!this.isMainInstance || inTest) return;
|
||||
|
||||
if (this.isPruningEnabled) this.setSoftDeletionInterval();
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import type { UserManagementMailer } from '@/UserManagement/email';
|
|||
import type { Variables } from '@db/entities/Variables';
|
||||
import type { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
import type { CredentialsEntity } from './databases/entities/CredentialsEntity';
|
||||
import type { WorkflowHistory } from './databases/entities/WorkflowHistory';
|
||||
|
||||
export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
|
||||
@IsEmail()
|
||||
|
@ -545,3 +546,20 @@ export declare namespace OrchestrationRequest {
|
|||
type GetAll = AuthenticatedRequest;
|
||||
type Get = AuthenticatedRequest<{ id: string }, {}, {}, {}>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// /workflow-history
|
||||
// ----------------------------------
|
||||
|
||||
export declare namespace WorkflowHistoryRequest {
|
||||
type GetList = AuthenticatedRequest<
|
||||
{ workflowId: string },
|
||||
Array<Omit<WorkflowHistory, 'nodes' | 'connections'>>,
|
||||
{},
|
||||
ListQuery.Options
|
||||
>;
|
||||
type GetVersion = AuthenticatedRequest<
|
||||
{ workflowId: string; versionId: string },
|
||||
WorkflowHistory
|
||||
>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import { Authorized, RestController, Get, Middleware } from '@/decorators';
|
||||
import { WorkflowHistoryRequest } from '@/requests';
|
||||
import { Service } from 'typedi';
|
||||
import {
|
||||
HistoryVersionNotFoundError,
|
||||
SharedWorkflowNotFoundError,
|
||||
WorkflowHistoryService,
|
||||
} from './workflowHistory.service.ee';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { isWorkflowHistoryEnabled, isWorkflowHistoryLicensed } from './workflowHistoryHelper.ee';
|
||||
import { NotFoundError } from '@/ResponseHelper';
|
||||
import { paginationListQueryMiddleware } from '@/middlewares/listQuery/pagination';
|
||||
|
||||
const DEFAULT_TAKE = 20;
|
||||
|
||||
@Service()
|
||||
@Authorized()
|
||||
@RestController('/workflow-history')
|
||||
export class WorkflowHistoryController {
|
||||
constructor(private readonly historyService: WorkflowHistoryService) {}
|
||||
|
||||
@Middleware()
|
||||
workflowHistoryLicense(_req: Request, res: Response, next: NextFunction) {
|
||||
if (!isWorkflowHistoryLicensed()) {
|
||||
res.status(403);
|
||||
res.send('Workflow History license data not found');
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
@Middleware()
|
||||
workflowHistoryEnabled(_req: Request, res: Response, next: NextFunction) {
|
||||
if (!isWorkflowHistoryEnabled()) {
|
||||
res.status(403);
|
||||
res.send('Workflow History is disabled');
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
@Get('/workflow/:workflowId', { middlewares: [paginationListQueryMiddleware] })
|
||||
async getList(req: WorkflowHistoryRequest.GetList) {
|
||||
try {
|
||||
return await this.historyService.getList(
|
||||
req.user,
|
||||
req.params.workflowId,
|
||||
req.query.take ?? DEFAULT_TAKE,
|
||||
req.query.skip ?? 0,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof SharedWorkflowNotFoundError) {
|
||||
throw new NotFoundError('Could not find workflow');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/workflow/:workflowId/version/:versionId')
|
||||
async getVersion(req: WorkflowHistoryRequest.GetVersion) {
|
||||
try {
|
||||
return await this.historyService.getVersion(
|
||||
req.user,
|
||||
req.params.workflowId,
|
||||
req.params.versionId,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof SharedWorkflowNotFoundError) {
|
||||
throw new NotFoundError('Could not find workflow');
|
||||
} else if (e instanceof HistoryVersionNotFoundError) {
|
||||
throw new NotFoundError('Could not find version');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import type { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
|
||||
import type { User } from '@/databases/entities/User';
|
||||
import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
|
||||
import type { WorkflowHistory } from '@/databases/entities/WorkflowHistory';
|
||||
import { SharedWorkflowRepository } from '@/databases/repositories';
|
||||
import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository';
|
||||
import { Service } from 'typedi';
|
||||
import { isWorkflowHistoryEnabled } from './workflowHistoryHelper.ee';
|
||||
|
||||
export class SharedWorkflowNotFoundError extends Error {}
|
||||
export class HistoryVersionNotFoundError extends Error {}
|
||||
|
||||
@Service()
|
||||
export class WorkflowHistoryService {
|
||||
constructor(
|
||||
private readonly workflowHistoryRepository: WorkflowHistoryRepository,
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
) {}
|
||||
|
||||
private async getSharedWorkflow(user: User, workflowId: string): Promise<SharedWorkflow | null> {
|
||||
return this.sharedWorkflowRepository.findOne({
|
||||
where: {
|
||||
...(!user.isOwner && { userId: user.id }),
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getList(
|
||||
user: User,
|
||||
workflowId: string,
|
||||
take: number,
|
||||
skip: number,
|
||||
): Promise<Array<Omit<WorkflowHistory, 'nodes' | 'connections'>>> {
|
||||
const sharedWorkflow = await this.getSharedWorkflow(user, workflowId);
|
||||
if (!sharedWorkflow) {
|
||||
throw new SharedWorkflowNotFoundError();
|
||||
}
|
||||
return this.workflowHistoryRepository.find({
|
||||
where: {
|
||||
workflowId: sharedWorkflow.workflowId,
|
||||
},
|
||||
take,
|
||||
skip,
|
||||
select: ['workflowId', 'versionId', 'authors', 'createdAt', 'updatedAt'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getVersion(user: User, workflowId: string, versionId: string): Promise<WorkflowHistory> {
|
||||
const sharedWorkflow = await this.getSharedWorkflow(user, workflowId);
|
||||
if (!sharedWorkflow) {
|
||||
throw new SharedWorkflowNotFoundError();
|
||||
}
|
||||
const hist = await this.workflowHistoryRepository.findOne({
|
||||
where: {
|
||||
workflowId: sharedWorkflow.workflowId,
|
||||
versionId,
|
||||
},
|
||||
});
|
||||
if (!hist) {
|
||||
throw new HistoryVersionNotFoundError();
|
||||
}
|
||||
return hist;
|
||||
}
|
||||
|
||||
async saveVersion(user: User, workflow: WorkflowEntity) {
|
||||
if (isWorkflowHistoryEnabled()) {
|
||||
await this.workflowHistoryRepository.insert({
|
||||
authors: user.firstName + ' ' + user.lastName,
|
||||
connections: workflow.connections,
|
||||
nodes: workflow.nodes,
|
||||
versionId: workflow.versionId,
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,3 +5,7 @@ export function isWorkflowHistoryLicensed() {
|
|||
const license = Container.get(License);
|
||||
return license.isWorkflowHistoryLicensed();
|
||||
}
|
||||
|
||||
export function isWorkflowHistoryEnabled() {
|
||||
return isWorkflowHistoryLicensed();
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export class WorkflowHistoryService {
|
||||
constructor(private readonly workflowHistoryRepository: WorkflowHistoryRepository) {}
|
||||
}
|
|
@ -33,6 +33,8 @@ import { WorkflowRepository } from '@/databases/repositories';
|
|||
import { RoleService } from '@/services/role.service';
|
||||
import { OwnershipService } from '@/services/ownership.service';
|
||||
import { isStringArray, isWorkflowIdValid } from '@/utils';
|
||||
import { isWorkflowHistoryLicensed } from './workflowHistory/workflowHistoryHelper.ee';
|
||||
import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee';
|
||||
|
||||
export class WorkflowsService {
|
||||
static async getSharing(
|
||||
|
@ -298,6 +300,10 @@ export class WorkflowsService {
|
|||
);
|
||||
}
|
||||
|
||||
if (isWorkflowHistoryLicensed()) {
|
||||
await Container.get(WorkflowHistoryService).saveVersion(user, shared.workflow);
|
||||
}
|
||||
|
||||
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
|
||||
|
||||
// We sadly get nothing back from "update". Neither if it updated a record
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { UserSettings } from 'n8n-core';
|
||||
import type { DataSourceOptions as ConnectionOptions } from 'typeorm';
|
||||
import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm';
|
||||
import { DataSource as Connection } from 'typeorm';
|
||||
import { Container } from 'typedi';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import config from '@/config';
|
||||
import * as Db from '@/Db';
|
||||
|
@ -26,12 +27,17 @@ import type { ExecutionData } from '@db/entities/ExecutionData';
|
|||
import { generateNanoId } from '@db/utils/generators';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
import { VariablesService } from '@/environments/variables/variables.service';
|
||||
import { TagRepository, WorkflowTagMappingRepository } from '@/databases/repositories';
|
||||
import {
|
||||
TagRepository,
|
||||
WorkflowHistoryRepository,
|
||||
WorkflowTagMappingRepository,
|
||||
} from '@/databases/repositories';
|
||||
import { separate } from '@/utils';
|
||||
|
||||
import { randomPassword } from '@/Ldap/helpers';
|
||||
import { TOTPService } from '@/Mfa/totp.service';
|
||||
import { MfaService } from '@/Mfa/mfa.service';
|
||||
import type { WorkflowHistory } from '@/databases/entities/WorkflowHistory';
|
||||
|
||||
export type TestDBType = 'postgres' | 'mysql';
|
||||
|
||||
|
@ -118,7 +124,12 @@ export async function truncate(collections: CollectionName[]) {
|
|||
}
|
||||
|
||||
for (const collection of rest) {
|
||||
if (typeof collection === 'string') {
|
||||
await Db.collections[collection].delete({});
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await Container.get(collection as { new (): Repository<any> }).delete({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -572,6 +583,33 @@ export async function getVariableById(id: string) {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// workflow history
|
||||
// ----------------------------------
|
||||
|
||||
export async function createWorkflowHistoryItem(
|
||||
workflowId: string,
|
||||
data?: Partial<WorkflowHistory>,
|
||||
) {
|
||||
return Container.get(WorkflowHistoryRepository).save({
|
||||
authors: 'John Smith',
|
||||
connections: {},
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'Start',
|
||||
parameters: {},
|
||||
position: [-20, 260],
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
},
|
||||
],
|
||||
versionId: uuid(),
|
||||
...(data ?? {}),
|
||||
workflowId,
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// connection options
|
||||
// ----------------------------------
|
||||
|
|
|
@ -6,8 +6,12 @@ import type { Server } from 'http';
|
|||
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { BooleanLicenseFeature, ICredentialsDb, IDatabaseCollections } from '@/Interfaces';
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
|
||||
export type CollectionName = keyof IDatabaseCollections;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CollectionName =
|
||||
| keyof IDatabaseCollections
|
||||
| { new (dataSource: DataSource): Repository<any> };
|
||||
|
||||
export type EndpointGroup =
|
||||
| 'me'
|
||||
|
@ -29,7 +33,8 @@ export type EndpointGroup =
|
|||
| 'externalSecrets'
|
||||
| 'mfa'
|
||||
| 'metrics'
|
||||
| 'executions';
|
||||
| 'executions'
|
||||
| 'workflowHistory';
|
||||
|
||||
export interface SetupProps {
|
||||
applyAuth?: boolean;
|
||||
|
|
|
@ -65,6 +65,7 @@ import { JwtService } from '@/services/jwt.service';
|
|||
import { RoleService } from '@/services/role.service';
|
||||
import { UserService } from '@/services/user.service';
|
||||
import { executionsController } from '@/executions/executions.controller';
|
||||
import { WorkflowHistoryController } from '@/workflows/workflowHistory/workflowHistory.controller.ee';
|
||||
|
||||
/**
|
||||
* Plugin to prefix a path segment into a request URL pathname.
|
||||
|
@ -161,7 +162,6 @@ export const setupTestServer = ({
|
|||
|
||||
config.set('userManagement.jwtSecret', 'My JWT secret');
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
config.set('executions.pruneData', false);
|
||||
|
||||
if (enabledFeatures) {
|
||||
Container.get(License).isFeatureEnabled = (feature) => enabledFeatures.includes(feature);
|
||||
|
@ -313,6 +313,9 @@ export const setupTestServer = ({
|
|||
case 'externalSecrets':
|
||||
registerController(app, config, Container.get(ExternalSecretsController));
|
||||
break;
|
||||
case 'workflowHistory':
|
||||
registerController(app, config, Container.get(WorkflowHistoryController));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
226
packages/cli/test/integration/workflowHistory.api.test.ts
Normal file
226
packages/cli/test/integration/workflowHistory.api.test.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
import type { SuperAgentTest } from 'supertest';
|
||||
import { License } from '@/License';
|
||||
import * as testDb from './shared/testDb';
|
||||
import * as utils from './shared/utils/';
|
||||
import type { User } from '@/databases/entities/User';
|
||||
import { WorkflowHistoryRepository } from '@/databases/repositories';
|
||||
|
||||
let owner: User;
|
||||
let authOwnerAgent: SuperAgentTest;
|
||||
let member: User;
|
||||
let authMemberAgent: SuperAgentTest;
|
||||
|
||||
const licenseLike = utils.mockInstance(License, {
|
||||
isWorkflowHistoryLicensed: jest.fn().mockReturnValue(true),
|
||||
isWithinUsersLimit: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['workflowHistory'] });
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await testDb.createOwner();
|
||||
authOwnerAgent = testServer.authAgentFor(owner);
|
||||
member = await testDb.createUser();
|
||||
authMemberAgent = testServer.authAgentFor(member);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testDb.truncate(['Workflow', 'SharedWorkflow', WorkflowHistoryRepository]);
|
||||
});
|
||||
|
||||
describe('GET /workflow-history/:workflowId', () => {
|
||||
test('should not work when license is not available', async () => {
|
||||
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
|
||||
const resp = await authOwnerAgent.get('/workflow-history/workflow/badid');
|
||||
expect(resp.status).toBe(403);
|
||||
expect(resp.text).toBe('Workflow History license data not found');
|
||||
});
|
||||
|
||||
test('should not return anything on an invalid workflow ID', async () => {
|
||||
await testDb.createWorkflow(undefined, owner);
|
||||
const resp = await authOwnerAgent.get('/workflow-history/workflow/badid');
|
||||
expect(resp.status).toBe(404);
|
||||
});
|
||||
|
||||
test('should not return anything if not shared with user', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const resp = await authMemberAgent.get('/workflow-history/workflow/' + workflow.id);
|
||||
expect(resp.status).toBe(404);
|
||||
});
|
||||
|
||||
test('should return any empty list if no versions', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const resp = await authOwnerAgent.get('/workflow-history/workflow/' + workflow.id);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.body).toEqual({ data: [] });
|
||||
});
|
||||
|
||||
test('should return versions for workflow', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const versions = await Promise.all(
|
||||
new Array(10)
|
||||
.fill(undefined)
|
||||
.map(async (_, i) =>
|
||||
testDb.createWorkflowHistoryItem(workflow.id, { createdAt: new Date(Date.now() + i) }),
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const last = versions.sort((a, b) => b.createdAt.valueOf() - a.createdAt.valueOf())[0]! as any;
|
||||
delete last.nodes;
|
||||
delete last.connections;
|
||||
|
||||
last.createdAt = last.createdAt.toISOString();
|
||||
last.updatedAt = last.updatedAt.toISOString();
|
||||
|
||||
const resp = await authOwnerAgent.get('/workflow-history/workflow/' + workflow.id);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.body.data).toHaveLength(10);
|
||||
expect(resp.body.data[0]).toEqual(last);
|
||||
});
|
||||
|
||||
test('should return versions only for workflow id provided', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const workflow2 = await testDb.createWorkflow(undefined, owner);
|
||||
const versions = await Promise.all(
|
||||
new Array(10)
|
||||
.fill(undefined)
|
||||
.map(async (_, i) =>
|
||||
testDb.createWorkflowHistoryItem(workflow.id, { createdAt: new Date(Date.now() + i) }),
|
||||
),
|
||||
);
|
||||
|
||||
const versions2 = await Promise.all(
|
||||
new Array(10)
|
||||
.fill(undefined)
|
||||
.map(async (_) => testDb.createWorkflowHistoryItem(workflow2.id)),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const last = versions.sort((a, b) => b.createdAt.valueOf() - a.createdAt.valueOf())[0]! as any;
|
||||
delete last.nodes;
|
||||
delete last.connections;
|
||||
|
||||
last.createdAt = last.createdAt.toISOString();
|
||||
last.updatedAt = last.updatedAt.toISOString();
|
||||
|
||||
const resp = await authOwnerAgent.get('/workflow-history/workflow/' + workflow.id);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.body.data).toHaveLength(10);
|
||||
expect(resp.body.data[0]).toEqual(last);
|
||||
});
|
||||
|
||||
test('should work with take parameter', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const versions = await Promise.all(
|
||||
new Array(10)
|
||||
.fill(undefined)
|
||||
.map(async (_, i) =>
|
||||
testDb.createWorkflowHistoryItem(workflow.id, { createdAt: new Date(Date.now() + i) }),
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const last = versions.sort((a, b) => b.createdAt.valueOf() - a.createdAt.valueOf())[0]! as any;
|
||||
delete last.nodes;
|
||||
delete last.connections;
|
||||
|
||||
last.createdAt = last.createdAt.toISOString();
|
||||
last.updatedAt = last.updatedAt.toISOString();
|
||||
|
||||
const resp = await authOwnerAgent.get(`/workflow-history/workflow/${workflow.id}?take=5`);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.body.data).toHaveLength(5);
|
||||
expect(resp.body.data[0]).toEqual(last);
|
||||
});
|
||||
|
||||
test('should work with skip parameter', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const versions = await Promise.all(
|
||||
new Array(10)
|
||||
.fill(undefined)
|
||||
.map(async (_, i) =>
|
||||
testDb.createWorkflowHistoryItem(workflow.id, { createdAt: new Date(Date.now() + i) }),
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const last = versions.sort((a, b) => b.createdAt.valueOf() - a.createdAt.valueOf())[5]! as any;
|
||||
delete last.nodes;
|
||||
delete last.connections;
|
||||
|
||||
last.createdAt = last.createdAt.toISOString();
|
||||
last.updatedAt = last.updatedAt.toISOString();
|
||||
|
||||
const resp = await authOwnerAgent.get(
|
||||
`/workflow-history/workflow/${workflow.id}?skip=5&take=20`,
|
||||
);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.body.data).toHaveLength(5);
|
||||
expect(resp.body.data[0]).toEqual(last);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /workflow-history/workflow/:workflowId/version/:versionId', () => {
|
||||
test('should not work when license is not available', async () => {
|
||||
licenseLike.isWorkflowHistoryLicensed.mockReturnValue(false);
|
||||
const resp = await authOwnerAgent.get('/workflow-history/workflow/badid/version/badid');
|
||||
expect(resp.status).toBe(403);
|
||||
expect(resp.text).toBe('Workflow History license data not found');
|
||||
});
|
||||
|
||||
test('should not return anything on an invalid workflow ID', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const version = await testDb.createWorkflowHistoryItem(workflow.id);
|
||||
const resp = await authOwnerAgent.get(
|
||||
`/workflow-history/workflow/badid/version/${version.versionId}`,
|
||||
);
|
||||
expect(resp.status).toBe(404);
|
||||
});
|
||||
|
||||
test('should not return anything on an invalid version ID', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
await testDb.createWorkflowHistoryItem(workflow.id);
|
||||
const resp = await authOwnerAgent.get(
|
||||
`/workflow-history/workflow/${workflow.id}/version/badid`,
|
||||
);
|
||||
expect(resp.status).toBe(404);
|
||||
});
|
||||
|
||||
test('should return version', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const version = await testDb.createWorkflowHistoryItem(workflow.id);
|
||||
const resp = await authOwnerAgent.get(
|
||||
`/workflow-history/workflow/${workflow.id}/version/${version.versionId}`,
|
||||
);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.body.data).toEqual({
|
||||
...version,
|
||||
createdAt: version.createdAt.toISOString(),
|
||||
updatedAt: version.updatedAt.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
test('should not return anything if not shared with user', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const version = await testDb.createWorkflowHistoryItem(workflow.id);
|
||||
const resp = await authMemberAgent.get(
|
||||
`/workflow-history/workflow/${workflow.id}/version/${version.versionId}`,
|
||||
);
|
||||
expect(resp.status).toBe(404);
|
||||
});
|
||||
|
||||
test('should not return anything if not shared with user and using workflow owned by unshared user', async () => {
|
||||
const workflow = await testDb.createWorkflow(undefined, owner);
|
||||
const workflowMember = await testDb.createWorkflow(undefined, member);
|
||||
const version = await testDb.createWorkflowHistoryItem(workflow.id);
|
||||
const resp = await authMemberAgent.get(
|
||||
`/workflow-history/workflow/${workflowMember.id}/version/${version.versionId}`,
|
||||
);
|
||||
expect(resp.status).toBe(404);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue