Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into create-execution-an…
Browse files Browse the repository at this point in the history
…d-its-data-in-atomic-transaction
  • Loading branch information
netroy committed Aug 2, 2024
2 parents 182b251 + 47a68b0 commit 3427753
Show file tree
Hide file tree
Showing 43 changed files with 1,381 additions and 301 deletions.
30 changes: 30 additions & 0 deletions packages/@n8n_io/eslint-config/local-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,36 @@ module.exports = {
};
},
},

'no-type-unsafe-event-emitter': {
meta: {
type: 'problem',
docs: {
description: 'Disallow extending from `EventEmitter`, which is not type-safe.',
recommended: 'error',
},
messages: {
noExtendsEventEmitter: 'Extend from the type-safe `TypedEmitter` class instead.',
},
},
create(context) {
return {
ClassDeclaration(node) {
if (
node.superClass &&
node.superClass.type === 'Identifier' &&
node.superClass.name === 'EventEmitter' &&
node.id.name !== 'TypedEmitter'
) {
context.report({
node: node.superClass,
messageId: 'noExtendsEventEmitter',
});
}
},
};
},
},
};

const isJsonParseCall = (node) =>
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
rules: {
'n8n-local-rules/no-dynamic-import-template': 'error',
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
'n8n-local-rules/no-type-unsafe-event-emitter': 'error',
complexity: 'error',

// TODO: Remove this
Expand All @@ -44,6 +45,12 @@ module.exports = {
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
},
},
{
files: ['./test/**/*.ts'],
rules: {
'n8n-local-rules/no-type-unsafe-event-emitter': 'off',
},
},
{
files: ['./src/decorators/**/*.ts'],
rules: {
Expand Down
158 changes: 2 additions & 156 deletions packages/cli/src/InternalHooks.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import { Service } from 'typedi';
import { snakeCase } from 'change-case';
import { get as pslGet } from 'psl';
import type {
ExecutionStatus,
INodesGraphResult,
IRun,
ITelemetryTrackProperties,
IWorkflowBase,
} from 'n8n-workflow';
import { TelemetryHelpers } from 'n8n-workflow';

import { N8N_VERSION } from '@/constants';
import type { ITelemetryTrackProperties } from 'n8n-workflow';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { User } from '@db/entities/User';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
import type { ITelemetryUserDeletionData, IExecutionTrackProperties } from '@/Interfaces';
import type { ITelemetryUserDeletionData } from '@/Interfaces';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import { NodeTypes } from '@/NodeTypes';
import { Telemetry } from '@/telemetry';
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';

Expand All @@ -30,8 +17,6 @@ import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
export class InternalHooks {
constructor(
private readonly telemetry: Telemetry,
private readonly nodeTypes: NodeTypes,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
workflowStatisticsService: WorkflowStatisticsService,
// Can't use @ts-expect-error because only dev time tsconfig considers this as an error, but not build time
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -64,145 +49,6 @@ export class InternalHooks {
this.telemetry.track('User responded to personalization questions', personalizationSurveyData);
}

// eslint-disable-next-line complexity
async onWorkflowPostExecute(
_executionId: string,
workflow: IWorkflowBase,
runData?: IRun,
userId?: string,
) {
if (!workflow.id) {
return;
}

if (runData?.status === 'waiting') {
// No need to send telemetry or logs when the workflow hasn't finished yet.
return;
}

const telemetryProperties: IExecutionTrackProperties = {
workflow_id: workflow.id,
is_manual: false,
version_cli: N8N_VERSION,
success: false,
};

if (userId) {
telemetryProperties.user_id = userId;
}

if (runData?.data.resultData.error?.message?.includes('canceled')) {
runData.status = 'canceled';
}

telemetryProperties.success = !!runData?.finished;

// const executionStatus: ExecutionStatus = runData?.status ?? 'unknown';
const executionStatus: ExecutionStatus = runData
? determineFinalExecutionStatus(runData)
: 'unknown';

if (runData !== undefined) {
telemetryProperties.execution_mode = runData.mode;
telemetryProperties.is_manual = runData.mode === 'manual';

let nodeGraphResult: INodesGraphResult | null = null;

if (!telemetryProperties.success && runData?.data.resultData.error) {
telemetryProperties.error_message = runData?.data.resultData.error.message;
let errorNodeName =
'node' in runData?.data.resultData.error
? runData?.data.resultData.error.node?.name
: undefined;
telemetryProperties.error_node_type =
'node' in runData?.data.resultData.error
? runData?.data.resultData.error.node?.type
: undefined;

if (runData.data.resultData.lastNodeExecuted) {
const lastNode = TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.resultData.lastNodeExecuted,
);

if (lastNode !== undefined) {
telemetryProperties.error_node_type = lastNode.type;
errorNodeName = lastNode.name;
}
}

if (telemetryProperties.is_manual) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
telemetryProperties.node_graph = nodeGraphResult.nodeGraph;
telemetryProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);

if (errorNodeName) {
telemetryProperties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
}
}
}

if (telemetryProperties.is_manual) {
if (!nodeGraphResult) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
}

let userRole: 'owner' | 'sharee' | undefined = undefined;
if (userId) {
const role = await this.sharedWorkflowRepository.findSharingRole(userId, workflow.id);
if (role) {
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
}
}

const manualExecEventProperties: ITelemetryTrackProperties = {
user_id: userId,
workflow_id: workflow.id,
status: executionStatus,
executionStatus: runData?.status ?? 'unknown',
error_message: telemetryProperties.error_message as string,
error_node_type: telemetryProperties.error_node_type,
node_graph_string: telemetryProperties.node_graph_string as string,
error_node_id: telemetryProperties.error_node_id as string,
webhook_domain: null,
sharing_role: userRole,
};

if (!manualExecEventProperties.node_graph_string) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
manualExecEventProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
}

if (runData.data.startData?.destinationNode) {
const telemetryPayload = {
...manualExecEventProperties,
node_type: TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.startData?.destinationNode,
)?.type,
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
};

this.telemetry.track('Manual node exec finished', telemetryPayload);
} else {
nodeGraphResult.webhookNodeNames.forEach((name: string) => {
const execJson = runData.data.resultData.runData[name]?.[0]?.data?.main?.[0]?.[0]
?.json as { headers?: { origin?: string } };
if (execJson?.headers?.origin && execJson.headers.origin !== '') {
manualExecEventProperties.webhook_domain = pslGet(
execJson.headers.origin.replace(/^https?:\/\//, ''),
);
}
});

this.telemetry.track('Manual workflow exec finished', manualExecEventProperties);
}
}
}

this.telemetry.trackWorkflowExecution(telemetryProperties);
}

onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) {
const properties: ITelemetryTrackProperties = {
workflow_id: workflowId,
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/PublicApi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export declare namespace CredentialRequest {
>;

type Delete = AuthenticatedRequest<{ id: string }, {}, {}, Record<string, string>>;

type Transfer = AuthenticatedRequest<
{ workflowId: string },
{},
{ destinationProjectId: string }
>;
}

export type OperationID = 'getUsers' | 'getUser';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
toJsonSchema,
} from './credentials.service';
import { Container } from 'typedi';
import { z } from 'zod';
import { EnterpriseCredentialsService } from '@/credentials/credentials.service.ee';

export = {
createCredential: [
Expand All @@ -44,6 +46,20 @@ export = {
}
},
],
transferCredential: [
projectScope('credential:move', 'credential'),
async (req: CredentialRequest.Transfer, res: express.Response) => {
const body = z.object({ destinationProjectId: z.string() }).parse(req.body);

await Container.get(EnterpriseCredentialsService).transferOne(
req.user,
req.params.workflowId,
body.destinationProjectId,
);

res.status(204).send();
},
],
deleteCredential: [
projectScope('credential:delete', 'credential'),
async (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
put:
x-eov-operation-id: transferCredential
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
tags:
- Workflow
summary: Transfer a credential to another project.
description: Transfer a credential to another project.
parameters:
- $ref: '../schemas/parameters/credentialId.yml'
requestBody:
description: Destination project for the credential transfer.
content:
application/json:
schema:
type: object
properties:
destinationProjectId:
type: string
description: The ID of the project to transfer the credential to.
required:
- destinationProjectId
required: true
responses:
'200':
description: Operation successful.
'400':
$ref: '../../../../shared/spec/responses/badRequest.yml'
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
'404':
$ref: '../../../../shared/spec/responses/notFound.yml'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: id
in: path
description: The ID of the credential.
required: true
schema:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { globalScope, isLicensed, validCursor } from '../../shared/middlewares/global.middleware';
import type { Response } from 'express';
import type { ProjectRequest } from '@/requests';
import type { PaginatedRequest } from '@/PublicApi/types';
import Container from 'typedi';
import { ProjectController } from '@/controllers/project.controller';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import { encodeNextCursor } from '../../shared/services/pagination.service';

type Create = ProjectRequest.Create;
type Update = ProjectRequest.Update;
type Delete = ProjectRequest.Delete;
type GetAll = PaginatedRequest;

export = {
createProject: [
isLicensed('feat:projectRole:admin'),
globalScope('project:create'),
async (req: Create, res: Response) => {
const project = await Container.get(ProjectController).createProject(req);

return res.status(201).json(project);
},
],
updateProject: [
isLicensed('feat:projectRole:admin'),
globalScope('project:update'),
async (req: Update, res: Response) => {
await Container.get(ProjectController).updateProject(req);

return res.status(204).send();
},
],
deleteProject: [
isLicensed('feat:projectRole:admin'),
globalScope('project:delete'),
async (req: Delete, res: Response) => {
await Container.get(ProjectController).deleteProject(req);

return res.status(204).send();
},
],
getProjects: [
isLicensed('feat:projectRole:admin'),
globalScope('project:list'),
validCursor,
async (req: GetAll, res: Response) => {
const { offset = 0, limit = 100 } = req.query;

const [projects, count] = await Container.get(ProjectRepository).findAndCount({
skip: offset,
take: limit,
});

return res.json({
data: projects,
nextCursor: encodeNextCursor({
offset,
limit,
numberOfTotalRecords: count,
}),
});
},
],
};
Loading

0 comments on commit 3427753

Please sign in to comment.