Skip to content

Commit

Permalink
feat(core): Execution curation (#10342)
Browse files Browse the repository at this point in the history
Co-authored-by: oleg <me@olegivaniv.com>
  • Loading branch information
burivuhster and OlegIvaniv authored Sep 2, 2024
1 parent 8603946 commit 022ddcb
Show file tree
Hide file tree
Showing 75 changed files with 2,746 additions and 726 deletions.
2 changes: 2 additions & 0 deletions cypress/e2e/20-workflow-executions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,11 @@ const createMockExecutions = () => {
executionsTab.actions.createManualExecutions(5);
// Make some failed executions by enabling Code node with syntax error
executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 0);
executionsTab.actions.createManualExecutions(2);
// Then add some more successful ones
executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 1);
executionsTab.actions.createManualExecutions(4);
};

Expand Down
8 changes: 7 additions & 1 deletion cypress/e2e/5-ndv.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,13 @@ describe('NDV', () => {
ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib');

ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON');
ndv.getters.outputDisplayMode().find('label').eq(1).click();
ndv.getters
.outputDisplayMode()
.find('label')
.eq(1)
.scrollIntoView()
.should('be.visible')
.click();

ndv.getters.outputDataContainer().find('.json-data').should('exist');
ndv.getters
Expand Down
1 change: 1 addition & 0 deletions packages/@n8n/permissions/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const DEFAULT_OPERATIONS = ['create', 'read', 'update', 'delete', 'list'] as const;
export const RESOURCES = {
annotationTag: [...DEFAULT_OPERATIONS] as const,
auditLogs: ['manage'] as const,
banner: ['dismiss'] as const,
communityPackage: ['install', 'uninstall', 'update', 'list', 'manage'] as const,
Expand Down
2 changes: 2 additions & 0 deletions packages/@n8n/permissions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ResourceScope<

export type WildcardScope = `${Resource}:*` | '*';

export type AnnotationTagScope = ResourceScope<'annotationTag'>;
export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>;
export type BannerScope = ResourceScope<'banner', 'dismiss'>;
export type CommunityPackageScope = ResourceScope<
Expand Down Expand Up @@ -44,6 +45,7 @@ export type WorkflowScope = ResourceScope<
>;

export type Scope =
| AnnotationTagScope
| AuditLogsScope
| BannerScope
| CommunityPackageScope
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/src/controllers/annotation-tags.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@/decorators';
import { AnnotationTagService } from '@/services/annotation-tag.service';
import { AnnotationTagsRequest } from '@/requests';

@RestController('/annotation-tags')
export class AnnotationTagsController {
constructor(private readonly annotationTagService: AnnotationTagService) {}

@Get('/')
@GlobalScope('annotationTag:list')
async getAll(req: AnnotationTagsRequest.GetAll) {
return await this.annotationTagService.getAll({
withUsageCount: req.query.withUsageCount === 'true',
});
}

@Post('/')
@GlobalScope('annotationTag:create')
async createTag(req: AnnotationTagsRequest.Create) {
const tag = this.annotationTagService.toEntity({ name: req.body.name });

return await this.annotationTagService.save(tag);
}

@Patch('/:id(\\w+)')
@GlobalScope('annotationTag:update')
async updateTag(req: AnnotationTagsRequest.Update) {
const newTag = this.annotationTagService.toEntity({
id: req.params.id,
name: req.body.name.trim(),
});

return await this.annotationTagService.save(newTag);
}

@Delete('/:id(\\w+)')
@GlobalScope('annotationTag:delete')
async deleteTag(req: AnnotationTagsRequest.Delete) {
const { id } = req.params;

await this.annotationTagService.delete(id);

return true;
}
}
1 change: 1 addition & 0 deletions packages/cli/src/databases/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const getSqliteConnectionOptions = (): SqliteConnectionOptions | SqlitePooledCon
database: path.resolve(Container.get(InstanceSettings).n8nFolder, sqliteConfig.database),
migrations: sqliteMigrations,
};

if (sqliteConfig.poolSize > 0) {
return {
type: 'sqlite-pooled',
Expand Down
20 changes: 20 additions & 0 deletions packages/cli/src/databases/entities/annotation-tag-entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Column, Entity, Index, ManyToMany, OneToMany } from '@n8n/typeorm';
import { IsString, Length } from 'class-validator';
import { WithTimestampsAndStringId } from './abstract-entity';
import type { ExecutionAnnotation } from '@/databases/entities/execution-annotation';
import type { AnnotationTagMapping } from '@/databases/entities/annotation-tag-mapping';

@Entity()
export class AnnotationTagEntity extends WithTimestampsAndStringId {
@Column({ length: 24 })
@Index({ unique: true })
@IsString({ message: 'Tag name must be of type string.' })
@Length(1, 24, { message: 'Tag name must be $constraint1 to $constraint2 characters long.' })
name: string;

@ManyToMany('ExecutionAnnotation', 'tags')
annotations: ExecutionAnnotation[];

@OneToMany('AnnotationTagMapping', 'tags')
annotationMappings: AnnotationTagMapping[];
}
23 changes: 23 additions & 0 deletions packages/cli/src/databases/entities/annotation-tag-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
import type { ExecutionAnnotation } from './execution-annotation';
import type { AnnotationTagEntity } from './annotation-tag-entity';

/**
* This entity represents the junction table between the execution annotations and the tags
*/
@Entity({ name: 'execution_annotation_tags' })
export class AnnotationTagMapping {
@PrimaryColumn()
annotationId: number;

@ManyToOne('ExecutionAnnotation', 'tagMappings')
@JoinColumn({ name: 'annotationId' })
annotations: ExecutionAnnotation[];

@PrimaryColumn()
tagId: string;

@ManyToOne('AnnotationTagEntity', 'annotationMappings')
@JoinColumn({ name: 'tagId' })
tags: AnnotationTagEntity[];
}
61 changes: 61 additions & 0 deletions packages/cli/src/databases/entities/execution-annotation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
RelationId,
} from '@n8n/typeorm';
import { ExecutionEntity } from './execution-entity';
import type { AnnotationTagEntity } from './annotation-tag-entity';
import type { AnnotationTagMapping } from './annotation-tag-mapping';
import type { AnnotationVote } from 'n8n-workflow';

@Entity({ name: 'execution_annotations' })
export class ExecutionAnnotation {
@PrimaryGeneratedColumn()
id: number;

/**
* This field stores the up- or down-vote of the execution by user.
*/
@Column({ type: 'varchar', nullable: true })
vote: AnnotationVote | null;

/**
* Custom text note added to the execution by user.
*/
@Column({ type: 'varchar', nullable: true })
note: string | null;

@RelationId((annotation: ExecutionAnnotation) => annotation.execution)
executionId: string;

@Index({ unique: true })
@OneToOne('ExecutionEntity', 'annotation', {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'executionId' })
execution: ExecutionEntity;

@ManyToMany('AnnotationTagEntity', 'annotations')
@JoinTable({
name: 'execution_annotation_tags', // table name for the junction table of this relation
joinColumn: {
name: 'annotationId',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'tagId',
referencedColumnName: 'id',
},
})
tags?: AnnotationTagEntity[];

@OneToMany('AnnotationTagMapping', 'annotations')
tagMappings: AnnotationTagMapping[];
}
4 changes: 4 additions & 0 deletions packages/cli/src/databases/entities/execution-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { idStringifier } from '../utils/transformers';
import type { ExecutionData } from './execution-data';
import type { ExecutionMetadata } from './execution-metadata';
import { WorkflowEntity } from './workflow-entity';
import type { ExecutionAnnotation } from '@/databases/entities/execution-annotation';

@Entity()
@Index(['workflowId', 'id'])
Expand Down Expand Up @@ -65,6 +66,9 @@ export class ExecutionEntity {
@OneToOne('ExecutionData', 'execution')
executionData: Relation<ExecutionData>;

@OneToOne('ExecutionAnnotation', 'execution')
annotation?: Relation<ExecutionAnnotation>;

@ManyToOne('WorkflowEntity')
workflow: WorkflowEntity;
}
6 changes: 6 additions & 0 deletions packages/cli/src/databases/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ import { WorkflowHistory } from './workflow-history';
import { Project } from './project';
import { ProjectRelation } from './project-relation';
import { InvalidAuthToken } from './invalid-auth-token';
import { AnnotationTagEntity } from './annotation-tag-entity';
import { AnnotationTagMapping } from './annotation-tag-mapping';
import { ExecutionAnnotation } from './execution-annotation';

export const entities = {
AnnotationTagEntity,
AnnotationTagMapping,
AuthIdentity,
AuthProviderSyncHistory,
AuthUser,
CredentialsEntity,
EventDestinations,
ExecutionAnnotation,
ExecutionEntity,
InstalledNodes,
InstalledPackages,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { MigrationContext, ReversibleMigration } from '@/databases/types';

const annotationsTableName = 'execution_annotations';
const annotationTagsTableName = 'annotation_tag_entity';
const annotationTagMappingsTableName = 'execution_annotation_tags';

export class CreateAnnotationTables1724753530828 implements ReversibleMigration {
async up({ schemaBuilder: { createTable, column } }: MigrationContext) {
await createTable(annotationsTableName)
.withColumns(
column('id').int.notNull.primary.autoGenerate,
column('executionId').int.notNull,
column('vote').varchar(6),
column('note').text,
)
.withIndexOn('executionId', true)
.withForeignKey('executionId', {
tableName: 'execution_entity',
columnName: 'id',
onDelete: 'CASCADE',
}).withTimestamps;

await createTable(annotationTagsTableName)
.withColumns(column('id').varchar(16).primary.notNull, column('name').varchar(24).notNull)
.withIndexOn('name', true).withTimestamps;

await createTable(annotationTagMappingsTableName)
.withColumns(column('annotationId').int.notNull, column('tagId').varchar(24).notNull)
.withForeignKey('annotationId', {
tableName: annotationsTableName,
columnName: 'id',
onDelete: 'CASCADE',
})
.withIndexOn('tagId')
.withIndexOn('annotationId')
.withForeignKey('tagId', {
tableName: annotationTagsTableName,
columnName: 'id',
onDelete: 'CASCADE',
});
}

async down({ schemaBuilder: { dropTable } }: MigrationContext) {
await dropTable(annotationTagMappingsTableName);
await dropTable(annotationTagsTableName);
await dropTable(annotationsTableName);
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/mysqldb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActiv
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata';
import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable';
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';

export const mysqlMigrations: Migration[] = [
InitialMigration1588157391238,
Expand Down Expand Up @@ -125,4 +126,5 @@ export const mysqlMigrations: Migration[] = [
AddConstraintToExecutionMetadata1720101653148,
CreateInvalidAuthTokenTable1723627610222,
RefactorExecutionIndices1723796243146,
CreateAnnotationTables1724753530828,
];
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/postgresdb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-R
import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata';
import { FixExecutionMetadataSequence1721377157740 } from './1721377157740-FixExecutionMetadataSequence';
import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable';
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';

export const postgresMigrations: Migration[] = [
InitialMigration1587669153312,
Expand Down Expand Up @@ -125,4 +126,5 @@ export const postgresMigrations: Migration[] = [
FixExecutionMetadataSequence1721377157740,
CreateInvalidAuthTokenTable1723627610222,
RefactorExecutionIndices1723796243146,
CreateAnnotationTables1724753530828,
];
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/sqlite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActiv
import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices';
import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata';
import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable';
import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables';

const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
Expand Down Expand Up @@ -119,6 +120,7 @@ const sqliteMigrations: Migration[] = [
AddConstraintToExecutionMetadata1720101653148,
CreateInvalidAuthTokenTable1723627610222,
RefactorExecutionIndices1723796243146,
CreateAnnotationTables1724753530828,
];

export { sqliteMigrations };
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Service } from 'typedi';
import { DataSource, Repository } from '@n8n/typeorm';
import { AnnotationTagMapping } from '@/databases/entities/annotation-tag-mapping';

@Service()
export class AnnotationTagMappingRepository extends Repository<AnnotationTagMapping> {
constructor(dataSource: DataSource) {
super(AnnotationTagMapping, dataSource.manager);
}

/**
* Overwrite annotation tags for the given execution. Annotation should already exist.
*/
async overwriteTags(annotationId: number, tagIds: string[]) {
return await this.manager.transaction(async (tx) => {
await tx.delete(AnnotationTagMapping, { annotationId });

const tagMappings = tagIds.map((tagId) => ({
annotationId,
tagId,
}));

return await tx.insert(AnnotationTagMapping, tagMappings);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Service } from 'typedi';
import { DataSource, Repository } from '@n8n/typeorm';
import { AnnotationTagEntity } from '@/databases/entities/annotation-tag-entity';

@Service()
export class AnnotationTagRepository extends Repository<AnnotationTagEntity> {
constructor(dataSource: DataSource) {
super(AnnotationTagEntity, dataSource.manager);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Service } from 'typedi';
import { DataSource, Repository } from '@n8n/typeorm';
import { ExecutionAnnotation } from '@/databases/entities/execution-annotation';

@Service()
export class ExecutionAnnotationRepository extends Repository<ExecutionAnnotation> {
constructor(dataSource: DataSource) {
super(ExecutionAnnotation, dataSource.manager);
}
}
Loading

0 comments on commit 022ddcb

Please sign in to comment.