refactor: Use string ids on Credentials, Workflows, Tags, and Executions DB entities (#5041)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-01-02 17:42:32 +01:00 committed by GitHub
parent 8bee04cd2a
commit ee28213538
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 468 additions and 645 deletions

View file

@ -55,11 +55,9 @@ export class ActiveExecutions {
fullExecutionData.retryOf = executionData.retryOf.toString(); fullExecutionData.retryOf = executionData.retryOf.toString();
} }
if ( const workflowId = executionData.workflowData.id;
executionData.workflowData.id !== undefined && if (workflowId !== undefined && WorkflowHelpers.isWorkflowIdValid(workflowId)) {
WorkflowHelpers.isWorkflowIdValid(executionData.workflowData.id.toString()) fullExecutionData.workflowId = workflowId;
) {
fullExecutionData.workflowId = executionData.workflowData.id.toString();
} }
const execution = ResponseHelper.flattenExecutionData(fullExecutionData); const execution = ResponseHelper.flattenExecutionData(fullExecutionData);

View file

@ -42,7 +42,6 @@ import {
IActivationError, IActivationError,
IQueuedWorkflowActivations, IQueuedWorkflowActivations,
IResponseCallbackData, IResponseCallbackData,
IWebhookDb,
IWorkflowDb, IWorkflowDb,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
} from '@/Interfaces'; } from '@/Interfaces';
@ -53,7 +52,8 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
import config from '@/config'; import config from '@/config';
import { User } from '@db/entities/User'; import { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { WebhookEntity } from '@db/entities/WebhookEntity';
import * as ActiveExecutions from '@/ActiveExecutions'; import * as ActiveExecutions from '@/ActiveExecutions';
import { createErrorExecution } from '@/GenericHelpers'; import { createErrorExecution } from '@/GenericHelpers';
import { WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT } from '@/constants'; import { WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT } from '@/constants';
@ -114,7 +114,7 @@ export class ActiveWorkflowRunner {
workflowId: workflowData.id, workflowId: workflowData.id,
}); });
try { try {
await this.add(workflowData.id.toString(), 'init', workflowData); await this.add(workflowData.id, 'init', workflowData);
Logger.verbose(`Successfully started workflow "${workflowData.name}"`, { Logger.verbose(`Successfully started workflow "${workflowData.name}"`, {
workflowName: workflowData.name, workflowName: workflowData.name,
workflowId: workflowData.id, workflowId: workflowData.id,
@ -165,10 +165,7 @@ export class ActiveWorkflowRunner {
} }
const activeWorkflows = await this.getActiveWorkflows(); const activeWorkflows = await this.getActiveWorkflows();
activeWorkflowIds = [ activeWorkflowIds = [...activeWorkflowIds, ...activeWorkflows.map((workflow) => workflow.id)];
...activeWorkflowIds,
...activeWorkflows.map((workflow) => workflow.id.toString()),
];
// Make sure IDs are unique // Make sure IDs are unique
activeWorkflowIds = Array.from(new Set(activeWorkflowIds)); activeWorkflowIds = Array.from(new Set(activeWorkflowIds));
@ -206,10 +203,10 @@ export class ActiveWorkflowRunner {
path = path.slice(0, -1); path = path.slice(0, -1);
} }
let webhook = (await Db.collections.Webhook.findOne({ let webhook = await Db.collections.Webhook.findOne({
webhookPath: path, webhookPath: path,
method: httpMethod, method: httpMethod,
})) as IWebhookDb; });
let webhookId: string | undefined; let webhookId: string | undefined;
// check if path is dynamic // check if path is dynamic
@ -280,7 +277,7 @@ export class ActiveWorkflowRunner {
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
const workflow = new Workflow({ const workflow = new Workflow({
id: webhook.workflowId.toString(), id: webhook.workflowId,
name: workflowData.name, name: workflowData.name,
nodes: workflowData.nodes, nodes: workflowData.nodes,
connections: workflowData.connections, connections: workflowData.connections,
@ -419,12 +416,12 @@ export class ActiveWorkflowRunner {
path = webhookData.path; path = webhookData.path;
const webhook = { const webhook: WebhookEntity = {
workflowId: webhookData.workflowId, workflowId: webhookData.workflowId,
webhookPath: path, webhookPath: path,
node: node.name, node: node.name,
method: webhookData.httpMethod, method: webhookData.httpMethod,
} as IWebhookDb; };
if (webhook.webhookPath.startsWith('/')) { if (webhook.webhookPath.startsWith('/')) {
webhook.webhookPath = webhook.webhookPath.slice(1); webhook.webhookPath = webhook.webhookPath.slice(1);
@ -549,11 +546,9 @@ export class ActiveWorkflowRunner {
await WorkflowHelpers.saveStaticData(workflow); await WorkflowHelpers.saveStaticData(workflow);
const webhook = { await Db.collections.Webhook.delete({
workflowId: workflowData.id, workflowId: workflowData.id,
} as IWebhookDb; });
await Db.collections.Webhook.delete(webhook);
} }
/** /**
@ -713,15 +708,15 @@ export class ActiveWorkflowRunner {
`The trigger node "${node.name}" of workflow "${workflowData.name}" failed with the error: "${error.message}". Will try to reactivate.`, `The trigger node "${node.name}" of workflow "${workflowData.name}" failed with the error: "${error.message}". Will try to reactivate.`,
{ {
nodeName: node.name, nodeName: node.name,
workflowId: workflowData.id.toString(), workflowId: workflowData.id,
workflowName: workflowData.name, workflowName: workflowData.name,
}, },
); );
// Remove the workflow as "active" // Remove the workflow as "active"
await this.activeWorkflows?.remove(workflowData.id.toString()); await this.activeWorkflows?.remove(workflowData.id);
this.activationErrors[workflowData.id.toString()] = { this.activationErrors[workflowData.id] = {
time: new Date().getTime(), time: new Date().getTime(),
error: { error: {
message: error.message, message: error.message,
@ -897,7 +892,7 @@ export class ActiveWorkflowRunner {
activationMode: WorkflowActivateMode, activationMode: WorkflowActivateMode,
workflowData: IWorkflowDb, workflowData: IWorkflowDb,
): void { ): void {
const workflowId = workflowData.id.toString(); const workflowId = workflowData.id;
const workflowName = workflowData.name; const workflowName = workflowData.name;
const retryFunction = async () => { const retryFunction = async () => {

View file

@ -279,7 +279,7 @@ export class CredentialsHelper extends ICredentialsHelper {
const credential = userId const credential = userId
? await Db.collections.SharedCredentials.findOneOrFail({ ? await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'], relations: ['credentials'],
where: { credentials: { id: nodeCredential.id, type }, user: { id: userId } }, where: { credentials: { id: nodeCredential.id, type }, userId },
}).then((shared) => shared.credentials) }).then((shared) => shared.credentials)
: await Db.collections.Credentials.findOneOrFail({ id: nodeCredential.id, type }); : await Db.collections.Credentials.findOneOrFail({ id: nodeCredential.id, type });
@ -290,7 +290,7 @@ export class CredentialsHelper extends ICredentialsHelper {
} }
return new Credentials( return new Credentials(
{ id: credential.id.toString(), name: credential.name }, { id: credential.id, name: credential.name },
credential.type, credential.type,
credential.nodesAccess, credential.nodesAccess,
credential.data, credential.data,
@ -581,7 +581,7 @@ export class CredentialsHelper extends ICredentialsHelper {
position: [0, 0], position: [0, 0],
credentials: { credentials: {
[credentialType]: { [credentialType]: {
id: credentialsDecrypted.id.toString(), id: credentialsDecrypted.id,
name: credentialsDecrypted.name, name: credentialsDecrypted.name,
}, },
}, },
@ -762,8 +762,7 @@ export async function getCredentialForUser(
export async function getCredentialWithoutUser( export async function getCredentialWithoutUser(
credentialId: string, credentialId: string,
): Promise<ICredentialsDb | undefined> { ): Promise<ICredentialsDb | undefined> {
const credential = await Db.collections.Credentials.findOne(credentialId); return Db.collections.Credentials.findOne(credentialId);
return credential;
} }
export function createCredentialsFromCredentialsEntity( export function createCredentialsFromCredentialsEntity(
@ -774,5 +773,5 @@ export function createCredentialsFromCredentialsEntity(
if (encrypt) { if (encrypt) {
return new Credentials({ id: null, name }, type, nodesAccess); return new Credentials({ id: null, name }, type, nodesAccess);
} }
return new Credentials({ id: id.toString(), name }, type, nodesAccess, data); return new Credentials({ id, name }, type, nodesAccess, data);
} }

View file

@ -14,7 +14,7 @@ import type {
ITaskData, ITaskData,
ITelemetrySettings, ITelemetrySettings,
ITelemetryTrackProperties, ITelemetryTrackProperties,
IWorkflowBase as IWorkflowBaseWorkflow, IWorkflowBase,
CredentialLoadingDetails, CredentialLoadingDetails,
Workflow, Workflow,
WorkflowActivateMode, WorkflowActivateMode,
@ -38,6 +38,7 @@ import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow'; import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { WebhookEntity } from '@db/entities/WebhookEntity';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
@ -71,7 +72,7 @@ export interface IDatabaseCollections {
Credentials: Repository<ICredentialsDb>; Credentials: Repository<ICredentialsDb>;
Execution: Repository<IExecutionFlattedDb>; Execution: Repository<IExecutionFlattedDb>;
Workflow: Repository<WorkflowEntity>; Workflow: Repository<WorkflowEntity>;
Webhook: Repository<IWebhookDb>; Webhook: Repository<WebhookEntity>;
Tag: Repository<TagEntity>; Tag: Repository<TagEntity>;
Role: Repository<Role>; Role: Repository<Role>;
User: Repository<User>; User: Repository<User>;
@ -83,28 +84,12 @@ export interface IDatabaseCollections {
WorkflowStatistics: Repository<WorkflowStatistics>; WorkflowStatistics: Repository<WorkflowStatistics>;
} }
export interface IWebhookDb {
workflowId: number | string;
webhookPath: string;
method: string;
node: string;
webhookId?: string;
pathLength?: number;
}
// ---------------------------------- // ----------------------------------
// tags // tags
// ---------------------------------- // ----------------------------------
export interface ITagDb {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
}
export interface ITagToImport { export interface ITagToImport {
id: string | number; id: string;
name: string; name: string;
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
@ -114,20 +99,16 @@ export type UsageCount = {
usageCount: number; usageCount: number;
}; };
export type ITagWithCountDb = ITagDb & UsageCount; export type ITagWithCountDb = TagEntity & UsageCount;
// ---------------------------------- // ----------------------------------
// workflows // workflows
// ---------------------------------- // ----------------------------------
export interface IWorkflowBase extends IWorkflowBaseWorkflow {
id?: number | string;
}
// Almost identical to editor-ui.Interfaces.ts // Almost identical to editor-ui.Interfaces.ts
export interface IWorkflowDb extends IWorkflowBase { export interface IWorkflowDb extends IWorkflowBase {
id: number | string; id: string;
tags?: ITagDb[]; tags?: TagEntity[];
} }
export interface IWorkflowToImport extends IWorkflowBase { export interface IWorkflowToImport extends IWorkflowBase {
@ -148,35 +129,27 @@ export interface ICredentialsBase {
} }
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted { export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted {
id: number | string; id: string;
name: string; name: string;
shared?: SharedCredentials[]; shared?: SharedCredentials[];
} }
export interface ICredentialsResponse extends ICredentialsDb { export type ICredentialsDecryptedDb = ICredentialsBase & ICredentialsDecrypted;
id: string;
}
export interface ICredentialsDecryptedDb extends ICredentialsBase, ICredentialsDecrypted { export type ICredentialsDecryptedResponse = ICredentialsDecryptedDb;
id: number | string;
}
export interface ICredentialsDecryptedResponse extends ICredentialsDecryptedDb {
id: string;
}
export type DatabaseType = 'mariadb' | 'postgresdb' | 'mysqldb' | 'sqlite'; export type DatabaseType = 'mariadb' | 'postgresdb' | 'mysqldb' | 'sqlite';
export type SaveExecutionDataType = 'all' | 'none'; export type SaveExecutionDataType = 'all' | 'none';
export interface IExecutionBase { export interface IExecutionBase {
id?: number | string; id?: string;
mode: WorkflowExecuteMode; mode: WorkflowExecuteMode;
startedAt: Date; startedAt: Date;
stoppedAt?: Date; // empty value means execution is still running stoppedAt?: Date; // empty value means execution is still running
workflowId?: string; // To be able to filter executions easily // workflowId?: string; // To be able to filter executions easily //
finished: boolean; finished: boolean;
retryOf?: number | string; // If it is a retry, the id of the execution it is a retry of. retryOf?: string; // If it is a retry, the id of the execution it is a retry of.
retrySuccessId?: number | string; // If it failed and a retry did succeed. The id of the successful retry. retrySuccessId?: string; // If it failed and a retry did succeed. The id of the successful retry.
} }
// Data in regular format with references // Data in regular format with references
@ -208,7 +181,7 @@ export interface IExecutionFlatted extends IExecutionBase {
} }
export interface IExecutionFlattedDb extends IExecutionBase { export interface IExecutionFlattedDb extends IExecutionBase {
id: number | string; id: string;
data: string; data: string;
waitTill?: Date | null; waitTill?: Date | null;
workflowData: Omit<IWorkflowBase, 'pinData'>; workflowData: Omit<IWorkflowBase, 'pinData'>;
@ -220,14 +193,14 @@ export interface IExecutionFlattedResponse extends IExecutionFlatted {
} }
export interface IExecutionResponseApi { export interface IExecutionResponseApi {
id: number | string; id: string;
mode: WorkflowExecuteMode; mode: WorkflowExecuteMode;
startedAt: Date; startedAt: Date;
stoppedAt?: Date; stoppedAt?: Date;
workflowId?: string; workflowId?: string;
finished: boolean; finished: boolean;
retryOf?: number | string; retryOf?: string;
retrySuccessId?: number | string; retrySuccessId?: string;
data?: object; data?: object;
waitTill?: Date | null; waitTill?: Date | null;
workflowData: IWorkflowBase; workflowData: IWorkflowBase;
@ -685,7 +658,7 @@ export interface IWorkflowExecutionDataProcess {
executionData?: IRunExecutionData; executionData?: IRunExecutionData;
runData?: IRunData; runData?: IRunData;
pinData?: IPinData; pinData?: IPinData;
retryOf?: number | string; retryOf?: string;
sessionId?: string; sessionId?: string;
startNodes?: string[]; startNodes?: string[];
workflowData: IWorkflowBase; workflowData: IWorkflowBase;

View file

@ -5,6 +5,7 @@ import {
INodeTypes, INodeTypes,
IRun, IRun,
ITelemetryTrackProperties, ITelemetryTrackProperties,
IWorkflowBase,
TelemetryHelpers, TelemetryHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { get as pslGet } from 'psl'; import { get as pslGet } from 'psl';
@ -12,7 +13,6 @@ import {
IDiagnosticInfo, IDiagnosticInfo,
IInternalHooksClass, IInternalHooksClass,
ITelemetryUserDeletionData, ITelemetryUserDeletionData,
IWorkflowBase,
IWorkflowDb, IWorkflowDb,
IExecutionTrackProperties, IExecutionTrackProperties,
} from '@/Interfaces'; } from '@/Interfaces';
@ -114,7 +114,7 @@ export class InternalHooksClass implements IInternalHooksClass {
let userRole: 'owner' | 'sharee' | undefined = undefined; let userRole: 'owner' | 'sharee' | undefined = undefined;
if (userId && workflow.id) { if (userId && workflow.id) {
const role = await RoleService.getUserRoleForWorkflow(userId, workflow.id.toString()); const role = await RoleService.getUserRoleForWorkflow(userId, workflow.id);
if (role) { if (role) {
userRole = role.name === 'owner' ? 'owner' : 'sharee'; userRole = role.name === 'owner' ? 'owner' : 'sharee';
} }
@ -150,7 +150,7 @@ export class InternalHooksClass implements IInternalHooksClass {
} }
const properties: IExecutionTrackProperties = { const properties: IExecutionTrackProperties = {
workflow_id: workflow.id.toString(), workflow_id: workflow.id,
is_manual: false, is_manual: false,
version_cli: this.versionCli, version_cli: this.versionCli,
success: false, success: false,
@ -208,7 +208,7 @@ export class InternalHooksClass implements IInternalHooksClass {
let userRole: 'owner' | 'sharee' | undefined = undefined; let userRole: 'owner' | 'sharee' | undefined = undefined;
if (userId) { if (userId) {
const role = await RoleService.getUserRoleForWorkflow(userId, workflow.id.toString()); const role = await RoleService.getUserRoleForWorkflow(userId, workflow.id);
if (role) { if (role) {
userRole = role.name === 'owner' ? 'owner' : 'sharee'; userRole = role.name === 'owner' ? 'owner' : 'sharee';
} }
@ -216,7 +216,7 @@ export class InternalHooksClass implements IInternalHooksClass {
const manualExecEventProperties: ITelemetryTrackProperties = { const manualExecEventProperties: ITelemetryTrackProperties = {
user_id: userId, user_id: userId,
workflow_id: workflow.id.toString(), workflow_id: workflow.id,
status: properties.success ? 'success' : 'failed', status: properties.success ? 'success' : 'failed',
error_message: properties.error_message as string, error_message: properties.error_message as string,
error_node_type: properties.error_node_type, error_node_type: properties.error_node_type,
@ -512,14 +512,14 @@ export class InternalHooksClass implements IInternalHooksClass {
*/ */
async onFirstProductionWorkflowSuccess(data: { async onFirstProductionWorkflowSuccess(data: {
user_id: string; user_id: string;
workflow_id: string | number; workflow_id: string;
}): Promise<void> { }): Promise<void> {
return this.telemetry.track('Workflow first prod success', data, { withPostHog: true }); return this.telemetry.track('Workflow first prod success', data, { withPostHog: true });
} }
async onFirstWorkflowDataLoad(data: { async onFirstWorkflowDataLoad(data: {
user_id: string; user_id: string;
workflow_id: string | number; workflow_id: string;
node_type: string; node_type: string;
node_id: string; node_id: string;
credential_type?: string; credential_type?: string;

View file

@ -37,7 +37,7 @@ export type PaginatatedRequest = AuthenticatedRequest<
limit?: number; limit?: number;
cursor?: string; cursor?: string;
offset?: number; offset?: number;
lastId?: number; lastId?: string;
} }
>; >;
export declare namespace ExecutionRequest { export declare namespace ExecutionRequest {
@ -51,12 +51,12 @@ export declare namespace ExecutionRequest {
cursor?: string; cursor?: string;
offset?: number; offset?: number;
includeData?: boolean; includeData?: boolean;
workflowId?: number; workflowId?: string;
lastId?: number; lastId?: string;
} }
>; >;
type Get = AuthenticatedRequest<{ id: number }, {}, {}, { includeData?: boolean }>; type Get = AuthenticatedRequest<{ id: string }, {}, {}, { includeData?: boolean }>;
type Delete = Get; type Delete = Get;
} }
@ -81,9 +81,9 @@ export declare namespace WorkflowRequest {
>; >;
type Create = AuthenticatedRequest<{}, {}, WorkflowEntity, {}>; type Create = AuthenticatedRequest<{}, {}, WorkflowEntity, {}>;
type Get = AuthenticatedRequest<{ id: number }, {}, {}, {}>; type Get = AuthenticatedRequest<{ id: string }, {}, {}, {}>;
type Delete = Get; type Delete = Get;
type Update = AuthenticatedRequest<{ id: number }, {}, WorkflowEntity, {}>; type Update = AuthenticatedRequest<{ id: string }, {}, WorkflowEntity, {}>;
type Activate = Get; type Activate = Get;
} }
@ -140,11 +140,11 @@ type PaginationBase = { limit: number };
type PaginationOffsetDecoded = PaginationBase & { offset: number }; type PaginationOffsetDecoded = PaginationBase & { offset: number };
type PaginationCursorDecoded = PaginationBase & { lastId: number }; type PaginationCursorDecoded = PaginationBase & { lastId: string };
type OffsetPagination = PaginationBase & { offset: number; numberOfTotalRecords: number }; type OffsetPagination = PaginationBase & { offset: number; numberOfTotalRecords: number };
type CursorPagination = PaginationBase & { lastId: number; numberOfNextRecords: number }; type CursorPagination = PaginationBase & { lastId: string; numberOfNextRecords: number };
export interface IRequired { export interface IRequired {
required?: string[]; required?: string[];
not?: { required?: string[] }; not?: { required?: string[] };

View file

@ -39,7 +39,7 @@ export = {
const savedCredential = await saveCredential(newCredential, req.user, encryptedData); const savedCredential = await saveCredential(newCredential, req.user, encryptedData);
// LoggerProxy.verbose('New credential created', { // LoggerProxy.verbose('New credential created', {
// credentialId: newCredential.id, // credentialsId: newCredential.id,
// ownerId: req.user.id, // ownerId: req.user.id,
// }); // });
@ -77,8 +77,6 @@ export = {
} }
await removeCredential(credential); await removeCredential(credential);
credential.id = Number(credentialId);
return res.json(sanitizeCredentials(credential)); return res.json(sanitizeCredentials(credential));
}, },
], ],

View file

@ -1,8 +1,4 @@
/* eslint-disable no-restricted-syntax */ import type { FindConditions } from 'typeorm';
/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { FindOneOptions } from 'typeorm';
import { UserSettings, Credentials } from 'n8n-core'; import { UserSettings, Credentials } from 'n8n-core';
import { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow'; import { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
@ -14,29 +10,17 @@ import { ExternalHooks } from '@/ExternalHooks';
import { IDependency, IJsonSchema } from '../../../types'; import { IDependency, IJsonSchema } from '../../../types';
import { CredentialRequest } from '@/requests'; import { CredentialRequest } from '@/requests';
export async function getCredentials( export async function getCredentials(credentialId: string): Promise<ICredentialsDb | undefined> {
credentialId: number | string,
): Promise<ICredentialsDb | undefined> {
return Db.collections.Credentials.findOne(credentialId); return Db.collections.Credentials.findOne(credentialId);
} }
export async function getSharedCredentials( export async function getSharedCredentials(
userId: string, userId: string,
credentialId: number | string, credentialId: string,
relations?: string[], relations?: string[],
): Promise<SharedCredentials | undefined> { ): Promise<SharedCredentials | undefined> {
const options: FindOneOptions = { const where: FindConditions<SharedCredentials> = { userId, credentialsId: credentialId };
where: { return Db.collections.SharedCredentials.findOne({ where, relations });
user: { id: userId },
credentials: { id: credentialId },
},
};
if (relations) {
options.relations = relations;
}
return Db.collections.SharedCredentials.findOne(options);
} }
export async function createCredential( export async function createCredential(

View file

@ -12,7 +12,7 @@ delete:
description: The credential ID that needs to be deleted description: The credential ID that needs to be deleted
required: true required: true
schema: schema:
type: number type: string
responses: responses:
'200': '200':
description: Operation successful. description: Operation successful.

View file

@ -5,9 +5,9 @@ required:
type: object type: object
properties: properties:
id: id:
type: number type: string
readOnly: true readOnly: true
example: 42 example: '42'
name: name:
type: string type: string
example: Joe's Github Credentials example: Joe's Github Credentials

View file

@ -36,7 +36,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
await BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(execution.id.toString()); await BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(execution.id);
await deleteExecution(execution); await deleteExecution(execution);
@ -97,7 +97,7 @@ export = {
// get running workflows so we exclude them from the result // get running workflows so we exclude them from the result
const runningExecutionsIds = ActiveExecutions.getInstance() const runningExecutionsIds = ActiveExecutions.getInstance()
.getActiveExecutions() .getActiveExecutions()
.map(({ id }) => Number(id)); .map(({ id }) => id);
const filters = { const filters = {
status, status,
@ -110,7 +110,7 @@ export = {
const executions = await getExecutions(filters); const executions = await getExecutions(filters);
const newLastId = !executions.length ? 0 : (executions.slice(-1)[0].id as number); const newLastId = !executions.length ? '0' : executions.slice(-1)[0].id;
filters.lastId = newLastId; filters.lastId = newLastId;

View file

@ -60,14 +60,14 @@ function getExecutionSelectableProperties(includeData?: boolean): Array<keyof IE
export async function getExecutions(params: { export async function getExecutions(params: {
limit: number; limit: number;
includeData?: boolean; includeData?: boolean;
lastId?: number; lastId?: string;
workflowIds?: number[]; workflowIds?: string[];
status?: ExecutionStatus; status?: ExecutionStatus;
excludedExecutionsIds?: number[]; excludedExecutionsIds?: string[];
}): Promise<IExecutionResponseApi[]> { }): Promise<IExecutionResponseApi[]> {
type WhereClause = Record< type WhereClause = Record<
string, string,
string | boolean | FindOperator<number | Partial<ExecutionEntity>> string | boolean | FindOperator<string | Partial<ExecutionEntity>>
>; >;
let where: WhereClause = {}; let where: WhereClause = {};
@ -103,10 +103,10 @@ export async function getExecutions(params: {
export async function getExecutionsCount(data: { export async function getExecutionsCount(data: {
limit: number; limit: number;
lastId?: number; lastId?: string;
workflowIds?: number[]; workflowIds?: string[];
status?: ExecutionStatus; status?: ExecutionStatus;
excludedWorkflowIds?: number[]; excludedWorkflowIds?: string[];
}): Promise<number> { }): Promise<number> {
const executions = await Db.collections.Execution.count({ const executions = await Db.collections.Execution.count({
where: { where: {
@ -122,15 +122,15 @@ export async function getExecutionsCount(data: {
} }
export async function getExecutionInWorkflows( export async function getExecutionInWorkflows(
id: number, id: string,
workflows: string[], workflowIds: string[],
includeData?: boolean, includeData?: boolean,
): Promise<IExecutionResponseApi | undefined> { ): Promise<IExecutionResponseApi | undefined> {
const execution = await Db.collections.Execution.findOne({ const execution = await Db.collections.Execution.findOne({
select: getExecutionSelectableProperties(includeData), select: getExecutionSelectableProperties(includeData),
where: { where: {
id, id,
workflowId: In(workflows), workflowId: In(workflowIds),
}, },
}); });

View file

@ -19,8 +19,8 @@ get:
description: Workflow to filter the executions by. description: Workflow to filter the executions by.
required: false required: false
schema: schema:
type: number type: string
example: 1000 example: '1000'
- $ref: '../../../../shared/spec/parameters/limit.yml' - $ref: '../../../../shared/spec/parameters/limit.yml'
- $ref: '../../../../shared/spec/parameters/cursor.yml' - $ref: '../../../../shared/spec/parameters/cursor.yml'
responses: responses:

View file

@ -1,8 +1,8 @@
type: object type: object
properties: properties:
id: id:
type: number type: string
example: 1000 example: '1000'
data: data:
type: object type: object
finished: finished:
@ -17,7 +17,7 @@ properties:
retrySuccessId: retrySuccessId:
type: string type: string
nullable: true nullable: true
example: 2 example: '2'
startedAt: startedAt:
type: string type: string
format: date-time format: date-time
@ -26,7 +26,7 @@ properties:
format: date-time format: date-time
workflowId: workflowId:
type: string type: string
example: 1000 example: '1000'
waitTill: waitTill:
type: string type: string
nullable: true nullable: true

View file

@ -3,4 +3,4 @@ in: path
description: The ID of the execution. description: The ID of the execution.
required: true required: true
schema: schema:
type: number type: string

View file

@ -3,4 +3,4 @@ in: path
description: The ID of the workflow. description: The ID of the workflow.
required: true required: true
schema: schema:
type: number type: string

View file

@ -2,7 +2,7 @@ type: object
properties: properties:
id: id:
type: string type: string
example: 12 example: '12'
name: name:
type: string type: string
example: Production example: Production

View file

@ -7,9 +7,9 @@ required:
- settings - settings
properties: properties:
id: id:
type: number type: string
readOnly: true readOnly: true
example: 1 example: '1'
name: name:
type: string type: string
example: Workflow 1 example: Workflow 1

View file

@ -17,7 +17,7 @@ properties:
maxLength: 3600 maxLength: 3600
errorWorkflow: errorWorkflow:
type: string type: string
example: 10 example: '10'
description: The ID of the workflow that contains the error trigger node. description: The ID of the workflow that contains the error trigger node.
timezone: timezone:
type: string type: string

View file

@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
import { FindManyOptions, In, ObjectLiteral } from 'typeorm'; import { FindConditions, FindManyOptions, In } from 'typeorm';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
@ -60,7 +60,7 @@ export = {
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => { async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
const { id } = req.params; const { id } = req.params;
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString()); const sharedWorkflow = await getSharedWorkflow(req.user, id);
if (!sharedWorkflow) { if (!sharedWorkflow) {
// user trying to access a workflow he does not own // user trying to access a workflow he does not own
@ -70,13 +70,13 @@ export = {
if (sharedWorkflow.workflow.active) { if (sharedWorkflow.workflow.active) {
// deactivate before deleting // deactivate before deleting
await ActiveWorkflowRunner.getInstance().remove(id.toString()); await ActiveWorkflowRunner.getInstance().remove(id);
} }
await Db.collections.Workflow.delete(id); await Db.collections.Workflow.delete(id);
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, id.toString(), true); void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, id, true);
await ExternalHooks().run('workflow.afterDelete', [id.toString()]); await ExternalHooks().run('workflow.afterDelete', [id]);
return res.json(sharedWorkflow.workflow); return res.json(sharedWorkflow.workflow);
}, },
@ -86,7 +86,7 @@ export = {
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => { async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
const { id } = req.params; const { id } = req.params;
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString()); const sharedWorkflow = await getSharedWorkflow(req.user, id);
if (!sharedWorkflow) { if (!sharedWorkflow) {
// user trying to access a workflow he does not own // user trying to access a workflow he does not own
@ -111,26 +111,27 @@ export = {
let workflows: WorkflowEntity[]; let workflows: WorkflowEntity[];
let count: number; let count: number;
const query: FindManyOptions<WorkflowEntity> & { where: ObjectLiteral } = { const where: FindConditions<WorkflowEntity> = {
...(active !== undefined && { active }),
};
const query: FindManyOptions<WorkflowEntity> = {
skip: offset, skip: offset,
take: limit, take: limit,
where: { where,
...(active !== undefined && { active }),
},
...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }), ...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }),
}; };
if (isInstanceOwner(req.user)) { if (isInstanceOwner(req.user)) {
if (tags) { if (tags) {
const workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags)); const workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
Object.assign(query.where, { id: In(workflowIds) }); Object.assign(where, { id: In(workflowIds) });
} }
workflows = await getWorkflows(query); workflows = await getWorkflows(query);
count = await getWorkflowsCount(query); count = await getWorkflowsCount(query);
} else { } else {
const options: { workflowIds?: number[] } = {}; const options: { workflowIds?: string[] } = {};
if (tags) { if (tags) {
options.workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags)); options.workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
@ -147,7 +148,7 @@ export = {
const workflowsIds = sharedWorkflows.map((shareWorkflow) => shareWorkflow.workflowId); const workflowsIds = sharedWorkflows.map((shareWorkflow) => shareWorkflow.workflowId);
Object.assign(query.where, { id: In(workflowsIds) }); Object.assign(where, { id: In(workflowsIds) });
workflows = await getWorkflows(query); workflows = await getWorkflows(query);
@ -176,7 +177,7 @@ export = {
const updateData = new WorkflowEntity(); const updateData = new WorkflowEntity();
Object.assign(updateData, req.body); Object.assign(updateData, req.body);
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString()); const sharedWorkflow = await getSharedWorkflow(req.user, id);
if (!sharedWorkflow) { if (!sharedWorkflow) {
// user trying to access a workflow he does not own // user trying to access a workflow he does not own
@ -196,7 +197,7 @@ export = {
if (sharedWorkflow.workflow.active) { if (sharedWorkflow.workflow.active) {
// When workflow gets saved always remove it as the triggers could have been // When workflow gets saved always remove it as the triggers could have been
// changed and so the changes would not take effect // changed and so the changes would not take effect
await workflowRunner.remove(id.toString()); await workflowRunner.remove(id);
} }
try { try {
@ -209,7 +210,7 @@ export = {
if (sharedWorkflow.workflow.active) { if (sharedWorkflow.workflow.active) {
try { try {
await workflowRunner.add(sharedWorkflow.workflowId.toString(), 'update'); await workflowRunner.add(sharedWorkflow.workflowId, 'update');
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
return res.status(400).json({ message: error.message }); return res.status(400).json({ message: error.message });
@ -230,7 +231,7 @@ export = {
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => { async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
const { id } = req.params; const { id } = req.params;
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString()); const sharedWorkflow = await getSharedWorkflow(req.user, id);
if (!sharedWorkflow) { if (!sharedWorkflow) {
// user trying to access a workflow he does not own // user trying to access a workflow he does not own
@ -240,10 +241,7 @@ export = {
if (!sharedWorkflow.workflow.active) { if (!sharedWorkflow.workflow.active) {
try { try {
await ActiveWorkflowRunner.getInstance().add( await ActiveWorkflowRunner.getInstance().add(sharedWorkflow.workflowId, 'activate');
sharedWorkflow.workflowId.toString(),
'activate',
);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
return res.status(400).json({ message: error.message }); return res.status(400).json({ message: error.message });
@ -267,7 +265,7 @@ export = {
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => { async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
const { id } = req.params; const { id } = req.params;
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString()); const sharedWorkflow = await getSharedWorkflow(req.user, id);
if (!sharedWorkflow) { if (!sharedWorkflow) {
// user trying to access a workflow he does not own // user trying to access a workflow he does not own
@ -278,7 +276,7 @@ export = {
const workflowRunner = ActiveWorkflowRunner.getInstance(); const workflowRunner = ActiveWorkflowRunner.getInstance();
if (sharedWorkflow.workflow.active) { if (sharedWorkflow.workflow.active) {
await workflowRunner.remove(sharedWorkflow.workflowId.toString()); await workflowRunner.remove(sharedWorkflow.workflowId);
await setWorkflowAsInactive(sharedWorkflow.workflow); await setWorkflowAsInactive(sharedWorkflow.workflow);

View file

@ -20,7 +20,7 @@ export async function getSharedWorkflowIds(user: User): Promise<string[]> {
where: { user }, where: { user },
}); });
return sharedWorkflows.map(({ workflowId }) => workflowId.toString()); return sharedWorkflows.map(({ workflowId }) => workflowId);
} }
export async function getSharedWorkflow( export async function getSharedWorkflow(
@ -30,7 +30,7 @@ export async function getSharedWorkflow(
return Db.collections.SharedWorkflow.findOne({ return Db.collections.SharedWorkflow.findOne({
where: { where: {
...(!isInstanceOwner(user) && { user }), ...(!isInstanceOwner(user) && { user }),
...(workflowId && { workflow: { id: workflowId } }), ...(workflowId && { workflowId }),
}, },
relations: [...insertIf(!config.getEnv('workflowTagsDisabled'), ['workflow.tags']), 'workflow'], relations: [...insertIf(!config.getEnv('workflowTagsDisabled'), ['workflow.tags']), 'workflow'],
}); });
@ -40,19 +40,19 @@ export async function getSharedWorkflows(
user: User, user: User,
options: { options: {
relations?: string[]; relations?: string[];
workflowIds?: number[]; workflowIds?: string[];
}, },
): Promise<SharedWorkflow[]> { ): Promise<SharedWorkflow[]> {
return Db.collections.SharedWorkflow.find({ return Db.collections.SharedWorkflow.find({
where: { where: {
...(!isInstanceOwner(user) && { user }), ...(!isInstanceOwner(user) && { user }),
...(options.workflowIds && { workflow: { id: In(options.workflowIds) } }), ...(options.workflowIds && { workflowId: In(options.workflowIds) }),
}, },
...(options.relations && { relations: options.relations }), ...(options.relations && { relations: options.relations }),
}); });
} }
export async function getWorkflowById(id: number): Promise<WorkflowEntity | undefined> { export async function getWorkflowById(id: string): Promise<WorkflowEntity | undefined> {
return Db.collections.Workflow.findOne({ return Db.collections.Workflow.findOne({
where: { id }, where: { id },
}); });
@ -62,7 +62,7 @@ export async function getWorkflowById(id: number): Promise<WorkflowEntity | unde
* Returns the workflow IDs that have certain tags. * Returns the workflow IDs that have certain tags.
* Intersection! e.g. workflow needs to have all provided tags. * Intersection! e.g. workflow needs to have all provided tags.
*/ */
export async function getWorkflowIdsViaTags(tags: string[]): Promise<number[]> { export async function getWorkflowIdsViaTags(tags: string[]): Promise<string[]> {
const dbTags = await Db.collections.Tag.find({ const dbTags = await Db.collections.Tag.find({
where: { name: In(tags) }, where: { name: In(tags) },
relations: ['workflows'], relations: ['workflows'],
@ -118,7 +118,7 @@ export async function getWorkflowsCount(options: FindManyOptions<WorkflowEntity>
} }
export async function updateWorkflow( export async function updateWorkflow(
workflowId: number, workflowId: string,
updateData: WorkflowEntity, updateData: WorkflowEntity,
): Promise<UpdateResult> { ): Promise<UpdateResult> {
return Db.collections.Workflow.update(workflowId, updateData); return Db.collections.Workflow.update(workflowId, updateData);

View file

@ -225,7 +225,7 @@ export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutio
}; };
if (fullExecutionData.id !== undefined) { if (fullExecutionData.id !== undefined) {
returnData.id = fullExecutionData.id.toString(); returnData.id = fullExecutionData.id;
} }
if (fullExecutionData.retryOf !== undefined) { if (fullExecutionData.retryOf !== undefined) {
@ -246,7 +246,7 @@ export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutio
*/ */
export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse { export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse {
const returnData: IExecutionResponse = { const returnData: IExecutionResponse = {
id: fullExecutionData.id.toString(), id: fullExecutionData.id,
workflowData: fullExecutionData.workflowData as IWorkflowDb, workflowData: fullExecutionData.workflowData as IWorkflowDb,
data: parse(fullExecutionData.data), data: parse(fullExecutionData.data),
mode: fullExecutionData.mode, mode: fullExecutionData.mode,

View file

@ -983,8 +983,7 @@ class App {
`/${this.restEndpoint}/active`, `/${this.restEndpoint}/active`,
ResponseHelper.send(async (req: WorkflowRequest.GetAllActive) => { ResponseHelper.send(async (req: WorkflowRequest.GetAllActive) => {
const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(req.user); const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(req.user);
return activeWorkflows.map(({ id }) => id);
return activeWorkflows.map(({ id }) => id.toString());
}), }),
); );
@ -1365,22 +1364,21 @@ class App {
for (const data of executingWorkflows) { for (const data of executingWorkflows) {
if ( if (
(filter.workflowId !== undefined && filter.workflowId !== data.workflowId) || (filter.workflowId !== undefined && filter.workflowId !== data.workflowId) ||
(data.workflowId !== undefined && (data.workflowId !== undefined && !sharedWorkflowIds.includes(data.workflowId))
!sharedWorkflowIds.includes(data.workflowId.toString()))
) { ) {
continue; continue;
} }
returnData.push({ returnData.push({
id: data.id.toString(), id: data.id,
workflowId: data.workflowId === undefined ? '' : data.workflowId.toString(), workflowId: data.workflowId === undefined ? '' : data.workflowId,
mode: data.mode, mode: data.mode,
retryOf: data.retryOf, retryOf: data.retryOf,
startedAt: new Date(data.startedAt), startedAt: new Date(data.startedAt),
}); });
} }
returnData.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); returnData.sort((a, b) => Number(b.id) - Number(a.id));
return returnData; return returnData;
}, },
@ -1435,7 +1433,7 @@ class App {
const queue = await Queue.getInstance(); const queue = await Queue.getInstance();
const currentJobs = await queue.getJobs(['active', 'waiting']); const currentJobs = await queue.getJobs(['active', 'waiting']);
const job = currentJobs.find((job) => job.data.executionId.toString() === req.params.id); const job = currentJobs.find((job) => job.data.executionId === req.params.id);
if (!job) { if (!job) {
throw new Error(`Could not stop "${req.params.id}" as it is no longer in queue.`); throw new Error(`Could not stop "${req.params.id}" as it is no longer in queue.`);

View file

@ -18,7 +18,7 @@ export function sortByRequestOrder(
{ requestOrder }: { requestOrder: string[] }, { requestOrder }: { requestOrder: string[] },
) { ) {
const tagMap = tags.reduce<Record<string, TagEntity>>((acc, tag) => { const tagMap = tags.reduce<Record<string, TagEntity>>((acc, tag) => {
acc[tag.id.toString()] = tag; acc[tag.id] = tag;
return acc; return acc;
}, {}); }, {});
@ -50,6 +50,7 @@ export async function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithC
.getRawMany() .getRawMany()
.then((tagsWithCount) => { .then((tagsWithCount) => {
tagsWithCount.forEach((tag) => { tagsWithCount.forEach((tag) => {
// NOTE: since this code doesn't use the DB entities, we need to stringify the IDs manually
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
tag.id = tag.id.toString(); tag.id = tag.id.toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@ -102,7 +103,7 @@ const findOrCreateTag = async (
// Assume tag is identical if createdAt date is the same to preserve a changed tag name // Assume tag is identical if createdAt date is the same to preserve a changed tag name
const identicalMatch = tagsEntities.find( const identicalMatch = tagsEntities.find(
(existingTag) => (existingTag) =>
existingTag.id.toString() === importTag.id.toString() && existingTag.id === importTag.id &&
existingTag.createdAt && existingTag.createdAt &&
importTag.createdAt && importTag.createdAt &&
existingTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(), existingTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(),

View file

@ -213,7 +213,7 @@ export class TestWebhooks {
// Remove test-webhooks automatically if they do not get called (after 120 seconds) // Remove test-webhooks automatically if they do not get called (after 120 seconds)
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
this.cancelTestWebhook(workflowData.id.toString()); this.cancelTestWebhook(workflowData.id);
}, 120000); }, 120000);
let key: string; let key: string;
@ -260,7 +260,7 @@ export class TestWebhooks {
for (const webhookKey of Object.keys(this.testWebhookData)) { for (const webhookKey of Object.keys(this.testWebhookData)) {
const webhookData = this.testWebhookData[webhookKey]; const webhookData = this.testWebhookData[webhookKey];
if (webhookData.workflowData.id.toString() !== workflowId) { if (webhookData.workflowData.id !== workflowId) {
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
continue; continue;
} }

View file

@ -42,7 +42,7 @@ export class PermissionChecker {
if (workflow.id && isSharingEnabled()) { if (workflow.id && isSharingEnabled()) {
const workflowSharings = await Db.collections.SharedWorkflow.find({ const workflowSharings = await Db.collections.SharedWorkflow.find({
relations: ['workflow'], relations: ['workflow'],
where: { workflow: { id: Number(workflow.id) } }, where: { workflowId: workflow.id },
}); });
workflowUserIds = workflowSharings.map((s) => s.userId); workflowUserIds = workflowSharings.map((s) => s.userId);
} }
@ -58,7 +58,7 @@ export class PermissionChecker {
where: credentialsWhere, where: credentialsWhere,
}); });
const accessibleCredIds = credentialSharings.map((s) => s.credentialsId.toString()); const accessibleCredIds = credentialSharings.map((s) => s.credentialsId);
const inaccessibleCredIds = workflowCredIds.filter((id) => !accessibleCredIds.includes(id)); const inaccessibleCredIds = workflowCredIds.filter((id) => !accessibleCredIds.includes(id));

View file

@ -17,11 +17,11 @@ import { getLicense } from '@/License';
import { WhereClause } from '@/Interfaces'; import { WhereClause } from '@/Interfaces';
import { RoleService } from '@/role/role.service'; import { RoleService } from '@/role/role.service';
export async function getWorkflowOwner(workflowId: string | number): Promise<User> { export async function getWorkflowOwner(workflowId: string): Promise<User> {
const workflowOwnerRole = await RoleService.get({ name: 'owner', scope: 'workflow' }); const workflowOwnerRole = await RoleService.get({ name: 'owner', scope: 'workflow' });
const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({ const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({
where: { workflow: { id: workflowId }, role: workflowOwnerRole }, where: { workflowId, role: workflowOwnerRole },
relations: ['user', 'user.globalRole'], relations: ['user', 'user.globalRole'],
}); });

View file

@ -426,13 +426,13 @@ export function usersNamespace(this: N8nApp): void {
await Db.transaction(async (transactionManager) => { await Db.transaction(async (transactionManager) => {
// Get all workflow ids belonging to user to delete // Get all workflow ids belonging to user to delete
const sharedWorkflows = await transactionManager.getRepository(SharedWorkflow).find({ const sharedWorkflowIds = await transactionManager
where: { user: userToDelete, role: workflowOwnerRole }, .getRepository(SharedWorkflow)
}); .find({
select: ['workflowId'],
const sharedWorkflowIds = sharedWorkflows.map((sharedWorkflow) => where: { userId: userToDelete.id, role: workflowOwnerRole },
sharedWorkflow.workflowId.toString(), })
); .then((sharedWorkflows) => sharedWorkflows.map(({ workflowId }) => workflowId));
// Prevents issues with unique key constraints since user being assigned // Prevents issues with unique key constraints since user being assigned
// workflows and credentials might be a sharee // workflows and credentials might be a sharee
@ -451,21 +451,21 @@ export function usersNamespace(this: N8nApp): void {
// Now do the same for creds // Now do the same for creds
// Get all workflow ids belonging to user to delete // Get all workflow ids belonging to user to delete
const sharedCredentials = await transactionManager.getRepository(SharedCredentials).find({ const sharedCredentialIds = await transactionManager
where: { user: userToDelete, role: credentialOwnerRole }, .getRepository(SharedCredentials)
}); .find({
select: ['credentialsId'],
const sharedCredentialIds = sharedCredentials.map((sharedCredential) => where: { user: userToDelete, role: credentialOwnerRole },
sharedCredential.credentialsId.toString(), })
); .then((sharedCredentials) =>
sharedCredentials.map(({ credentialsId }) => credentialsId),
);
// Prevents issues with unique key constraints since user being assigned // Prevents issues with unique key constraints since user being assigned
// workflows and credentials might be a sharee // workflows and credentials might be a sharee
await transactionManager.delete(SharedCredentials, { await transactionManager.delete(SharedCredentials, {
user: transferee, user: transferee,
credentials: In( credentialsId: In(sharedCredentialIds),
sharedCredentialIds.map((sharedCredentialId) => ({ id: sharedCredentialId })),
),
}); });
// Transfer ownership of owned credentials // Transfer ownership of owned credentials
@ -500,7 +500,7 @@ export function usersNamespace(this: N8nApp): void {
ownedSharedWorkflows.map(async ({ workflow }) => { ownedSharedWorkflows.map(async ({ workflow }) => {
if (workflow.active) { if (workflow.active) {
// deactivate before deleting // deactivate before deleting
await this.activeWorkflowRunner.remove(workflow.id.toString()); await this.activeWorkflowRunner.remove(workflow.id);
} }
return workflow; return workflow;
}), }),

View file

@ -77,7 +77,7 @@ export class WaitTrackerClass {
return; return;
} }
const executionIds = executions.map((execution) => execution.id.toString()).join(', '); const executionIds = executions.map((execution) => execution.id).join(', ');
Logger.debug( Logger.debug(
`Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`, `Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`,
); );
@ -85,7 +85,7 @@ export class WaitTrackerClass {
// Add timers for each waiting execution that they get started at the correct time // Add timers for each waiting execution that they get started at the correct time
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const execution of executions) { for (const execution of executions) {
const executionId = execution.id.toString(); const executionId = execution.id;
if (this.waitingExecutions[executionId] === undefined) { if (this.waitingExecutions[executionId] === undefined) {
const triggerTime = execution.waitTill!.getTime() - new Date().getTime(); const triggerTime = execution.waitTill!.getTime() - new Date().getTime();
this.waitingExecutions[executionId] = { this.waitingExecutions[executionId] = {
@ -161,7 +161,7 @@ export class WaitTrackerClass {
if (!fullExecutionData.workflowData.id) { if (!fullExecutionData.workflowData.id) {
throw new Error('Only saved workflows can be resumed.'); throw new Error('Only saved workflows can be resumed.');
} }
const user = await getWorkflowOwner(fullExecutionData.workflowData.id.toString()); const user = await getWorkflowOwner(fullExecutionData.workflowData.id);
const data: IWorkflowExecutionDataProcess = { const data: IWorkflowExecutionDataProcess = {
executionMode: fullExecutionData.mode, executionMode: fullExecutionData.mode,

View file

@ -167,7 +167,7 @@ export async function executeWebhook(
user = (workflowData as WorkflowEntity).shared[0].user; user = (workflowData as WorkflowEntity).shared[0].user;
} else { } else {
try { try {
user = await getWorkflowOwner(workflowData.id.toString()); user = await getWorkflowOwner(workflowData.id);
} catch (error) { } catch (error) {
throw new ResponseHelper.NotFoundError('Cannot find workflow'); throw new ResponseHelper.NotFoundError('Cannot find workflow');
} }

View file

@ -27,13 +27,13 @@ import {
IRun, IRun,
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
IWorkflowBase,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
IWorkflowExecuteHooks, IWorkflowExecuteHooks,
IWorkflowHooksOptionalParameters, IWorkflowHooksOptionalParameters,
IWorkflowSettings, IWorkflowSettings,
ErrorReporterProxy as ErrorReporter, ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger, LoggerProxy as Logger,
SubworkflowOperationError,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
WorkflowHooks, WorkflowHooks,
@ -51,7 +51,6 @@ import {
IExecutionFlattedDb, IExecutionFlattedDb,
IExecutionResponse, IExecutionResponse,
IPushDataExecutionFinished, IPushDataExecutionFinished,
IWorkflowBase,
IWorkflowExecuteProcess, IWorkflowExecuteProcess,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
IWorkflowErrorData, IWorkflowErrorData,
@ -62,7 +61,7 @@ import * as Push from '@/Push';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { getUserById, getWorkflowOwner, whereClause } from '@/UserManagement/UserManagementHelper'; import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { findSubworkflowStart } from '@/utils'; import { findSubworkflowStart } from '@/utils';
import { PermissionChecker } from './UserManagement/PermissionChecker'; import { PermissionChecker } from './UserManagement/PermissionChecker';
import { WorkflowsService } from './workflows/workflows.services'; import { WorkflowsService } from './workflows/workflows.services';
@ -94,6 +93,7 @@ export function executeErrorWorkflow(
if (fullRunData.data.resultData.error !== undefined) { if (fullRunData.data.resultData.error !== undefined) {
let workflowErrorData: IWorkflowErrorData; let workflowErrorData: IWorkflowErrorData;
const workflowId = workflowData.id;
if (executionId) { if (executionId) {
// The error did happen in an execution // The error did happen in an execution
@ -107,7 +107,7 @@ export function executeErrorWorkflow(
retryOf, retryOf,
}, },
workflow: { workflow: {
id: workflowData.id !== undefined ? workflowData.id.toString() : undefined, id: workflowId,
name: workflowData.name, name: workflowData.name,
}, },
}; };
@ -119,7 +119,7 @@ export function executeErrorWorkflow(
mode, mode,
}, },
workflow: { workflow: {
id: workflowData.id !== undefined ? workflowData.id.toString() : undefined, id: workflowId,
name: workflowData.name, name: workflowData.name,
}, },
}; };
@ -128,30 +128,28 @@ export function executeErrorWorkflow(
// Run the error workflow // Run the error workflow
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow. // To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
if ( if (
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain workflowData.settings?.errorWorkflow &&
workflowData.settings !== undefined &&
workflowData.settings.errorWorkflow &&
!( !(
mode === 'error' && mode === 'error' &&
workflowData.id && workflowId &&
workflowData.settings.errorWorkflow.toString() === workflowData.id.toString() workflowData.settings.errorWorkflow.toString() === workflowId
) )
) { ) {
Logger.verbose('Start external error workflow', { Logger.verbose('Start external error workflow', {
executionId, executionId,
errorWorkflowId: workflowData.settings.errorWorkflow.toString(), errorWorkflowId: workflowData.settings.errorWorkflow.toString(),
workflowId: workflowData.id, workflowId,
}); });
// If a specific error workflow is set run only that one // If a specific error workflow is set run only that one
// First, do permission checks. // First, do permission checks.
if (!workflowData.id) { if (!workflowId) {
// Manual executions do not trigger error workflows // Manual executions do not trigger error workflows
// So this if should never happen. It was added to // So this if should never happen. It was added to
// make sure there are no possible security gaps // make sure there are no possible security gaps
return; return;
} }
getWorkflowOwner(workflowData.id) getWorkflowOwner(workflowId)
.then((user) => { .then((user) => {
void WorkflowHelpers.executeErrorWorkflow( void WorkflowHelpers.executeErrorWorkflow(
workflowData.settings!.errorWorkflow as string, workflowData.settings!.errorWorkflow as string,
@ -166,7 +164,7 @@ export function executeErrorWorkflow(
{ {
executionId, executionId,
errorWorkflowId: workflowData.settings!.errorWorkflow!.toString(), errorWorkflowId: workflowData.settings!.errorWorkflow!.toString(),
workflowId: workflowData.id, workflowId,
error, error,
workflowErrorData, workflowErrorData,
}, },
@ -174,16 +172,12 @@ export function executeErrorWorkflow(
}); });
} else if ( } else if (
mode !== 'error' && mode !== 'error' &&
workflowData.id !== undefined && workflowId !== undefined &&
workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE) workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)
) { ) {
Logger.verbose('Start internal error workflow', { executionId, workflowId: workflowData.id }); Logger.verbose('Start internal error workflow', { executionId, workflowId });
void getWorkflowOwner(workflowData.id).then((user) => { void getWorkflowOwner(workflowId).then((user) => {
void WorkflowHelpers.executeErrorWorkflow( void WorkflowHelpers.executeErrorWorkflow(workflowId, workflowErrorData, user);
workflowData.id!.toString(),
workflowErrorData,
user,
);
}); });
} }
} }
@ -219,7 +213,7 @@ async function pruneExecutionData(this: WorkflowHooks): Promise<void> {
}, timeout * 1000); }, timeout * 1000);
// Mark binary data for deletion for all executions // Mark binary data for deletion for all executions
await BinaryDataManager.getInstance().markDataForDeletionByExecutionIds( await BinaryDataManager.getInstance().markDataForDeletionByExecutionIds(
executions.map(({ id }) => id.toString()), executions.map(({ id }) => id),
); );
} catch (error) { } catch (error) {
ErrorReporter.error(error); ErrorReporter.error(error);
@ -505,7 +499,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
try { try {
if ( if (
!isManualMode && !isManualMode &&
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id) &&
newStaticData newStaticData
) { ) {
// Workflow is saved so update in database // Workflow is saved so update in database
@ -593,17 +587,15 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
fullExecutionData.retryOf = this.retryOf.toString(); fullExecutionData.retryOf = this.retryOf.toString();
} }
if ( const workflowId = this.workflowData.id;
this.workflowData.id !== undefined && if (WorkflowHelpers.isWorkflowIdValid(workflowId)) {
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) fullExecutionData.workflowId = workflowId;
) {
fullExecutionData.workflowId = this.workflowData.id.toString();
} }
// Leave log message before flatten as that operation increased memory usage a lot and the chance of a crash is highest here // Leave log message before flatten as that operation increased memory usage a lot and the chance of a crash is highest here
Logger.debug(`Save execution data to database for execution ID ${this.executionId}`, { Logger.debug(`Save execution data to database for execution ID ${this.executionId}`, {
executionId: this.executionId, executionId: this.executionId,
workflowId: this.workflowData.id, workflowId,
finished: fullExecutionData.finished, finished: fullExecutionData.finished,
stoppedAt: fullExecutionData.stoppedAt, stoppedAt: fullExecutionData.stoppedAt,
}); });
@ -685,7 +677,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
newStaticData: IDataObject, newStaticData: IDataObject,
): Promise<void> { ): Promise<void> {
try { try {
if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) && newStaticData) { if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id) && newStaticData) {
// Workflow is saved so update in database // Workflow is saved so update in database
try { try {
await WorkflowHelpers.saveStaticDataById( await WorkflowHelpers.saveStaticDataById(
@ -726,11 +718,9 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
fullExecutionData.retryOf = this.retryOf.toString(); fullExecutionData.retryOf = this.retryOf.toString();
} }
if ( const workflowId = this.workflowData.id;
this.workflowData.id !== undefined && if (WorkflowHelpers.isWorkflowIdValid(workflowId)) {
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) fullExecutionData.workflowId = workflowId;
) {
fullExecutionData.workflowId = this.workflowData.id.toString();
} }
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
@ -844,12 +834,7 @@ export async function getWorkflowData(
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags']; const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
workflowData = await WorkflowsService.get( workflowData = await WorkflowsService.get({ id: workflowInfo.id }, { relations });
{ id: parseInt(workflowInfo.id, 10) },
{
relations,
},
);
if (workflowData === undefined) { if (workflowData === undefined) {
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`);
@ -1004,7 +989,7 @@ async function executeWorkflow(
workflowData, workflowData,
}; };
if (workflowData.id) { if (workflowData.id) {
fullExecutionData.workflowId = workflowData.id as string; fullExecutionData.workflowId = workflowData.id;
} }
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);

View file

@ -71,16 +71,8 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
* *
* @param {(string | null | undefined)} id The id to check * @param {(string | null | undefined)} id The id to check
*/ */
export function isWorkflowIdValid(id: string | null | undefined | number): boolean { export function isWorkflowIdValid(id: string | null | undefined): boolean {
if (typeof id === 'string') { return !(typeof id === 'string' && isNaN(parseInt(id, 10)));
id = parseInt(id, 10);
}
// eslint-disable-next-line no-restricted-globals
if (isNaN(id as number)) {
return false;
}
return true;
} }
/** /**
@ -97,7 +89,7 @@ export async function executeErrorWorkflow(
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here // Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
try { try {
let workflowData; let workflowData;
if (workflowId.toString() !== workflowErrorData.workflow.id?.toString()) { if (workflowId !== workflowErrorData.workflow.id) {
// To make this code easier to understand, we split it in 2 parts: // To make this code easier to understand, we split it in 2 parts:
// 1) Fetch the owner of the errored workflows and then // 1) Fetch the owner of the errored workflows and then
// 2) if now instance owner, then check if the user has access to the // 2) if now instance owner, then check if the user has access to the
@ -106,13 +98,10 @@ export async function executeErrorWorkflow(
const user = await getWorkflowOwner(workflowErrorData.workflow.id!); const user = await getWorkflowOwner(workflowErrorData.workflow.id!);
if (user.globalRole.name === 'owner') { if (user.globalRole.name === 'owner') {
workflowData = await Db.collections.Workflow.findOne({ id: Number(workflowId) }); workflowData = await Db.collections.Workflow.findOne({ id: workflowId });
} else { } else {
const sharedWorkflowData = await Db.collections.SharedWorkflow.findOne({ const sharedWorkflowData = await Db.collections.SharedWorkflow.findOne({
where: { where: { workflowId, userId: user.id },
workflow: { id: workflowId },
user,
},
relations: ['workflow'], relations: ['workflow'],
}); });
if (sharedWorkflowData) { if (sharedWorkflowData) {
@ -120,7 +109,7 @@ export async function executeErrorWorkflow(
} }
} }
} else { } else {
workflowData = await Db.collections.Workflow.findOne({ id: Number(workflowId) }); workflowData = await Db.collections.Workflow.findOne({ id: workflowId });
} }
if (workflowData === undefined) { if (workflowData === undefined) {
@ -250,11 +239,11 @@ export async function saveStaticData(workflow: Workflow): Promise<void> {
/** /**
* Saves the given static data on workflow * Saves the given static data on workflow
* *
* @param {(string | number)} workflowId The id of the workflow to save data on * @param {(string)} workflowId The id of the workflow to save data on
* @param {IDataObject} newStaticData The static data to save * @param {IDataObject} newStaticData The static data to save
*/ */
export async function saveStaticDataById( export async function saveStaticDataById(
workflowId: string | number, workflowId: string,
newStaticData: IDataObject, newStaticData: IDataObject,
): Promise<void> { ): Promise<void> {
await Db.collections.Workflow.update(workflowId, { await Db.collections.Workflow.update(workflowId, {
@ -265,10 +254,10 @@ export async function saveStaticDataById(
/** /**
* Returns the static data of workflow * Returns the static data of workflow
* *
* @param {(string | number)} workflowId The id of the workflow to get static data of * @param {(string)} workflowId The id of the workflow to get static data of
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function getStaticDataById(workflowId: string | number) { export async function getStaticDataById(workflowId: string) {
const workflowData = await Db.collections.Workflow.findOne(workflowId, { const workflowData = await Db.collections.Workflow.findOne(workflowId, {
select: ['staticData'], select: ['staticData'],
}); });
@ -329,7 +318,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
// if credential name-type combination is unique, use it // if credential name-type combination is unique, use it
if (credentials?.length === 1) { if (credentials?.length === 1) {
credentialsByName[nodeCredentialType][name] = { credentialsByName[nodeCredentialType][name] = {
id: credentials[0].id.toString(), id: credentials[0].id,
name: credentials[0].name, name: credentials[0].name,
}; };
node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name]; node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name];
@ -364,7 +353,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
}); });
if (credentials) { if (credentials) {
credentialsById[nodeCredentialType][nodeCredentials.id] = { credentialsById[nodeCredentialType][nodeCredentials.id] = {
id: credentials.id.toString(), id: credentials.id,
name: credentials.name, name: credentials.name,
}; };
node.credentials[nodeCredentialType] = node.credentials[nodeCredentialType] =
@ -380,7 +369,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
if (credsByName?.length === 1) { if (credsByName?.length === 1) {
// add found credential to cache // add found credential to cache
credentialsById[nodeCredentialType][credsByName[0].id] = { credentialsById[nodeCredentialType][credsByName[0].id] = {
id: credsByName[0].id.toString(), id: credsByName[0].id,
name: credsByName[0].name, name: credsByName[0].name,
}; };
node.credentials[nodeCredentialType] = node.credentials[nodeCredentialType] =
@ -410,9 +399,10 @@ export async function getSharedWorkflowIds(user: User, roles?: string[]): Promis
const sharedWorkflows = await Db.collections.SharedWorkflow.find({ const sharedWorkflows = await Db.collections.SharedWorkflow.find({
relations: ['workflow', 'role'], relations: ['workflow', 'role'],
where: whereClause({ user, entityType: 'workflow', roles }), where: whereClause({ user, entityType: 'workflow', roles }),
select: ['workflowId'],
}); });
return sharedWorkflows.map(({ workflowId }) => workflowId.toString()); return sharedWorkflows.map(({ workflowId }) => workflowId);
} }
/** /**
@ -428,9 +418,12 @@ export async function isBelowOnboardingThreshold(user: User): Promise<boolean> {
scope: 'workflow', scope: 'workflow',
}); });
const ownedWorkflowsIds = await Db.collections.SharedWorkflow.find({ const ownedWorkflowsIds = await Db.collections.SharedWorkflow.find({
user, where: {
role: workflowOwnerRole, user,
}).then((ownedWorkflows) => ownedWorkflows.map((wf) => wf.workflowId)); role: workflowOwnerRole,
},
select: ['workflowId'],
}).then((ownedWorkflows) => ownedWorkflows.map(({ workflowId }) => workflowId));
if (ownedWorkflowsIds.length > 15) { if (ownedWorkflowsIds.length > 15) {
belowThreshold = false; belowThreshold = false;
@ -538,7 +531,7 @@ export function validateWorkflowCredentialUsage(
* - It's a new node which indicates tampering and therefore must fail saving * - It's a new node which indicates tampering and therefore must fail saving
*/ */
const allowedCredentialIds = credentialsUserHasAccessTo.map((cred) => cred.id.toString()); const allowedCredentialIds = credentialsUserHasAccessTo.map((cred) => cred.id);
const nodesWithCredentialsUserDoesNotHaveAccessTo = getNodesWithInaccessibleCreds( const nodesWithCredentialsUserDoesNotHaveAccessTo = getNodesWithInaccessibleCreds(
newWorkflowVersion, newWorkflowVersion,

View file

@ -196,10 +196,9 @@ export class WorkflowRunner {
restartExecutionId?: string, restartExecutionId?: string,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>, responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): Promise<string> { ): Promise<string> {
if (loadStaticData === true && data.workflowData.id) { const workflowId = data.workflowData.id;
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById( if (loadStaticData === true && workflowId) {
data.workflowData.id as string, data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(workflowId);
);
} }
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
@ -218,7 +217,7 @@ export class WorkflowRunner {
} }
const workflow = new Workflow({ const workflow = new Workflow({
id: data.workflowData.id as string | undefined, id: workflowId,
name: data.workflowData.name, name: data.workflowData.name,
nodes: data.workflowData.nodes, nodes: data.workflowData.nodes,
connections: data.workflowData.connections, connections: data.workflowData.connections,
@ -596,13 +595,12 @@ export class WorkflowRunner {
restartExecutionId?: string, restartExecutionId?: string,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>, responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): Promise<string> { ): Promise<string> {
const workflowId = data.workflowData.id;
let startedAt = new Date(); let startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js')); const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
if (loadStaticData === true && data.workflowData.id) { if (loadStaticData === true && workflowId) {
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById( data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(workflowId);
data.workflowData.id as string,
);
} }
// Register the active execution // Register the active execution

View file

@ -135,7 +135,7 @@ class WorkflowRunnerProcess {
} }
this.workflow = new Workflow({ this.workflow = new Workflow({
id: this.data.workflowData.id as string | undefined, id: this.data.workflowData.id,
name: this.data.workflowData.name, name: this.data.workflowData.name,
nodes: this.data.workflowData.nodes, nodes: this.data.workflowData.nodes,
connections: this.data.workflowData.connections, connections: this.data.workflowData.connections,

View file

@ -63,7 +63,7 @@ tagsController.post(
// Updates a tag // Updates a tag
tagsController.patch( tagsController.patch(
'/:id', '/:id(\\d+)',
workflowsEnabledMiddleware, workflowsEnabledMiddleware,
ResponseHelper.send(async (req: express.Request): Promise<TagEntity | void> => { ResponseHelper.send(async (req: express.Request): Promise<TagEntity | void> => {
const { name } = req.body; const { name } = req.body;
@ -86,7 +86,7 @@ tagsController.patch(
); );
tagsController.delete( tagsController.delete(
'/:id', '/:id(\\d+)',
workflowsEnabledMiddleware, workflowsEnabledMiddleware,
ResponseHelper.send(async (req: TagsRequest.Delete): Promise<boolean> => { ResponseHelper.send(async (req: TagsRequest.Delete): Promise<boolean> => {
if ( if (
@ -98,7 +98,7 @@ tagsController.delete(
'Only owners can remove tags', 'Only owners can remove tags',
); );
} }
const id = Number(req.params.id); const id = req.params.id;
await externalHooks.run('tag.beforeDelete', [id]); await externalHooks.run('tag.beforeDelete', [id]);

View file

@ -13,7 +13,7 @@ interface IResult {
executions: IExecutionResult[]; executions: IExecutionResult[];
} }
interface IExecutionResult { interface IExecutionResult {
workflowId: string | number; workflowId: string;
workflowName: string; workflowName: string;
executionTime: number; // Given in seconds with decimals for milliseconds executionTime: number; // Given in seconds with decimals for milliseconds
finished: boolean; finished: boolean;
@ -26,12 +26,12 @@ interface IExecutionResult {
} }
interface IExecutionError { interface IExecutionError {
workflowId: string | number; workflowId: string;
error: string; error: string;
} }
interface IWorkflowExecutionProgress { interface IWorkflowExecutionProgress {
workflowId: string | number; workflowId: string;
status: ExecutionStatus; status: ExecutionStatus;
} }

View file

@ -4,7 +4,7 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
import { BinaryDataManager, UserSettings, PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core'; import { BinaryDataManager, UserSettings, PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core';
import { LoggerProxy } from 'n8n-workflow'; import { LoggerProxy, IWorkflowBase } from 'n8n-workflow';
import * as ActiveExecutions from '@/ActiveExecutions'; import * as ActiveExecutions from '@/ActiveExecutions';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
@ -17,7 +17,7 @@ import { NodeTypes } from '@/NodeTypes';
import { InternalHooksManager } from '@/InternalHooksManager'; import { InternalHooksManager } from '@/InternalHooksManager';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import { IWorkflowBase, IWorkflowExecutionDataProcess } from '@/Interfaces'; import { IWorkflowExecutionDataProcess } from '@/Interfaces';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import config from '@/config'; import config from '@/config';
import { getInstanceOwner } from '@/UserManagement/UserManagementHelper'; import { getInstanceOwner } from '@/UserManagement/UserManagementHelper';
@ -96,8 +96,7 @@ export class Execute extends Command {
return; return;
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment workflowId = workflowData.id ?? PLACEHOLDER_EMPTY_WORKFLOW_ID;
workflowId = workflowData.id ? workflowData.id.toString() : PLACEHOLDER_EMPTY_WORKFLOW_ID;
} }
// Wait till the database is ready // Wait till the database is ready

View file

@ -36,6 +36,8 @@ import { User } from '@db/entities/User';
import { getInstanceOwner } from '@/UserManagement/UserManagementHelper'; import { getInstanceOwner } from '@/UserManagement/UserManagementHelper';
import { findCliWorkflowStart } from '@/utils'; import { findCliWorkflowStart } from '@/utils';
const re = /\d+/;
export class ExecuteBatch extends Command { export class ExecuteBatch extends Command {
static description = '\nExecutes multiple workflows once'; static description = '\nExecutes multiple workflows once';
@ -199,8 +201,8 @@ export class ExecuteBatch extends Command {
ExecuteBatch.debug = flags.debug; ExecuteBatch.debug = flags.debug;
ExecuteBatch.concurrency = flags.concurrency || 1; ExecuteBatch.concurrency = flags.concurrency || 1;
const ids: number[] = []; const ids: string[] = [];
const skipIds: number[] = []; const skipIds: string[] = [];
if (flags.snapshot !== undefined) { if (flags.snapshot !== undefined) {
if (fs.existsSync(flags.snapshot)) { if (fs.existsSync(flags.snapshot)) {
@ -241,13 +243,10 @@ export class ExecuteBatch extends Command {
if (flags.ids !== undefined) { if (flags.ids !== undefined) {
if (fs.existsSync(flags.ids)) { if (fs.existsSync(flags.ids)) {
const contents = fs.readFileSync(flags.ids, { encoding: 'utf-8' }); const contents = fs.readFileSync(flags.ids, { encoding: 'utf-8' });
ids.push(...contents.split(',').map((id) => parseInt(id.trim(), 10))); ids.push(...contents.split(',').filter((id) => re.exec(id)));
} else { } else {
const paramIds = flags.ids.split(','); const paramIds = flags.ids.split(',');
const re = /\d+/; const matchedIds = paramIds.filter((id) => re.exec(id));
const matchedIds = paramIds
.filter((id) => re.exec(id))
.map((id) => parseInt(id.trim(), 10));
if (matchedIds.length === 0) { if (matchedIds.length === 0) {
console.log( console.log(
@ -263,7 +262,7 @@ export class ExecuteBatch extends Command {
if (flags.skipList !== undefined) { if (flags.skipList !== undefined) {
if (fs.existsSync(flags.skipList)) { if (fs.existsSync(flags.skipList)) {
const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' }); const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' });
skipIds.push(...contents.split(',').map((id) => parseInt(id.trim(), 10))); skipIds.push(...contents.split(',').filter((id) => re.exec(id)));
} else { } else {
console.log('Skip list file not found. Exiting.'); console.log('Skip list file not found. Exiting.');
return; return;

View file

@ -128,7 +128,7 @@ export class ExportCredentialsCommand extends Command {
for (let i = 0; i < credentials.length; i++) { for (let i = 0; i < credentials.length; i++) {
const { name, type, nodesAccess, data } = credentials[i]; const { name, type, nodesAccess, data } = credentials[i];
const id = credentials[i].id as string; const id = credentials[i].id;
const credential = new Credentials({ id, name }, type, nodesAccess, data); const credential = new Credentials({ id, name }, type, nodesAccess, data);
const plainData = credential.getData(encryptionKey); const plainData = credential.getData(encryptionKey);
(credentials[i] as ICredentialsDecryptedDb).data = plainData; (credentials[i] as ICredentialsDecryptedDb).data = plainData;

View file

@ -263,7 +263,7 @@ export class ImportWorkflowsCommand extends Command {
); );
if (matchingCredentials.length === 1) { if (matchingCredentials.length === 1) {
nodeCredentials.id = matchingCredentials[0].id.toString(); nodeCredentials.id = matchingCredentials[0].id;
} }
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign

View file

@ -35,13 +35,9 @@ EECredentialsController.get(
}); });
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
return allCredentials return allCredentials.map((credential: CredentialsEntity & CredentialWithSharings) =>
.map((credential: CredentialsEntity & CredentialWithSharings) => EECredentials.addOwnerAndSharings(credential),
EECredentials.addOwnerAndSharings(credential), );
)
.map(
(credential): CredentialWithSharings => ({ ...credential, id: credential.id.toString() }),
);
} catch (error) { } catch (error) {
LoggerProxy.error('Request to list credentials failed', error as Error); LoggerProxy.error('Request to list credentials failed', error as Error);
throw error; throw error;
@ -53,16 +49,12 @@ EECredentialsController.get(
* GET /credentials/:id * GET /credentials/:id
*/ */
EECredentialsController.get( EECredentialsController.get(
'/:id', '/:id(\\d+)',
(req, res, next) => (req.params.id === 'new' ? next('router') : next()), // skip ee router and use free one for naming (req, res, next) => (req.params.id === 'new' ? next('router') : next()), // skip ee router and use free one for naming
ResponseHelper.send(async (req: CredentialRequest.Get) => { ResponseHelper.send(async (req: CredentialRequest.Get) => {
const { id: credentialId } = req.params; const { id: credentialId } = req.params;
const includeDecryptedData = req.query.includeData === 'true'; const includeDecryptedData = req.query.includeData === 'true';
if (Number.isNaN(Number(credentialId))) {
throw new ResponseHelper.BadRequestError('Credential ID must be a number.');
}
let credential = (await EECredentials.get( let credential = (await EECredentials.get(
{ id: credentialId }, { id: credentialId },
{ relations: ['shared', 'shared.role', 'shared.user'] }, { relations: ['shared', 'shared.role', 'shared.user'] },
@ -82,19 +74,12 @@ EECredentialsController.get(
credential = EECredentials.addOwnerAndSharings(credential); credential = EECredentials.addOwnerAndSharings(credential);
// @ts-ignore @TODO_TECH_DEBT: Stringify `id` with entity field transformer
credential.id = credential.id.toString();
if (!includeDecryptedData || !userSharing || userSharing.role.name !== 'owner') { if (!includeDecryptedData || !userSharing || userSharing.role.name !== 'owner') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars const { data: _, ...rest } = credential;
const { id, data: _, ...rest } = credential; return { ...rest };
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer
return { id: id.toString(), ...rest };
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars const { data: _, ...rest } = credential;
const { id, data: _, ...rest } = credential;
const key = await EECredentials.getEncryptionKey(); const key = await EECredentials.getEncryptionKey();
const decryptedData = EECredentials.redact( const decryptedData = EECredentials.redact(
@ -102,8 +87,7 @@ EECredentialsController.get(
credential, credential,
); );
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer return { data: decryptedData, ...rest };
return { id: id.toString(), data: decryptedData, ...rest };
}), }),
); );
@ -119,9 +103,10 @@ EECredentialsController.post(
const encryptionKey = await EECredentials.getEncryptionKey(); const encryptionKey = await EECredentials.getEncryptionKey();
const { ownsCredential } = await EECredentials.isOwned(req.user, credentials.id.toString()); const credentialId = credentials.id;
const { ownsCredential } = await EECredentials.isOwned(req.user, credentialId);
const sharing = await EECredentials.getSharing(req.user, credentials.id); const sharing = await EECredentials.getSharing(req.user, credentialId);
if (!ownsCredential) { if (!ownsCredential) {
if (!sharing) { if (!sharing) {
throw new ResponseHelper.UnauthorizedError('Forbidden'); throw new ResponseHelper.UnauthorizedError('Forbidden');
@ -161,7 +146,6 @@ EECredentialsController.put(
} }
const { ownsCredential, credential } = await EECredentials.isOwned(req.user, credentialId); const { ownsCredential, credential } = await EECredentials.isOwned(req.user, credentialId);
if (!ownsCredential || !credential) { if (!ownsCredential || !credential) {
throw new ResponseHelper.UnauthorizedError('Forbidden'); throw new ResponseHelper.UnauthorizedError('Forbidden');
} }
@ -191,7 +175,7 @@ EECredentialsController.put(
void InternalHooksManager.getInstance().onUserSharedCredentials({ void InternalHooksManager.getInstance().onUserSharedCredentials({
credential_type: credential.type, credential_type: credential.type,
credential_id: credential.id.toString(), credential_id: credential.id,
user_id_sharer: req.user.id, user_id_sharer: req.user.id,
user_ids_sharees_added: newShareeIds, user_ids_sharees_added: newShareeIds,
sharees_removed: amountRemoved, sharees_removed: amountRemoved,

View file

@ -11,7 +11,7 @@ import { getLogger } from '@/Logger';
import { EECredentialsController } from './credentials.controller.ee'; import { EECredentialsController } from './credentials.controller.ee';
import { CredentialsService } from './credentials.service'; import { CredentialsService } from './credentials.service';
import type { ICredentialsResponse } from '@/Interfaces'; import type { ICredentialsDb } from '@/Interfaces';
import type { CredentialRequest } from '@/requests'; import type { CredentialRequest } from '@/requests';
export const credentialsController = express.Router(); export const credentialsController = express.Router();
@ -35,14 +35,8 @@ credentialsController.use('/', EECredentialsController);
*/ */
credentialsController.get( credentialsController.get(
'/', '/',
ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise<ICredentialsResponse[]> => { ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise<ICredentialsDb[]> => {
const credentials = await CredentialsService.getAll(req.user, { roles: ['owner'] }); return CredentialsService.getAll(req.user, { roles: ['owner'] });
return credentials.map((credential) => {
// eslint-disable-next-line no-param-reassign
credential.id = credential.id.toString();
return credential as ICredentialsResponse;
});
}), }),
); );
@ -69,15 +63,11 @@ credentialsController.get(
* GET /credentials/:id * GET /credentials/:id
*/ */
credentialsController.get( credentialsController.get(
'/:id', '/:id(\\d+)',
ResponseHelper.send(async (req: CredentialRequest.Get) => { ResponseHelper.send(async (req: CredentialRequest.Get) => {
const { id: credentialId } = req.params; const { id: credentialId } = req.params;
const includeDecryptedData = req.query.includeData === 'true'; const includeDecryptedData = req.query.includeData === 'true';
if (Number.isNaN(Number(credentialId))) {
throw new ResponseHelper.BadRequestError('Credential ID must be a number.');
}
const sharing = await CredentialsService.getSharing(req.user, credentialId, ['credentials']); const sharing = await CredentialsService.getSharing(req.user, credentialId, ['credentials']);
if (!sharing) { if (!sharing) {
@ -88,11 +78,10 @@ credentialsController.get(
const { credentials: credential } = sharing; const { credentials: credential } = sharing;
const { id, data: _, ...rest } = credential; const { data: _, ...rest } = credential;
if (!includeDecryptedData) { if (!includeDecryptedData) {
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer return { ...rest };
return { id: id.toString(), ...rest };
} }
const key = await CredentialsService.getEncryptionKey(); const key = await CredentialsService.getEncryptionKey();
@ -101,8 +90,7 @@ credentialsController.get(
credential, credential,
); );
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer return { data: decryptedData, ...rest };
return { id: id.toString(), data: decryptedData, ...rest };
}), }),
); );
@ -139,15 +127,15 @@ credentialsController.post(
const key = await CredentialsService.getEncryptionKey(); const key = await CredentialsService.getEncryptionKey();
const encryptedData = CredentialsService.createEncryptedData(key, null, newCredential); const encryptedData = CredentialsService.createEncryptedData(key, null, newCredential);
const { id, ...rest } = await CredentialsService.save(newCredential, encryptedData, req.user); const credential = await CredentialsService.save(newCredential, encryptedData, req.user);
void InternalHooksManager.getInstance().onUserCreatedCredentials({ void InternalHooksManager.getInstance().onUserCreatedCredentials({
credential_type: rest.type, credential_type: credential.type,
credential_id: id.toString(), credential_id: credential.id,
public_api: false, public_api: false,
}); });
return { id: id.toString(), ...rest }; return credential;
}), }),
); );
@ -155,8 +143,8 @@ credentialsController.post(
* PATCH /credentials/:id * PATCH /credentials/:id
*/ */
credentialsController.patch( credentialsController.patch(
'/:id', '/:id(\\d+)',
ResponseHelper.send(async (req: CredentialRequest.Update): Promise<ICredentialsResponse> => { ResponseHelper.send(async (req: CredentialRequest.Update): Promise<ICredentialsDb> => {
const { id: credentialId } = req.params; const { id: credentialId } = req.params;
const sharing = await CredentialsService.getSharing(req.user, credentialId); const sharing = await CredentialsService.getSharing(req.user, credentialId);
@ -194,14 +182,11 @@ credentialsController.patch(
} }
// Remove the encrypted data as it is not needed in the frontend // Remove the encrypted data as it is not needed in the frontend
const { id, data: _, ...rest } = responseData; const { data: _, ...rest } = responseData;
LoggerProxy.verbose('Credential updated', { credentialId }); LoggerProxy.verbose('Credential updated', { credentialId });
return { return { ...rest };
id: id.toString(),
...rest,
};
}), }),
); );
@ -209,7 +194,7 @@ credentialsController.patch(
* DELETE /credentials/:id * DELETE /credentials/:id
*/ */
credentialsController.delete( credentialsController.delete(
'/:id', '/:id(\\d+)',
ResponseHelper.send(async (req: CredentialRequest.Delete) => { ResponseHelper.send(async (req: CredentialRequest.Delete) => {
const { id: credentialId } = req.params; const { id: credentialId } = req.params;

View file

@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { DeleteResult, EntityManager, FindOneOptions, In, Not, ObjectLiteral } from 'typeorm'; import { DeleteResult, EntityManager, FindConditions, In, Not } from 'typeorm';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { RoleService } from '@/role/role.service'; import { RoleService } from '@/role/role.service';
import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity';
@ -30,35 +30,31 @@ export class EECredentialsService extends CredentialsService {
*/ */
static async getSharing( static async getSharing(
user: User, user: User,
credentialId: number | string, credentialId: string,
relations: string[] = ['credentials'], relations: string[] = ['credentials'],
{ allowGlobalOwner } = { allowGlobalOwner: true }, { allowGlobalOwner } = { allowGlobalOwner: true },
): Promise<SharedCredentials | undefined> { ): Promise<SharedCredentials | undefined> {
const options: FindOneOptions<SharedCredentials> & { where: ObjectLiteral } = { const where: FindConditions<SharedCredentials> = { credentialsId: credentialId };
where: {
credentials: { id: credentialId },
},
};
// Omit user from where if the requesting user is the global // Omit user from where if the requesting user is the global
// owner. This allows the global owner to view and delete // owner. This allows the global owner to view and delete
// credentials they don't own. // credentials they don't own.
if (!allowGlobalOwner || user.globalRole.name !== 'owner') { if (!allowGlobalOwner || user.globalRole.name !== 'owner') {
options.where.user = { id: user.id }; where.user = { id: user.id };
} }
if (relations?.length) { return Db.collections.SharedCredentials.findOne({
options.relations = relations; where,
} relations,
});
return Db.collections.SharedCredentials.findOne(options);
} }
static async getSharings( static async getSharings(
transaction: EntityManager, transaction: EntityManager,
credentialId: string, credentialId: string,
): Promise<SharedCredentials[]> { ): Promise<SharedCredentials[]> {
const credential = await transaction.findOne(CredentialsEntity, credentialId, { const credential = await transaction.findOne(CredentialsEntity, {
where: { id: credentialId },
relations: ['shared'], relations: ['shared'],
}); });
return credential?.shared ?? []; return credential?.shared ?? [];
@ -69,10 +65,11 @@ export class EECredentialsService extends CredentialsService {
credentialId: string, credentialId: string,
userIds: string[], userIds: string[],
): Promise<DeleteResult> { ): Promise<DeleteResult> {
return transaction.delete(SharedCredentials, { const conditions: FindConditions<SharedCredentials> = {
credentials: { id: credentialId }, credentialsId: credentialId,
user: { id: Not(In(userIds)) }, userId: Not(In(userIds)),
}); };
return transaction.delete(SharedCredentials, conditions);
} }
static async share( static async share(

View file

@ -10,7 +10,7 @@ import {
LoggerProxy, LoggerProxy,
NodeHelpers, NodeHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { FindManyOptions, FindOneOptions, In } from 'typeorm'; import { FindConditions, FindManyOptions, In } from 'typeorm';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
@ -59,22 +59,13 @@ export class CredentialsService {
} }
// if member, return credentials owned by or shared with member // if member, return credentials owned by or shared with member
const userSharings = await Db.collections.SharedCredentials.find({
const whereConditions: FindManyOptions = {
where: { where: {
user, userId: user.id,
...(options?.roles?.length ? { role: { name: In(options.roles) } } : {}),
}, },
}; relations: options?.roles?.length ? ['role'] : [],
});
if (options?.roles?.length) {
whereConditions.where = {
...whereConditions.where,
role: { name: In(options.roles) },
} as FindManyOptions;
whereConditions.relations = ['role'];
}
const userSharings = await Db.collections.SharedCredentials.find(whereConditions);
return Db.collections.Credentials.find({ return Db.collections.Credentials.find({
select: SELECT_FIELDS, select: SELECT_FIELDS,
@ -94,35 +85,26 @@ export class CredentialsService {
*/ */
static async getSharing( static async getSharing(
user: User, user: User,
credentialId: number | string, credentialId: string,
relations: string[] = ['credentials'], relations: string[] = ['credentials'],
{ allowGlobalOwner } = { allowGlobalOwner: true }, { allowGlobalOwner } = { allowGlobalOwner: true },
): Promise<SharedCredentials | undefined> { ): Promise<SharedCredentials | undefined> {
const options: FindOneOptions = { const where: FindConditions<SharedCredentials> = { credentialsId: credentialId };
where: {
credentials: { id: credentialId },
},
};
// Omit user from where if the requesting user is the global // Omit user from where if the requesting user is the global
// owner. This allows the global owner to view and delete // owner. This allows the global owner to view and delete
// credentials they don't own. // credentials they don't own.
if (!allowGlobalOwner || user.globalRole.name !== 'owner') { if (!allowGlobalOwner || user.globalRole.name !== 'owner') {
options.where = { Object.assign(where, {
...options.where, userId: user.id,
user: { id: user.id },
role: { name: 'owner' }, role: { name: 'owner' },
} as FindOneOptions; });
if (!relations.includes('role')) { if (!relations.includes('role')) {
relations.push('role'); relations.push('role');
} }
} }
if (relations?.length) { return Db.collections.SharedCredentials.findOne({ where, relations });
options.relations = relations;
}
return Db.collections.SharedCredentials.findOne(options);
} }
static async prepareCreateData( static async prepareCreateData(
@ -132,7 +114,7 @@ export class CredentialsService {
const { id, ...rest } = data; const { id, ...rest } = data;
// This saves us a merge but requires some type casting. These // This saves us a merge but requires some type casting. These
// types are compatiable for this case. // types are compatible for this case.
const newCredentials = Db.collections.Credentials.create( const newCredentials = Db.collections.Credentials.create(
rest as ICredentialsDb, rest as ICredentialsDb,
) as CredentialsEntity; ) as CredentialsEntity;
@ -182,11 +164,11 @@ export class CredentialsService {
static createEncryptedData( static createEncryptedData(
encryptionKey: string, encryptionKey: string,
credentialsId: string | null, credentialId: string | null,
data: CredentialsEntity, data: CredentialsEntity,
): ICredentialsDb { ): ICredentialsDb {
const credentials = new Credentials( const credentials = new Credentials(
{ id: credentialsId, name: data.name }, { id: credentialId, name: data.name },
data.type, data.type,
data.nodesAccess, data.nodesAccess,
); );

View file

@ -1,14 +1,16 @@
import type { ICredentialNodeAccess } from 'n8n-workflow'; import type { ICredentialNodeAccess } from 'n8n-workflow';
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Generated, Index, OneToMany, PrimaryColumn } from 'typeorm';
import { IsArray, IsObject, IsString, Length } from 'class-validator'; import { IsArray, IsObject, IsString, Length } from 'class-validator';
import type { SharedCredentials } from './SharedCredentials'; import type { SharedCredentials } from './SharedCredentials';
import { AbstractEntity, jsonColumnType } from './AbstractEntity'; import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import type { ICredentialsDb } from '@/Interfaces'; import type { ICredentialsDb } from '@/Interfaces';
import { idStringifier } from '../utils/transformers';
@Entity() @Entity()
export class CredentialsEntity extends AbstractEntity implements ICredentialsDb { export class CredentialsEntity extends AbstractEntity implements ICredentialsDb {
@PrimaryGeneratedColumn() @Generated()
id: number; @PrimaryColumn({ transformer: idStringifier })
id: string;
@Column({ length: 128 }) @Column({ length: 128 })
@IsString({ message: 'Credential `name` must be of type string.' }) @IsString({ message: 'Credential `name` must be of type string.' })

View file

@ -1,7 +1,8 @@
import type { WorkflowExecuteMode } from 'n8n-workflow'; import type { WorkflowExecuteMode } from 'n8n-workflow';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Generated, Index, PrimaryColumn } from 'typeorm';
import { datetimeColumnType, jsonColumnType } from './AbstractEntity'; import { datetimeColumnType, jsonColumnType } from './AbstractEntity';
import type { IExecutionFlattedDb, IWorkflowDb } from '@/Interfaces'; import type { IExecutionFlattedDb, IWorkflowDb } from '@/Interfaces';
import { idStringifier } from '../utils/transformers';
@Entity() @Entity()
@Index(['workflowId', 'id']) @Index(['workflowId', 'id'])
@ -10,8 +11,9 @@ import type { IExecutionFlattedDb, IWorkflowDb } from '@/Interfaces';
@Index(['workflowId', 'finished', 'id']) @Index(['workflowId', 'finished', 'id'])
@Index(['workflowId', 'waitTill', 'id']) @Index(['workflowId', 'waitTill', 'id'])
export class ExecutionEntity implements IExecutionFlattedDb { export class ExecutionEntity implements IExecutionFlattedDb {
@PrimaryGeneratedColumn() @Generated()
id: number; @PrimaryColumn({ transformer: idStringifier })
id: string;
@Column('text') @Column('text')
data: string; data: string;
@ -38,7 +40,7 @@ export class ExecutionEntity implements IExecutionFlattedDb {
@Column(jsonColumnType) @Column(jsonColumnType)
workflowData: IWorkflowDb; workflowData: IWorkflowDb;
@Column({ nullable: true }) @Column({ nullable: true, transformer: idStringifier })
workflowId: string; workflowId: string;
@Column({ type: datetimeColumnType, nullable: true }) @Column({ type: datetimeColumnType, nullable: true })

View file

@ -3,6 +3,7 @@ import type { CredentialsEntity } from './CredentialsEntity';
import type { User } from './User'; import type { User } from './User';
import type { Role } from './Role'; import type { Role } from './Role';
import { AbstractEntity } from './AbstractEntity'; import { AbstractEntity } from './AbstractEntity';
import { idStringifier } from '../utils/transformers';
@Entity() @Entity()
export class SharedCredentials extends AbstractEntity { export class SharedCredentials extends AbstractEntity {
@ -22,7 +23,7 @@ export class SharedCredentials extends AbstractEntity {
}) })
credentials: CredentialsEntity; credentials: CredentialsEntity;
@PrimaryColumn() @PrimaryColumn({ transformer: idStringifier })
@RelationId((sharedCredential: SharedCredentials) => sharedCredential.credentials) @RelationId((sharedCredential: SharedCredentials) => sharedCredential.credentials)
credentialsId: number; credentialsId: string;
} }

View file

@ -3,6 +3,7 @@ import type { WorkflowEntity } from './WorkflowEntity';
import type { User } from './User'; import type { User } from './User';
import type { Role } from './Role'; import type { Role } from './Role';
import { AbstractEntity } from './AbstractEntity'; import { AbstractEntity } from './AbstractEntity';
import { idStringifier } from '../utils/transformers';
@Entity() @Entity()
export class SharedWorkflow extends AbstractEntity { export class SharedWorkflow extends AbstractEntity {
@ -22,7 +23,7 @@ export class SharedWorkflow extends AbstractEntity {
}) })
workflow: WorkflowEntity; workflow: WorkflowEntity;
@PrimaryColumn() @PrimaryColumn({ transformer: idStringifier })
@RelationId((sharedWorkflow: SharedWorkflow) => sharedWorkflow.workflow) @RelationId((sharedWorkflow: SharedWorkflow) => sharedWorkflow.workflow)
workflowId: number; workflowId: string;
} }

View file

@ -1,18 +1,15 @@
import { Column, Entity, Generated, Index, ManyToMany, PrimaryColumn } from 'typeorm'; import { Column, Entity, Generated, Index, ManyToMany, PrimaryColumn } from 'typeorm';
import { IsString, Length } from 'class-validator'; import { IsString, Length } from 'class-validator';
import type { ITagDb } from '@/Interfaces';
import { idStringifier } from '../utils/transformers'; import { idStringifier } from '../utils/transformers';
import type { WorkflowEntity } from './WorkflowEntity'; import type { WorkflowEntity } from './WorkflowEntity';
import { AbstractEntity } from './AbstractEntity'; import { AbstractEntity } from './AbstractEntity';
@Entity() @Entity()
export class TagEntity extends AbstractEntity implements ITagDb { export class TagEntity extends AbstractEntity {
@Generated() @Generated()
@PrimaryColumn({ @PrimaryColumn({ transformer: idStringifier })
transformer: idStringifier, id: string;
})
id: number;
@Column({ length: 24 }) @Column({ length: 24 })
@Index({ unique: true }) @Index({ unique: true })

View file

@ -1,12 +1,12 @@
import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
import { IWebhookDb } from '@/Interfaces'; import { idStringifier } from '../utils/transformers';
@Entity() @Entity()
@Index(['webhookId', 'method', 'pathLength']) @Index(['webhookId', 'method', 'pathLength'])
export class WebhookEntity implements IWebhookDb { export class WebhookEntity {
@Column() @Column({ transformer: idStringifier })
workflowId: number; workflowId: string;
@PrimaryColumn() @PrimaryColumn()
webhookPath: string; webhookPath: string;
@ -18,8 +18,8 @@ export class WebhookEntity implements IWebhookDb {
node: string; node: string;
@Column({ nullable: true }) @Column({ nullable: true })
webhookId: string; webhookId?: string;
@Column({ nullable: true }) @Column({ nullable: true })
pathLength: number; pathLength?: number;
} }

View file

@ -12,26 +12,28 @@ import type {
import { import {
Column, Column,
Entity, Entity,
Generated,
Index, Index,
JoinColumn, JoinColumn,
JoinTable, JoinTable,
ManyToMany, ManyToMany,
OneToMany, OneToMany,
PrimaryGeneratedColumn, PrimaryColumn,
} from 'typeorm'; } from 'typeorm';
import config from '@/config'; import config from '@/config';
import type { TagEntity } from './TagEntity'; import type { TagEntity } from './TagEntity';
import type { SharedWorkflow } from './SharedWorkflow'; import type { SharedWorkflow } from './SharedWorkflow';
import type { WorkflowStatistics } from './WorkflowStatistics'; import type { WorkflowStatistics } from './WorkflowStatistics';
import { objectRetriever, sqlite } from '../utils/transformers'; import { idStringifier, objectRetriever, sqlite } from '../utils/transformers';
import { AbstractEntity, jsonColumnType } from './AbstractEntity'; import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import type { IWorkflowDb } from '@/Interfaces'; import type { IWorkflowDb } from '@/Interfaces';
@Entity() @Entity()
export class WorkflowEntity extends AbstractEntity implements IWorkflowDb { export class WorkflowEntity extends AbstractEntity implements IWorkflowDb {
@PrimaryGeneratedColumn() @Generated()
id: number; @PrimaryColumn({ transformer: idStringifier })
id: string;
// TODO: Add XSS check // TODO: Add XSS check
@Index({ unique: true }) @Index({ unique: true })

View file

@ -1,4 +1,5 @@
import { Column, Entity, RelationId, ManyToOne, PrimaryColumn } from 'typeorm'; import { Column, Entity, RelationId, ManyToOne, PrimaryColumn } from 'typeorm';
import { idStringifier } from '../utils/transformers';
import { datetimeColumnType } from './AbstractEntity'; import { datetimeColumnType } from './AbstractEntity';
import type { WorkflowEntity } from './WorkflowEntity'; import type { WorkflowEntity } from './WorkflowEntity';
@ -26,7 +27,7 @@ export class WorkflowStatistics {
}) })
workflow: WorkflowEntity; workflow: WorkflowEntity;
@PrimaryColumn({ transformer: idStringifier })
@RelationId((workflowStatistics: WorkflowStatistics) => workflowStatistics.workflow) @RelationId((workflowStatistics: WorkflowStatistics) => workflowStatistics.workflow)
@PrimaryColumn() workflowId: string;
workflowId: number;
} }

View file

@ -36,7 +36,7 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }
@ -79,7 +79,7 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }
@ -123,7 +123,7 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }

View file

@ -42,7 +42,7 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }
@ -85,7 +85,7 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }
@ -131,7 +131,7 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }

View file

@ -39,7 +39,7 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }
@ -82,7 +82,7 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }
@ -126,7 +126,7 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
// @ts-ignore // @ts-ignore
(credentials) => credentials.name === name && credentials.type === type, (credentials) => credentials.name === name && credentials.type === type,
); );
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name }; node.credentials[type] = { id: matchingCredentials?.id || null, name };
credentialsUpdated = true; credentialsUpdated = true;
} }
} }

View file

@ -1,10 +1,13 @@
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import { ValueTransformer } from 'typeorm'; import type { ValueTransformer, FindOperator } from 'typeorm';
import config from '@/config'; import config from '@/config';
export const idStringifier = { export const idStringifier = {
from: (value: number): string | number => (typeof value === 'number' ? value.toString() : value), from: (value?: number): string | undefined => value?.toString(),
to: (value: string): number | string => (typeof value === 'string' ? Number(value) : value), to: (
value: string | FindOperator<unknown> | undefined,
): number | FindOperator<unknown> | undefined =>
typeof value === 'string' ? Number(value) : value,
}; };
export const lowerCaser = { export const lowerCaser = {

View file

@ -1,4 +1,4 @@
import { INode, IRun, IWorkflowBase, LoggerProxy } from 'n8n-workflow'; import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager'; import { InternalHooksManager } from '@/InternalHooksManager';
import { StatisticsNames } from '@/databases/entities/WorkflowStatistics'; import { StatisticsNames } from '@/databases/entities/WorkflowStatistics';
@ -22,14 +22,8 @@ export async function workflowExecutionCompleted(
} }
// Get the workflow id // Get the workflow id
let workflowId: number; const workflowId = workflowData.id;
try { if (workflowId === undefined) return;
workflowId = parseInt(workflowData.id as string, 10);
if (isNaN(workflowId)) throw new Error('not a number');
} catch (error) {
LoggerProxy.error(`Error "${error as string}" when casting workflow ID to a number`);
return;
}
// Try insertion and if it fails due to key conflicts then update the existing entry instead // Try insertion and if it fails due to key conflicts then update the existing entry instead
try { try {
@ -62,19 +56,9 @@ export async function workflowExecutionCompleted(
} }
export async function nodeFetchedData(workflowId: string, node: INode): Promise<void> { export async function nodeFetchedData(workflowId: string, node: INode): Promise<void> {
// Get the workflow id
let id: number;
try {
id = parseInt(workflowId, 10);
if (isNaN(id)) throw new Error('not a number');
} catch (error) {
LoggerProxy.error(`Error ${error as string} when casting workflow ID to a number`);
return;
}
// Update only if necessary // Update only if necessary
const response = await Db.collections.Workflow.update( const response = await Db.collections.Workflow.update(
{ id, dataLoaded: false }, { id: workflowId, dataLoaded: false },
{ dataLoaded: true }, { dataLoaded: true },
); );
@ -85,7 +69,7 @@ export async function nodeFetchedData(workflowId: string, node: INode): Promise<
const owner = await getWorkflowOwner(workflowId); const owner = await getWorkflowOwner(workflowId);
let metrics = { let metrics = {
user_id: owner.id, user_id: owner.id,
workflow_id: id, workflow_id: workflowId,
node_type: node.type, node_type: node.type,
node_id: node.id, node_id: node.id,
}; };

View file

@ -36,7 +36,7 @@ EEExecutionsController.get(
* GET /executions/:id * GET /executions/:id
*/ */
EEExecutionsController.get( EEExecutionsController.get(
'/:id', '/:id(\\d+)',
ResponseHelper.send( ResponseHelper.send(
async ( async (
req: ExecutionRequest.Get, req: ExecutionRequest.Get,

View file

@ -41,7 +41,7 @@ executionsController.get(
* GET /executions/:id * GET /executions/:id
*/ */
executionsController.get( executionsController.get(
'/:id', '/:id(\\d+)',
ResponseHelper.send( ResponseHelper.send(
async ( async (
req: ExecutionRequest.Get, req: ExecutionRequest.Get,

View file

@ -3,7 +3,15 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { validate as jsonSchemaValidate } from 'jsonschema'; import { validate as jsonSchemaValidate } from 'jsonschema';
import { BinaryDataManager } from 'n8n-core'; import { BinaryDataManager } from 'n8n-core';
import { deepCopy, IDataObject, LoggerProxy, JsonObject, jsonParse, Workflow } from 'n8n-workflow'; import {
deepCopy,
IDataObject,
IWorkflowBase,
LoggerProxy,
JsonObject,
jsonParse,
Workflow,
} from 'n8n-workflow';
import { FindOperator, In, IsNull, LessThanOrEqual, Not, Raw } from 'typeorm'; import { FindOperator, In, IsNull, LessThanOrEqual, Not, Raw } from 'typeorm';
import * as ActiveExecutions from '@/ActiveExecutions'; import * as ActiveExecutions from '@/ActiveExecutions';
import config from '@/config'; import config from '@/config';
@ -12,7 +20,6 @@ import {
IExecutionFlattedResponse, IExecutionFlattedResponse,
IExecutionResponse, IExecutionResponse,
IExecutionsListResponse, IExecutionsListResponse,
IWorkflowBase,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
} from '@/Interfaces'; } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@ -31,7 +38,7 @@ interface IGetExecutionsQueryFilter {
mode?: string; mode?: string;
retryOf?: string; retryOf?: string;
retrySuccessId?: string; retrySuccessId?: string;
workflowId?: number | string; workflowId?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
waitTill?: FindOperator<any> | boolean; waitTill?: FindOperator<any> | boolean;
} }
@ -247,7 +254,7 @@ export class ExecutionsService {
const formattedExecutions = executions.map((execution) => { const formattedExecutions = executions.map((execution) => {
return { return {
id: execution.id.toString(), id: execution.id,
finished: execution.finished, finished: execution.finished,
mode: execution.mode, mode: execution.mode,
retryOf: execution.retryOf?.toString(), retryOf: execution.retryOf?.toString(),
@ -255,7 +262,7 @@ export class ExecutionsService {
waitTill: execution.waitTill as Date | undefined, waitTill: execution.waitTill as Date | undefined,
startedAt: execution.startedAt, startedAt: execution.startedAt,
stoppedAt: execution.stoppedAt, stoppedAt: execution.stoppedAt,
workflowId: execution.workflowData?.id?.toString() ?? '', workflowId: execution.workflowData?.id ?? '',
workflowName: execution.workflowData?.name, workflowName: execution.workflowData?.name,
}; };
}); });
@ -293,13 +300,8 @@ export class ExecutionsService {
return ResponseHelper.unflattenExecutionData(execution); return ResponseHelper.unflattenExecutionData(execution);
} }
const { id, ...rest } = execution;
// @ts-ignore // @ts-ignore
return { return execution;
id: id.toString(),
...rest,
};
} }
static async retryExecution(req: ExecutionRequest.Retry): Promise<boolean> { static async retryExecution(req: ExecutionRequest.Retry): Promise<boolean> {
@ -460,7 +462,7 @@ export class ExecutionsService {
}; };
let query = Db.collections.Execution.createQueryBuilder() let query = Db.collections.Execution.createQueryBuilder()
.select() .select('id')
.where({ .where({
...filters, ...filters,
workflowId: In(sharedWorkflowIds), workflowId: In(sharedWorkflowIds),
@ -474,7 +476,7 @@ export class ExecutionsService {
if (!executions.length) return; if (!executions.length) return;
const idsToDelete = executions.map(({ id }) => id.toString()); const idsToDelete = executions.map(({ id }) => id);
await Promise.all( await Promise.all(
idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)), idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)),
@ -503,7 +505,7 @@ export class ExecutionsService {
return; return;
} }
const idsToDelete = executions.map(({ id }) => id.toString()); const idsToDelete = executions.map(({ id }) => id);
await Promise.all( await Promise.all(
idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)), idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)),

View file

@ -13,10 +13,7 @@ export class RoleService {
static async getUserRoleForWorkflow(userId: string, workflowId: string) { static async getUserRoleForWorkflow(userId: string, workflowId: string) {
const shared = await Db.collections.SharedWorkflow.findOne({ const shared = await Db.collections.SharedWorkflow.findOne({
where: { where: { workflowId, userId },
workflow: { id: workflowId },
user: { id: userId },
},
relations: ['role'], relations: ['role'],
}); });
return shared?.role; return shared?.role;

View file

@ -92,7 +92,7 @@ EEWorkflowController.get(
relations.push('tags'); relations.push('tags');
} }
const workflow = await EEWorkflows.get({ id: parseInt(workflowId, 10) }, { relations }); const workflow = await EEWorkflows.get({ id: workflowId }, { relations });
if (!workflow) { if (!workflow) {
throw new ResponseHelper.NotFoundError(`Workflow with ID "${workflowId}" does not exist`); throw new ResponseHelper.NotFoundError(`Workflow with ID "${workflowId}" does not exist`);
@ -108,7 +108,7 @@ EEWorkflowController.get(
EEWorkflows.addOwnerAndSharings(workflow); EEWorkflows.addOwnerAndSharings(workflow);
await EEWorkflows.addCredentialsToWorkflow(workflow, req.user); await EEWorkflows.addCredentialsToWorkflow(workflow, req.user);
return EEWorkflows.entityToResponse(workflow); return workflow;
}), }),
); );
@ -189,7 +189,7 @@ EEWorkflowController.post(
await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]); await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false); void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
return EEWorkflows.entityToResponse(savedWorkflow); return savedWorkflow;
}), }),
); );
@ -205,7 +205,7 @@ EEWorkflowController.get(
return workflows.map((workflow) => { return workflows.map((workflow) => {
EEWorkflows.addOwnerAndSharings(workflow); EEWorkflows.addOwnerAndSharings(workflow);
workflow.nodes = []; workflow.nodes = [];
return EEWorkflows.entityToResponse(workflow); return workflow;
}); });
}), }),
); );
@ -230,7 +230,7 @@ EEWorkflowController.patch(
forceSave, forceSave,
); );
return EEWorkflows.entityToResponse(updatedWorkflow); return updatedWorkflow;
}), }),
); );
@ -244,11 +244,7 @@ EEWorkflowController.post(
Object.assign(workflow, req.body.workflowData); Object.assign(workflow, req.body.workflowData);
if (workflow.id !== undefined) { if (workflow.id !== undefined) {
const safeWorkflow = await EEWorkflows.preventTampering( const safeWorkflow = await EEWorkflows.preventTampering(workflow, workflow.id, req.user);
workflow,
workflow.id.toString(),
req.user,
);
req.body.workflowData.nodes = safeWorkflow.nodes; req.body.workflowData.nodes = safeWorkflow.nodes;
} }

View file

@ -106,7 +106,7 @@ workflowsController.post(
await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]); await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false); void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
return WorkflowsService.entityToResponse(savedWorkflow); return savedWorkflow;
}), }),
); );
@ -116,8 +116,7 @@ workflowsController.post(
workflowsController.get( workflowsController.get(
'/', '/',
ResponseHelper.send(async (req: WorkflowRequest.GetAll) => { ResponseHelper.send(async (req: WorkflowRequest.GetAll) => {
const workflows = await WorkflowsService.getMany(req.user, req.query.filter); return WorkflowsService.getMany(req.user, req.query.filter);
return workflows.map((workflow) => WorkflowsService.entityToResponse(workflow));
}), }),
); );
@ -218,7 +217,7 @@ workflowsController.get(
); );
} }
return WorkflowsService.entityToResponse(shared.workflow); return shared.workflow;
}), }),
); );
@ -244,7 +243,7 @@ workflowsController.patch(
['owner'], ['owner'],
); );
return WorkflowsService.entityToResponse(updatedWorkflow); return updatedWorkflow;
}), }),
); );

View file

@ -42,7 +42,8 @@ export class EEWorkflowsService extends WorkflowsService {
transaction: EntityManager, transaction: EntityManager,
workflowId: string, workflowId: string,
): Promise<SharedWorkflow[]> { ): Promise<SharedWorkflow[]> {
const workflow = await transaction.findOne(WorkflowEntity, workflowId, { const workflow = await transaction.findOne(WorkflowEntity, {
where: { id: workflowId },
relations: ['shared'], relations: ['shared'],
}); });
return workflow?.shared ?? []; return workflow?.shared ?? [];
@ -54,8 +55,8 @@ export class EEWorkflowsService extends WorkflowsService {
userIds: string[], userIds: string[],
): Promise<DeleteResult> { ): Promise<DeleteResult> {
return transaction.delete(SharedWorkflow, { return transaction.delete(SharedWorkflow, {
workflow: { id: workflowId }, workflowId,
user: { id: Not(In(userIds)) }, userId: Not(In(userIds)),
}); });
} }
@ -113,7 +114,7 @@ export class EEWorkflowsService extends WorkflowsService {
): Promise<void> { ): Promise<void> {
workflow.usedCredentials = []; workflow.usedCredentials = [];
const userCredentials = await EECredentials.getAll(currentUser, { disableGlobalRole: true }); const userCredentials = await EECredentials.getAll(currentUser, { disableGlobalRole: true });
const credentialIdsUsedByWorkflow = new Set<number>(); const credentialIdsUsedByWorkflow = new Set<string>();
workflow.nodes.forEach((node) => { workflow.nodes.forEach((node) => {
if (!node.credentials) { if (!node.credentials) {
return; return;
@ -123,8 +124,7 @@ export class EEWorkflowsService extends WorkflowsService {
if (!credential?.id) { if (!credential?.id) {
return; return;
} }
const credentialId = parseInt(credential.id, 10); credentialIdsUsedByWorkflow.add(credential.id);
credentialIdsUsedByWorkflow.add(credentialId);
}); });
}); });
const workflowCredentials = await EECredentials.getMany({ const workflowCredentials = await EECredentials.getMany({
@ -133,11 +133,11 @@ export class EEWorkflowsService extends WorkflowsService {
}, },
relations: ['shared', 'shared.user', 'shared.role'], relations: ['shared', 'shared.user', 'shared.role'],
}); });
const userCredentialIds = userCredentials.map((credential) => credential.id.toString()); const userCredentialIds = userCredentials.map((credential) => credential.id);
workflowCredentials.forEach((credential) => { workflowCredentials.forEach((credential) => {
const credentialId = credential.id.toString(); const credentialId = credential.id;
const workflowCredential: CredentialUsedByWorkflow = { const workflowCredential: CredentialUsedByWorkflow = {
id: credential.id.toString(), id: credentialId,
name: credential.name, name: credential.name,
type: credential.type, type: credential.type,
currentUserHasAccess: userCredentialIds.includes(credentialId), currentUserHasAccess: userCredentialIds.includes(credentialId),
@ -190,23 +190,24 @@ export class EEWorkflowsService extends WorkflowsService {
relations: ['shared', 'shared.user', 'shared.role'], relations: ['shared', 'shared.user', 'shared.role'],
}); });
const userCredentials = await EECredentials.getAll(currentUser, { disableGlobalRole: true }); const userCredentials = await EECredentials.getAll(currentUser, { disableGlobalRole: true });
const userCredentialIds = userCredentials.map((credential) => credential.id.toString()); const userCredentialIds = userCredentials.map((credential) => credential.id);
const credentialsMap: Record<string, CredentialUsedByWorkflow> = {}; const credentialsMap: Record<string, CredentialUsedByWorkflow> = {};
usedWorkflowsCredentials.forEach((credential) => { usedWorkflowsCredentials.forEach((credential) => {
credentialsMap[credential.id.toString()] = { const credentialId = credential.id;
id: credential.id.toString(), credentialsMap[credentialId] = {
id: credentialId,
name: credential.name, name: credential.name,
type: credential.type, type: credential.type,
currentUserHasAccess: userCredentialIds.includes(credential.id.toString()), currentUserHasAccess: userCredentialIds.includes(credentialId),
sharedWith: [], sharedWith: [],
ownedBy: null, ownedBy: null,
}; };
credential.shared?.forEach(({ user, role }) => { credential.shared?.forEach(({ user, role }) => {
const { id, email, firstName, lastName } = user; const { id, email, firstName, lastName } = user;
if (role.name === 'owner') { if (role.name === 'owner') {
credentialsMap[credential.id.toString()].ownedBy = { id, email, firstName, lastName }; credentialsMap[credentialId].ownedBy = { id, email, firstName, lastName };
} else { } else {
credentialsMap[credential.id.toString()].sharedWith?.push({ credentialsMap[credentialId].sharedWith?.push({
id, id,
email, email,
firstName, firstName,
@ -230,10 +231,9 @@ export class EEWorkflowsService extends WorkflowsService {
return; return;
} }
Object.keys(node.credentials).forEach((credentialType) => { Object.keys(node.credentials).forEach((credentialType) => {
const credentialId = parseInt(node.credentials?.[credentialType].id ?? '', 10); const credentialId = node.credentials?.[credentialType].id;
const matchedCredential = allowedCredentials.find( if (credentialId === undefined) return;
(credential) => credential.id === credentialId, const matchedCredential = allowedCredentials.find(({ id }) => id === credentialId);
);
if (!matchedCredential) { if (!matchedCredential) {
throw new Error('The workflow contains credentials that you do not have access to'); throw new Error('The workflow contains credentials that you do not have access to');
} }
@ -242,7 +242,7 @@ export class EEWorkflowsService extends WorkflowsService {
} }
static async preventTampering(workflow: WorkflowEntity, workflowId: string, user: User) { static async preventTampering(workflow: WorkflowEntity, workflowId: string, user: User) {
const previousVersion = await EEWorkflowsService.get({ id: parseInt(workflowId, 10) }); const previousVersion = await EEWorkflowsService.get({ id: workflowId });
if (!previousVersion) { if (!previousVersion) {
throw new ResponseHelper.NotFoundError('Workflow not found'); throw new ResponseHelper.NotFoundError('Workflow not found');

View file

@ -1,6 +1,6 @@
import { validate as jsonSchemaValidate } from 'jsonschema'; import { validate as jsonSchemaValidate } from 'jsonschema';
import { INode, IPinData, JsonObject, jsonParse, LoggerProxy, Workflow } from 'n8n-workflow'; import { INode, IPinData, JsonObject, jsonParse, LoggerProxy, Workflow } from 'n8n-workflow';
import { FindManyOptions, FindOneOptions, In, ObjectLiteral } from 'typeorm'; import { FindConditions, In } from 'typeorm';
import pick from 'lodash.pick'; import pick from 'lodash.pick';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
@ -16,7 +16,7 @@ import { validateEntity } from '@/GenericHelpers';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import * as TagHelpers from '@/TagHelpers'; import * as TagHelpers from '@/TagHelpers';
import { WorkflowRequest } from '@/requests'; import { WorkflowRequest } from '@/requests';
import { IWorkflowDb, IWorkflowExecutionDataProcess, IWorkflowResponse } from '@/Interfaces'; import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
@ -25,7 +25,7 @@ import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper'; import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper';
export interface IGetWorkflowsQueryFilter { export interface IGetWorkflowsQueryFilter {
id?: number | string; id?: string;
name?: string; name?: string;
active?: boolean; active?: boolean;
} }
@ -45,28 +45,20 @@ const allowedWorkflowsQueryFilterFields = Object.keys(schemaGetWorkflowsQueryFil
export class WorkflowsService { export class WorkflowsService {
static async getSharing( static async getSharing(
user: User, user: User,
workflowId: number | string, workflowId: string,
relations: string[] = ['workflow'], relations: string[] = ['workflow'],
{ allowGlobalOwner } = { allowGlobalOwner: true }, { allowGlobalOwner } = { allowGlobalOwner: true },
): Promise<SharedWorkflow | undefined> { ): Promise<SharedWorkflow | undefined> {
const options: FindOneOptions<SharedWorkflow> & { where: ObjectLiteral } = { const where: FindConditions<SharedWorkflow> = { workflowId };
where: {
workflow: { id: workflowId },
},
};
// Omit user from where if the requesting user is the global // Omit user from where if the requesting user is the global
// owner. This allows the global owner to view and delete // owner. This allows the global owner to view and delete
// workflows they don't own. // workflows they don't own.
if (!allowGlobalOwner || user.globalRole.name !== 'owner') { if (!allowGlobalOwner || user.globalRole.name !== 'owner') {
options.where.user = { id: user.id }; where.userId = user.id;
} }
if (relations?.length) { return Db.collections.SharedWorkflow.findOne({ where, relations });
options.relations = relations;
}
return Db.collections.SharedWorkflow.findOne(options);
} }
/** /**
@ -120,11 +112,6 @@ export class WorkflowsService {
return getSharedWorkflowIds(user, roles); return getSharedWorkflowIds(user, roles);
} }
static entityToResponse(entity: WorkflowEntity): IWorkflowResponse {
const { id, ...rest } = entity;
return { ...rest, id: id.toString() };
}
static async getMany(user: User, rawFilter: string): Promise<WorkflowEntity[]> { static async getMany(user: User, rawFilter: string): Promise<WorkflowEntity[]> {
const sharedWorkflowIds = await this.getWorkflowIdsForUser(user, ['owner']); const sharedWorkflowIds = await this.getWorkflowIdsForUser(user, ['owner']);
if (sharedWorkflowIds.length === 0) { if (sharedWorkflowIds.length === 0) {
@ -181,16 +168,14 @@ export class WorkflowsService {
relations.push('shared', 'shared.user', 'shared.role'); relations.push('shared', 'shared.user', 'shared.role');
} }
const query: FindManyOptions<WorkflowEntity> = { return Db.collections.Workflow.find({
select: isSharingEnabled() ? [...fields, 'versionId'] : fields, select: isSharingEnabled() ? [...fields, 'versionId'] : fields,
relations, relations,
where: { where: {
id: In(sharedWorkflowIds), id: In(sharedWorkflowIds),
...filter, ...filter,
}, },
}; });
return Db.collections.Workflow.find(query);
} }
static async update( static async update(
@ -310,17 +295,11 @@ export class WorkflowsService {
} }
} }
const options: FindManyOptions<WorkflowEntity> = { const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
relations: ['tags'],
};
if (config.getEnv('workflowTagsDisabled')) {
delete options.relations;
}
// We sadly get nothing back from "update". Neither if it updated a record // We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry. // nor the new value. So query now the hopefully updated entry.
const updatedWorkflow = await Db.collections.Workflow.findOne(workflowId, options); const updatedWorkflow = await Db.collections.Workflow.findOne(workflowId, { relations });
if (updatedWorkflow === undefined) { if (updatedWorkflow === undefined) {
throw new ResponseHelper.BadRequestError( throw new ResponseHelper.BadRequestError(

View file

@ -351,7 +351,7 @@ test('GET /credentials/:id should return 404 if cred not found', async () => {
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
const responseAbc = await authAgent(ownerShell).get('/credentials/abc'); const responseAbc = await authAgent(ownerShell).get('/credentials/abc');
expect(responseAbc.statusCode).toBe(400); expect(responseAbc.statusCode).toBe(404);
// because EE router has precedence, check if forwards this route // because EE router has precedence, check if forwards this route
const responseNew = await authAgent(ownerShell).get('/credentials/new'); const responseNew = await authAgent(ownerShell).get('/credentials/new');

View file

@ -81,7 +81,7 @@ test('GET /credentials should return all creds for owner', async () => {
response.body.data.forEach((credential: CredentialsEntity) => { response.body.data.forEach((credential: CredentialsEntity) => {
validateMainCredentialData(credential); validateMainCredentialData(credential);
expect(credential.data).toBeUndefined(); expect(credential.data).toBeUndefined();
expect(savedCredentialsIds.includes(Number(credential.id))).toBe(true); expect(savedCredentialsIds).toContain(credential.id);
}); });
}); });
@ -104,7 +104,7 @@ test('GET /credentials should return only own creds for member', async () => {
validateMainCredentialData(member1Credential); validateMainCredentialData(member1Credential);
expect(member1Credential.data).toBeUndefined(); expect(member1Credential.data).toBeUndefined();
expect(member1Credential.id).toBe(savedCredential1.id.toString()); expect(member1Credential.id).toBe(savedCredential1.id);
}); });
test('POST /credentials should create cred', async () => { test('POST /credentials should create cred', async () => {
@ -573,16 +573,11 @@ test('GET /credentials/:id should fail with missing encryption key', async () =>
test('GET /credentials/:id should return 404 if cred not found', async () => { test('GET /credentials/:id should return 404 if cred not found', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).get('/credentials/789'); const response = await authAgent(ownerShell).get('/credentials/789');
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
});
test('GET /credentials/:id should return 400 if id is not a number', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const responseAbc = await authAgent(ownerShell).get('/credentials/abc'); const responseAbc = await authAgent(ownerShell).get('/credentials/abc');
expect(responseAbc.statusCode).toBe(400); expect(responseAbc.statusCode).toBe(404);
}); });
function validateMainCredentialData(credential: CredentialsEntity) { function validateMainCredentialData(credential: CredentialsEntity) {

View file

@ -463,7 +463,7 @@ test('GET /executions should retrieve all executions of specific workflow', asyn
await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution); await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution);
const response = await authOwnerAgent.get(`/executions`).query({ const response = await authOwnerAgent.get(`/executions`).query({
workflowId: workflow.id.toString(), workflowId: workflow.id,
}); });
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -490,7 +490,7 @@ test('GET /executions should retrieve all executions of specific workflow', asyn
expect(retryOf).toBeNull(); expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull(); expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull(); expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(workflow.id.toString()); expect(workflowId).toBe(workflow.id);
expect(waitTill).toBeNull(); expect(waitTill).toBeNull();
} }
}); });

View file

@ -195,7 +195,7 @@ test('GET /workflows should return all owned workflows with pagination', async (
} }
// check that we really received a different result // check that we really received a different result
expect(response.body.data[0].id).toBeLessThan(response2.body.data[0].id); expect(Number(response.body.data[0].id)).toBeLessThan(Number(response2.body.data[0].id));
}); });
test('GET /workflows should return all owned workflows filtered by tag', async () => { test('GET /workflows should return all owned workflows filtered by tag', async () => {
@ -690,7 +690,7 @@ test('POST /workflows/:id/activate should set workflow as active', async () => {
expect(sharedWorkflow?.workflow.active).toBe(true); expect(sharedWorkflow?.workflow.active).toBe(true);
// check whether the workflow is on the active workflow runner // check whether the workflow is on the active workflow runner
expect(await workflowRunner.isActive(workflow.id.toString())).toBe(true); expect(await workflowRunner.isActive(workflow.id)).toBe(true);
}); });
test('POST /workflows/:id/activate should set non-owned workflow as active when owner', async () => { test('POST /workflows/:id/activate should set non-owned workflow as active when owner', async () => {
@ -744,7 +744,7 @@ test('POST /workflows/:id/activate should set non-owned workflow as active when
expect(sharedWorkflow?.workflow.active).toBe(true); expect(sharedWorkflow?.workflow.active).toBe(true);
// check whether the workflow is on the active workflow runner // check whether the workflow is on the active workflow runner
expect(await workflowRunner.isActive(workflow.id.toString())).toBe(true); expect(await workflowRunner.isActive(workflow.id)).toBe(true);
}); });
test('POST /workflows/:id/deactivate should fail due to missing API Key', async () => { test('POST /workflows/:id/deactivate should fail due to missing API Key', async () => {
@ -835,7 +835,7 @@ test('POST /workflows/:id/deactivate should deactivate workflow', async () => {
// check whether the workflow is deactivated in the database // check whether the workflow is deactivated in the database
expect(sharedWorkflow?.workflow.active).toBe(false); expect(sharedWorkflow?.workflow.active).toBe(false);
expect(await workflowRunner.isActive(workflow.id.toString())).toBe(false); expect(await workflowRunner.isActive(workflow.id)).toBe(false);
}); });
test('POST /workflows/:id/deactivate should deactivate non-owned workflow when owner', async () => { test('POST /workflows/:id/deactivate should deactivate non-owned workflow when owner', async () => {
@ -888,7 +888,7 @@ test('POST /workflows/:id/deactivate should deactivate non-owned workflow when o
expect(sharedWorkflow?.workflow.active).toBe(false); expect(sharedWorkflow?.workflow.active).toBe(false);
expect(await workflowRunner.isActive(workflow.id.toString())).toBe(false); expect(await workflowRunner.isActive(workflow.id)).toBe(false);
}); });
test('POST /workflows should fail due to missing API Key', async () => { test('POST /workflows should fail due to missing API Key', async () => {

View file

@ -474,7 +474,7 @@ export async function createExecution(
finished: finished ?? true, finished: finished ?? true,
mode: mode ?? 'manual', mode: mode ?? 'manual',
startedAt: startedAt ?? new Date(), startedAt: startedAt ?? new Date(),
...(workflow !== undefined && { workflowData: workflow, workflowId: workflow.id.toString() }), ...(workflow !== undefined && { workflowData: workflow, workflowId: workflow.id }),
stoppedAt: stoppedAt ?? new Date(), stoppedAt: stoppedAt ?? new Date(),
waitTill: waitTill ?? null, waitTill: waitTill ?? null,
}); });

View file

@ -180,7 +180,7 @@ describe('GET /workflows', () => {
position: [0, 0], position: [0, 0],
credentials: { credentials: {
actionNetworkApi: { actionNetworkApi: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },
@ -220,7 +220,7 @@ describe('GET /workflows', () => {
const [usedCredential] = fetchedWorkflow.usedCredentials; const [usedCredential] = fetchedWorkflow.usedCredentials;
expect(usedCredential).toMatchObject({ expect(usedCredential).toMatchObject({
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
type: savedCredential.type, type: savedCredential.type,
currentUserHasAccess: true, currentUserHasAccess: true,
@ -313,7 +313,7 @@ describe('GET /workflows/:id', () => {
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const workflow = await createWorkflow(workflowPayload, owner); const workflow = await createWorkflow(workflowPayload, owner);
@ -322,7 +322,7 @@ describe('GET /workflows/:id', () => {
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data.usedCredentials).toMatchObject([ expect(response.body.data.usedCredentials).toMatchObject([
{ {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
currentUserHasAccess: true, currentUserHasAccess: true,
}, },
@ -338,7 +338,7 @@ describe('GET /workflows/:id', () => {
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const workflow = await createWorkflow(workflowPayload, owner); const workflow = await createWorkflow(workflowPayload, owner);
@ -347,7 +347,7 @@ describe('GET /workflows/:id', () => {
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data.usedCredentials).toMatchObject([ expect(response.body.data.usedCredentials).toMatchObject([
{ {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
currentUserHasAccess: false, // although owner can see, he does not have access currentUserHasAccess: false, // although owner can see, he does not have access
}, },
@ -363,7 +363,7 @@ describe('GET /workflows/:id', () => {
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const workflow = await createWorkflow(workflowPayload, member1); const workflow = await createWorkflow(workflowPayload, member1);
await testDb.shareWorkflowWithUsers(workflow, [member2]); await testDb.shareWorkflowWithUsers(workflow, [member2]);
@ -372,7 +372,7 @@ describe('GET /workflows/:id', () => {
expect(responseMember1.statusCode).toBe(200); expect(responseMember1.statusCode).toBe(200);
expect(responseMember1.body.data.usedCredentials).toMatchObject([ expect(responseMember1.body.data.usedCredentials).toMatchObject([
{ {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
currentUserHasAccess: true, // one user has access currentUserHasAccess: true, // one user has access
}, },
@ -383,7 +383,7 @@ describe('GET /workflows/:id', () => {
expect(responseMember2.statusCode).toBe(200); expect(responseMember2.statusCode).toBe(200);
expect(responseMember2.body.data.usedCredentials).toMatchObject([ expect(responseMember2.body.data.usedCredentials).toMatchObject([
{ {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
currentUserHasAccess: false, // the other one doesn't currentUserHasAccess: false, // the other one doesn't
}, },
@ -400,7 +400,7 @@ describe('GET /workflows/:id', () => {
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const workflow = await createWorkflow(workflowPayload, member1); const workflow = await createWorkflow(workflowPayload, member1);
await testDb.shareWorkflowWithUsers(workflow, [member2]); await testDb.shareWorkflowWithUsers(workflow, [member2]);
@ -409,7 +409,7 @@ describe('GET /workflows/:id', () => {
expect(responseMember1.statusCode).toBe(200); expect(responseMember1.statusCode).toBe(200);
expect(responseMember1.body.data.usedCredentials).toMatchObject([ expect(responseMember1.body.data.usedCredentials).toMatchObject([
{ {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
currentUserHasAccess: true, currentUserHasAccess: true,
}, },
@ -420,7 +420,7 @@ describe('GET /workflows/:id', () => {
expect(responseMember2.statusCode).toBe(200); expect(responseMember2.statusCode).toBe(200);
expect(responseMember2.body.data.usedCredentials).toMatchObject([ expect(responseMember2.body.data.usedCredentials).toMatchObject([
{ {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
currentUserHasAccess: true, currentUserHasAccess: true,
}, },
@ -446,7 +446,7 @@ describe('POST /workflows', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const workflow = makeWorkflow({ const workflow = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const response = await authAgent(owner).post('/workflows').send(workflow); const response = await authAgent(owner).post('/workflows').send(workflow);
@ -462,7 +462,7 @@ describe('POST /workflows', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const workflow = makeWorkflow({ const workflow = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const response = await authAgent(member).post('/workflows').send(workflow); const response = await authAgent(member).post('/workflows').send(workflow);
@ -481,7 +481,7 @@ describe('POST /workflows', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const workflow = makeWorkflow({ const workflow = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const response = await authAgent(owner).post('/workflows').send(workflow); const response = await authAgent(owner).post('/workflows').send(workflow);
@ -498,7 +498,7 @@ describe('POST /workflows', () => {
const workflow = makeWorkflow({ const workflow = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const response = await authAgent(member2).post('/workflows').send(workflow); const response = await authAgent(member2).post('/workflows').send(workflow);
@ -525,7 +525,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
typeVersion: 1, typeVersion: 1,
credentials: { credentials: {
default: { default: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },
@ -563,7 +563,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
typeVersion: 1, typeVersion: 1,
credentials: { credentials: {
default: { default: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },
@ -588,7 +588,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
typeVersion: 1, typeVersion: 1,
credentials: { credentials: {
default: { default: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },
@ -619,7 +619,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
typeVersion: 1, typeVersion: 1,
credentials: { credentials: {
default: { default: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },
@ -653,7 +653,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
typeVersion: 1, typeVersion: 1,
credentials: { credentials: {
default: { default: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },
@ -682,7 +682,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
typeVersion: 1, typeVersion: 1,
credentials: { credentials: {
default: { default: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },
@ -721,7 +721,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
typeVersion: 1, typeVersion: 1,
credentials: { credentials: {
default: { default: {
id: savedCredential.id.toString(), id: savedCredential.id,
name: savedCredential.name, name: savedCredential.name,
}, },
}, },

View file

@ -22,13 +22,13 @@ jest.mock('@/Db', () => {
collections: { collections: {
Workflow: { Workflow: {
update: jest.fn(({ id, dataLoaded }, updateArgs) => { update: jest.fn(({ id, dataLoaded }, updateArgs) => {
if (id === 1) return { affected: 1 }; if (id === '1') return { affected: 1 };
return { affected: 0 }; return { affected: 0 };
}), }),
}, },
WorkflowStatistics: { WorkflowStatistics: {
insert: jest.fn(({ count, name, workflowId }) => { insert: jest.fn(({ count, name, workflowId }) => {
if (workflowId === -1) throw new Error('test error'); if (workflowId === '-1') throw new Error('test error');
return null; return null;
}), }),
update: jest.fn((...args) => {}), update: jest.fn((...args) => {}),
@ -104,7 +104,7 @@ describe('Events', () => {
expect(mockedFirstProductionWorkflowSuccess).toBeCalledTimes(1); expect(mockedFirstProductionWorkflowSuccess).toBeCalledTimes(1);
expect(mockedFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, { expect(mockedFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, {
user_id: FAKE_USER_ID, user_id: FAKE_USER_ID,
workflow_id: parseInt(workflow.id, 10), workflow_id: workflow.id,
}); });
}); });
@ -180,7 +180,7 @@ describe('Events', () => {
expect(mockedFirstWorkflowDataLoad).toBeCalledTimes(1); expect(mockedFirstWorkflowDataLoad).toBeCalledTimes(1);
expect(mockedFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { expect(mockedFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, {
user_id: FAKE_USER_ID, user_id: FAKE_USER_ID,
workflow_id: parseInt(workflowId, 10), workflow_id: workflowId,
node_type: node.type, node_type: node.type,
node_id: node.id, node_id: node.id,
}); });
@ -207,7 +207,7 @@ describe('Events', () => {
expect(mockedFirstWorkflowDataLoad).toBeCalledTimes(1); expect(mockedFirstWorkflowDataLoad).toBeCalledTimes(1);
expect(mockedFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { expect(mockedFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, {
user_id: FAKE_USER_ID, user_id: FAKE_USER_ID,
workflow_id: parseInt(workflowId, 10), workflow_id: workflowId,
node_type: node.type, node_type: node.type,
node_id: node.id, node_id: node.id,
credential_type: 'testCredentials', credential_type: 'testCredentials',

View file

@ -129,7 +129,7 @@ describe('PermissionChecker.check()', () => {
position: [0, 0], position: [0, 0],
credentials: { credentials: {
actionNetworkApi: { actionNetworkApi: {
id: ownerCred.id.toString(), id: ownerCred.id,
name: ownerCred.name, name: ownerCred.name,
}, },
}, },
@ -143,7 +143,7 @@ describe('PermissionChecker.check()', () => {
position: [0, 0], position: [0, 0],
credentials: { credentials: {
actionNetworkApi: { actionNetworkApi: {
id: memberCred.id.toString(), id: memberCred.id,
name: memberCred.name, name: memberCred.name,
}, },
}, },
@ -160,7 +160,7 @@ describe('PermissionChecker.check()', () => {
const memberCred = await saveCredential(randomCred(), { user: member }); const memberCred = await saveCredential(randomCred(), { user: member });
const workflowDetails = { const workflowDetails = {
id: randomPositiveDigit(), id: randomPositiveDigit().toString(),
name: 'test', name: 'test',
active: false, active: false,
connections: {}, connections: {},
@ -175,7 +175,7 @@ describe('PermissionChecker.check()', () => {
position: [0, 0] as [number, number], position: [0, 0] as [number, number],
credentials: { credentials: {
actionNetworkApi: { actionNetworkApi: {
id: memberCred.id.toString(), id: memberCred.id,
name: memberCred.name, name: memberCred.name,
}, },
}, },
@ -205,7 +205,7 @@ describe('PermissionChecker.check()', () => {
role: workflowOwnerRole, role: workflowOwnerRole,
}); });
const workflow = new Workflow({ ...workflowDetails, id: workflowDetails.id.toString() }); const workflow = new Workflow(workflowDetails);
expect(PermissionChecker.check(workflow, member.id)).rejects.toThrow(); expect(PermissionChecker.check(workflow, member.id)).rejects.toThrow();
}); });

View file

@ -8,7 +8,7 @@ async function mockFind({
id, id,
type, type,
}: { }: {
id: string | number; id: string;
type: string; type: string;
}): Promise<IWorkflowCredentials | null> { }): Promise<IWorkflowCredentials | null> {
// Simple statement that maps a return value based on the `id` parameter // Simple statement that maps a return value based on the `id` parameter

View file

@ -148,7 +148,7 @@ describe('WorkflowHelpers', () => {
function generateCredentialEntity(credentialId: string) { function generateCredentialEntity(credentialId: string) {
const credentialEntity = new CredentialsEntity(); const credentialEntity = new CredentialsEntity();
credentialEntity.id = parseInt(credentialId, 10); credentialEntity.id = credentialId;
return credentialEntity; return credentialEntity;
} }

View file

@ -217,8 +217,8 @@ export interface IRestApi {
getPastExecutions( getPastExecutions(
filter: object, filter: object,
limit: number, limit: number,
lastId?: string | number, lastId?: string,
firstId?: string | number, firstId?: string,
): Promise<IExecutionsListResponse>; ): Promise<IExecutionsListResponse>;
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>; stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>;
@ -276,7 +276,7 @@ export interface IVariableSelectorOption {
// Simple version of n8n-workflow.Workflow // Simple version of n8n-workflow.Workflow
export interface IWorkflowData { export interface IWorkflowData {
id?: string | number; id?: string;
name?: string; name?: string;
active?: boolean; active?: boolean;
nodes: INode[]; nodes: INode[];
@ -288,7 +288,7 @@ export interface IWorkflowData {
} }
export interface IWorkflowDataUpdate { export interface IWorkflowDataUpdate {
id?: string | number; id?: string;
name?: string; name?: string;
nodes?: INode[]; nodes?: INode[];
connections?: IConnections; connections?: IConnections;
@ -391,7 +391,7 @@ export interface ICredentialsDecryptedResponse extends ICredentialsBase, ICreden
} }
export interface IExecutionBase { export interface IExecutionBase {
id?: number | string; id?: string;
finished: boolean; finished: boolean;
mode: WorkflowExecuteMode; mode: WorkflowExecuteMode;
retryOf?: string; retryOf?: string;

View file

@ -153,7 +153,7 @@ export default mixins(restApi).extend({
}, },
credentialData: {}, credentialData: {},
credentialId: { credentialId: {
type: [String, Number], type: String,
default: '', default: '',
}, },
showValidationWarning: { showValidationWarning: {

View file

@ -171,7 +171,7 @@ export default mixins(showMessage, nodeHelpers).extend({
required: true, required: true,
}, },
activeId: { activeId: {
type: [String, Number], type: [String],
required: true, required: true,
}, },
mode: { mode: {

View file

@ -742,7 +742,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
this.isDataLoading = true; this.isDataLoading = true;
const filter = this.workflowFilterPast; const filter = this.workflowFilterPast;
let lastId: string | number | undefined; let lastId: string | undefined;
if (this.finishedExecutions.length !== 0) { if (this.finishedExecutions.length !== 0) {
const lastItem = this.finishedExecutions.slice(-1)[0]; const lastItem = this.finishedExecutions.slice(-1)[0];

View file

@ -219,7 +219,7 @@ export default mixins(
} }
this.loadingMore = true; this.loadingMore = true;
let lastId: string | number | undefined; let lastId: string | undefined;
if (this.executions.length !== 0) { if (this.executions.length !== 0) {
const lastItem = this.executions.slice(-1)[0]; const lastItem = this.executions.slice(-1)[0];
lastId = lastItem.id; lastId = lastItem.id;

View file

@ -434,10 +434,6 @@ export default mixins(workflowHelpers, titleChange).extend({
case WORKFLOW_MENU_ACTIONS.DOWNLOAD: { case WORKFLOW_MENU_ACTIONS.DOWNLOAD: {
const workflowData = await this.getWorkflowDataToSave(); const workflowData = await this.getWorkflowDataToSave();
const { tags, ...data } = workflowData; const { tags, ...data } = workflowData;
if (data.id && typeof data.id === 'string') {
data.id = parseInt(data.id, 10);
}
const exportData: IWorkflowToShare = { const exportData: IWorkflowToShare = {
...data, ...data,
meta: { meta: {

View file

@ -179,8 +179,8 @@ export const restApi = Vue.extend({
getPastExecutions: ( getPastExecutions: (
filter: object, filter: object,
limit: number, limit: number,
lastId?: string | number, lastId?: string,
firstId?: string | number, firstId?: string,
): Promise<IExecutionsListResponse> => { ): Promise<IExecutionsListResponse> => {
let sendData = {}; let sendData = {};
if (filter) { if (filter) {

View file

@ -133,7 +133,7 @@ export interface ICredentialNodeAccess {
} }
export interface ICredentialsDecrypted { export interface ICredentialsDecrypted {
id: string | number; id: string;
name: string; name: string;
type: string; type: string;
nodesAccess: ICredentialNodeAccess[]; nodesAccess: ICredentialNodeAccess[];
@ -143,7 +143,7 @@ export interface ICredentialsDecrypted {
} }
export interface ICredentialsEncrypted { export interface ICredentialsEncrypted {
id?: string | number; id?: string;
name: string; name: string;
type: string; type: string;
nodesAccess: ICredentialNodeAccess[]; nodesAccess: ICredentialNodeAccess[];
@ -1430,7 +1430,7 @@ export interface IWorkflowDataProxyData {
export type IWorkflowDataProxyAdditionalKeys = IDataObject; export type IWorkflowDataProxyAdditionalKeys = IDataObject;
export interface IWorkflowMetadata { export interface IWorkflowMetadata {
id?: number | string; id?: string;
name?: string; name?: string;
active: boolean; active: boolean;
} }
@ -1571,7 +1571,7 @@ export interface IWaitingForExecutionSource {
} }
export interface IWorkflowBase { export interface IWorkflowBase {
id?: number | string | any; id?: string;
name: string; name: string;
active: boolean; active: boolean;
createdAt: Date; createdAt: Date;