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();
}
if (
executionData.workflowData.id !== undefined &&
WorkflowHelpers.isWorkflowIdValid(executionData.workflowData.id.toString())
) {
fullExecutionData.workflowId = executionData.workflowData.id.toString();
const workflowId = executionData.workflowData.id;
if (workflowId !== undefined && WorkflowHelpers.isWorkflowIdValid(workflowId)) {
fullExecutionData.workflowId = workflowId;
}
const execution = ResponseHelper.flattenExecutionData(fullExecutionData);

View file

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

View file

@ -279,7 +279,7 @@ export class CredentialsHelper extends ICredentialsHelper {
const credential = userId
? await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentials: { id: nodeCredential.id, type }, user: { id: userId } },
where: { credentials: { id: nodeCredential.id, type }, userId },
}).then((shared) => shared.credentials)
: await Db.collections.Credentials.findOneOrFail({ id: nodeCredential.id, type });
@ -290,7 +290,7 @@ export class CredentialsHelper extends ICredentialsHelper {
}
return new Credentials(
{ id: credential.id.toString(), name: credential.name },
{ id: credential.id, name: credential.name },
credential.type,
credential.nodesAccess,
credential.data,
@ -581,7 +581,7 @@ export class CredentialsHelper extends ICredentialsHelper {
position: [0, 0],
credentials: {
[credentialType]: {
id: credentialsDecrypted.id.toString(),
id: credentialsDecrypted.id,
name: credentialsDecrypted.name,
},
},
@ -762,8 +762,7 @@ export async function getCredentialForUser(
export async function getCredentialWithoutUser(
credentialId: string,
): Promise<ICredentialsDb | undefined> {
const credential = await Db.collections.Credentials.findOne(credentialId);
return credential;
return Db.collections.Credentials.findOne(credentialId);
}
export function createCredentialsFromCredentialsEntity(
@ -774,5 +773,5 @@ export function createCredentialsFromCredentialsEntity(
if (encrypt) {
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,
ITelemetrySettings,
ITelemetryTrackProperties,
IWorkflowBase as IWorkflowBaseWorkflow,
IWorkflowBase,
CredentialLoadingDetails,
Workflow,
WorkflowActivateMode,
@ -38,6 +38,7 @@ import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User';
import type { WebhookEntity } from '@db/entities/WebhookEntity';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
@ -71,7 +72,7 @@ export interface IDatabaseCollections {
Credentials: Repository<ICredentialsDb>;
Execution: Repository<IExecutionFlattedDb>;
Workflow: Repository<WorkflowEntity>;
Webhook: Repository<IWebhookDb>;
Webhook: Repository<WebhookEntity>;
Tag: Repository<TagEntity>;
Role: Repository<Role>;
User: Repository<User>;
@ -83,28 +84,12 @@ export interface IDatabaseCollections {
WorkflowStatistics: Repository<WorkflowStatistics>;
}
export interface IWebhookDb {
workflowId: number | string;
webhookPath: string;
method: string;
node: string;
webhookId?: string;
pathLength?: number;
}
// ----------------------------------
// tags
// ----------------------------------
export interface ITagDb {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
}
export interface ITagToImport {
id: string | number;
id: string;
name: string;
createdAt?: string;
updatedAt?: string;
@ -114,20 +99,16 @@ export type UsageCount = {
usageCount: number;
};
export type ITagWithCountDb = ITagDb & UsageCount;
export type ITagWithCountDb = TagEntity & UsageCount;
// ----------------------------------
// workflows
// ----------------------------------
export interface IWorkflowBase extends IWorkflowBaseWorkflow {
id?: number | string;
}
// Almost identical to editor-ui.Interfaces.ts
export interface IWorkflowDb extends IWorkflowBase {
id: number | string;
tags?: ITagDb[];
id: string;
tags?: TagEntity[];
}
export interface IWorkflowToImport extends IWorkflowBase {
@ -148,35 +129,27 @@ export interface ICredentialsBase {
}
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted {
id: number | string;
id: string;
name: string;
shared?: SharedCredentials[];
}
export interface ICredentialsResponse extends ICredentialsDb {
id: string;
}
export type ICredentialsDecryptedDb = ICredentialsBase & ICredentialsDecrypted;
export interface ICredentialsDecryptedDb extends ICredentialsBase, ICredentialsDecrypted {
id: number | string;
}
export interface ICredentialsDecryptedResponse extends ICredentialsDecryptedDb {
id: string;
}
export type ICredentialsDecryptedResponse = ICredentialsDecryptedDb;
export type DatabaseType = 'mariadb' | 'postgresdb' | 'mysqldb' | 'sqlite';
export type SaveExecutionDataType = 'all' | 'none';
export interface IExecutionBase {
id?: number | string;
id?: string;
mode: WorkflowExecuteMode;
startedAt: Date;
stoppedAt?: Date; // empty value means execution is still running
workflowId?: string; // To be able to filter executions easily //
finished: boolean;
retryOf?: number | 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.
retryOf?: string; // If it is a retry, the id of the execution it is a retry of.
retrySuccessId?: string; // If it failed and a retry did succeed. The id of the successful retry.
}
// Data in regular format with references
@ -208,7 +181,7 @@ export interface IExecutionFlatted extends IExecutionBase {
}
export interface IExecutionFlattedDb extends IExecutionBase {
id: number | string;
id: string;
data: string;
waitTill?: Date | null;
workflowData: Omit<IWorkflowBase, 'pinData'>;
@ -220,14 +193,14 @@ export interface IExecutionFlattedResponse extends IExecutionFlatted {
}
export interface IExecutionResponseApi {
id: number | string;
id: string;
mode: WorkflowExecuteMode;
startedAt: Date;
stoppedAt?: Date;
workflowId?: string;
finished: boolean;
retryOf?: number | string;
retrySuccessId?: number | string;
retryOf?: string;
retrySuccessId?: string;
data?: object;
waitTill?: Date | null;
workflowData: IWorkflowBase;
@ -685,7 +658,7 @@ export interface IWorkflowExecutionDataProcess {
executionData?: IRunExecutionData;
runData?: IRunData;
pinData?: IPinData;
retryOf?: number | string;
retryOf?: string;
sessionId?: string;
startNodes?: string[];
workflowData: IWorkflowBase;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@ export = {
return res.status(404).json({ message: 'Not Found' });
}
await BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(execution.id.toString());
await BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(execution.id);
await deleteExecution(execution);
@ -97,7 +97,7 @@ export = {
// get running workflows so we exclude them from the result
const runningExecutionsIds = ActiveExecutions.getInstance()
.getActiveExecutions()
.map(({ id }) => Number(id));
.map(({ id }) => id);
const filters = {
status,
@ -110,7 +110,7 @@ export = {
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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -983,8 +983,7 @@ class App {
`/${this.restEndpoint}/active`,
ResponseHelper.send(async (req: WorkflowRequest.GetAllActive) => {
const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(req.user);
return activeWorkflows.map(({ id }) => id.toString());
return activeWorkflows.map(({ id }) => id);
}),
);
@ -1365,22 +1364,21 @@ class App {
for (const data of executingWorkflows) {
if (
(filter.workflowId !== undefined && filter.workflowId !== data.workflowId) ||
(data.workflowId !== undefined &&
!sharedWorkflowIds.includes(data.workflowId.toString()))
(data.workflowId !== undefined && !sharedWorkflowIds.includes(data.workflowId))
) {
continue;
}
returnData.push({
id: data.id.toString(),
workflowId: data.workflowId === undefined ? '' : data.workflowId.toString(),
id: data.id,
workflowId: data.workflowId === undefined ? '' : data.workflowId,
mode: data.mode,
retryOf: data.retryOf,
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;
},
@ -1435,7 +1433,7 @@ class App {
const queue = await Queue.getInstance();
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) {
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[] },
) {
const tagMap = tags.reduce<Record<string, TagEntity>>((acc, tag) => {
acc[tag.id.toString()] = tag;
acc[tag.id] = tag;
return acc;
}, {});
@ -50,6 +50,7 @@ export async function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithC
.getRawMany()
.then((tagsWithCount) => {
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
tag.id = tag.id.toString();
// 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
const identicalMatch = tagsEntities.find(
(existingTag) =>
existingTag.id.toString() === importTag.id.toString() &&
existingTag.id === importTag.id &&
existingTag.createdAt &&
importTag.createdAt &&
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)
const timeout = setTimeout(() => {
this.cancelTestWebhook(workflowData.id.toString());
this.cancelTestWebhook(workflowData.id);
}, 120000);
let key: string;
@ -260,7 +260,7 @@ export class TestWebhooks {
for (const webhookKey of Object.keys(this.testWebhookData)) {
const webhookData = this.testWebhookData[webhookKey];
if (webhookData.workflowData.id.toString() !== workflowId) {
if (webhookData.workflowData.id !== workflowId) {
// eslint-disable-next-line no-continue
continue;
}

View file

@ -42,7 +42,7 @@ export class PermissionChecker {
if (workflow.id && isSharingEnabled()) {
const workflowSharings = await Db.collections.SharedWorkflow.find({
relations: ['workflow'],
where: { workflow: { id: Number(workflow.id) } },
where: { workflowId: workflow.id },
});
workflowUserIds = workflowSharings.map((s) => s.userId);
}
@ -58,7 +58,7 @@ export class PermissionChecker {
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));

View file

@ -17,11 +17,11 @@ import { getLicense } from '@/License';
import { WhereClause } from '@/Interfaces';
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 sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({
where: { workflow: { id: workflowId }, role: workflowOwnerRole },
where: { workflowId, role: workflowOwnerRole },
relations: ['user', 'user.globalRole'],
});

View file

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

View file

@ -77,7 +77,7 @@ export class WaitTrackerClass {
return;
}
const executionIds = executions.map((execution) => execution.id.toString()).join(', ');
const executionIds = executions.map((execution) => execution.id).join(', ');
Logger.debug(
`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
// eslint-disable-next-line no-restricted-syntax
for (const execution of executions) {
const executionId = execution.id.toString();
const executionId = execution.id;
if (this.waitingExecutions[executionId] === undefined) {
const triggerTime = execution.waitTill!.getTime() - new Date().getTime();
this.waitingExecutions[executionId] = {
@ -161,7 +161,7 @@ export class WaitTrackerClass {
if (!fullExecutionData.workflowData.id) {
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 = {
executionMode: fullExecutionData.mode,

View file

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

View file

@ -27,13 +27,13 @@ import {
IRun,
IRunExecutionData,
ITaskData,
IWorkflowBase,
IWorkflowExecuteAdditionalData,
IWorkflowExecuteHooks,
IWorkflowHooksOptionalParameters,
IWorkflowSettings,
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger,
SubworkflowOperationError,
Workflow,
WorkflowExecuteMode,
WorkflowHooks,
@ -51,7 +51,6 @@ import {
IExecutionFlattedDb,
IExecutionResponse,
IPushDataExecutionFinished,
IWorkflowBase,
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcess,
IWorkflowErrorData,
@ -62,7 +61,7 @@ import * as Push from '@/Push';
import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import { getUserById, getWorkflowOwner, whereClause } from '@/UserManagement/UserManagementHelper';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { findSubworkflowStart } from '@/utils';
import { PermissionChecker } from './UserManagement/PermissionChecker';
import { WorkflowsService } from './workflows/workflows.services';
@ -94,6 +93,7 @@ export function executeErrorWorkflow(
if (fullRunData.data.resultData.error !== undefined) {
let workflowErrorData: IWorkflowErrorData;
const workflowId = workflowData.id;
if (executionId) {
// The error did happen in an execution
@ -107,7 +107,7 @@ export function executeErrorWorkflow(
retryOf,
},
workflow: {
id: workflowData.id !== undefined ? workflowData.id.toString() : undefined,
id: workflowId,
name: workflowData.name,
},
};
@ -119,7 +119,7 @@ export function executeErrorWorkflow(
mode,
},
workflow: {
id: workflowData.id !== undefined ? workflowData.id.toString() : undefined,
id: workflowId,
name: workflowData.name,
},
};
@ -128,30 +128,28 @@ export function executeErrorWorkflow(
// 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.
if (
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
workflowData.settings !== undefined &&
workflowData.settings.errorWorkflow &&
workflowData.settings?.errorWorkflow &&
!(
mode === 'error' &&
workflowData.id &&
workflowData.settings.errorWorkflow.toString() === workflowData.id.toString()
workflowId &&
workflowData.settings.errorWorkflow.toString() === workflowId
)
) {
Logger.verbose('Start external error workflow', {
executionId,
errorWorkflowId: workflowData.settings.errorWorkflow.toString(),
workflowId: workflowData.id,
workflowId,
});
// If a specific error workflow is set run only that one
// First, do permission checks.
if (!workflowData.id) {
if (!workflowId) {
// Manual executions do not trigger error workflows
// So this if should never happen. It was added to
// make sure there are no possible security gaps
return;
}
getWorkflowOwner(workflowData.id)
getWorkflowOwner(workflowId)
.then((user) => {
void WorkflowHelpers.executeErrorWorkflow(
workflowData.settings!.errorWorkflow as string,
@ -166,7 +164,7 @@ export function executeErrorWorkflow(
{
executionId,
errorWorkflowId: workflowData.settings!.errorWorkflow!.toString(),
workflowId: workflowData.id,
workflowId,
error,
workflowErrorData,
},
@ -174,16 +172,12 @@ export function executeErrorWorkflow(
});
} else if (
mode !== 'error' &&
workflowData.id !== undefined &&
workflowId !== undefined &&
workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)
) {
Logger.verbose('Start internal error workflow', { executionId, workflowId: workflowData.id });
void getWorkflowOwner(workflowData.id).then((user) => {
void WorkflowHelpers.executeErrorWorkflow(
workflowData.id!.toString(),
workflowErrorData,
user,
);
Logger.verbose('Start internal error workflow', { executionId, workflowId });
void getWorkflowOwner(workflowId).then((user) => {
void WorkflowHelpers.executeErrorWorkflow(workflowId, workflowErrorData, user);
});
}
}
@ -219,7 +213,7 @@ async function pruneExecutionData(this: WorkflowHooks): Promise<void> {
}, timeout * 1000);
// Mark binary data for deletion for all executions
await BinaryDataManager.getInstance().markDataForDeletionByExecutionIds(
executions.map(({ id }) => id.toString()),
executions.map(({ id }) => id),
);
} catch (error) {
ErrorReporter.error(error);
@ -505,7 +499,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
try {
if (
!isManualMode &&
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) &&
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id) &&
newStaticData
) {
// Workflow is saved so update in database
@ -593,17 +587,15 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
fullExecutionData.retryOf = this.retryOf.toString();
}
if (
this.workflowData.id !== undefined &&
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString())
) {
fullExecutionData.workflowId = this.workflowData.id.toString();
const workflowId = this.workflowData.id;
if (WorkflowHelpers.isWorkflowIdValid(workflowId)) {
fullExecutionData.workflowId = workflowId;
}
// 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}`, {
executionId: this.executionId,
workflowId: this.workflowData.id,
workflowId,
finished: fullExecutionData.finished,
stoppedAt: fullExecutionData.stoppedAt,
});
@ -685,7 +677,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
newStaticData: IDataObject,
): Promise<void> {
try {
if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) && newStaticData) {
if (WorkflowHelpers.isWorkflowIdValid(this.workflowData.id) && newStaticData) {
// Workflow is saved so update in database
try {
await WorkflowHelpers.saveStaticDataById(
@ -726,11 +718,9 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
fullExecutionData.retryOf = this.retryOf.toString();
}
if (
this.workflowData.id !== undefined &&
WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString())
) {
fullExecutionData.workflowId = this.workflowData.id.toString();
const workflowId = this.workflowData.id;
if (WorkflowHelpers.isWorkflowIdValid(workflowId)) {
fullExecutionData.workflowId = workflowId;
}
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
@ -844,12 +834,7 @@ export async function getWorkflowData(
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
workflowData = await WorkflowsService.get(
{ id: parseInt(workflowInfo.id, 10) },
{
relations,
},
);
workflowData = await WorkflowsService.get({ id: workflowInfo.id }, { relations });
if (workflowData === undefined) {
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`);
@ -1004,7 +989,7 @@ async function executeWorkflow(
workflowData,
};
if (workflowData.id) {
fullExecutionData.workflowId = workflowData.id as string;
fullExecutionData.workflowId = workflowData.id;
}
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
*/
export function isWorkflowIdValid(id: string | null | undefined | number): boolean {
if (typeof id === 'string') {
id = parseInt(id, 10);
}
// eslint-disable-next-line no-restricted-globals
if (isNaN(id as number)) {
return false;
}
return true;
export function isWorkflowIdValid(id: string | null | undefined): boolean {
return !(typeof id === 'string' && isNaN(parseInt(id, 10)));
}
/**
@ -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
try {
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:
// 1) Fetch the owner of the errored workflows and then
// 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!);
if (user.globalRole.name === 'owner') {
workflowData = await Db.collections.Workflow.findOne({ id: Number(workflowId) });
workflowData = await Db.collections.Workflow.findOne({ id: workflowId });
} else {
const sharedWorkflowData = await Db.collections.SharedWorkflow.findOne({
where: {
workflow: { id: workflowId },
user,
},
where: { workflowId, userId: user.id },
relations: ['workflow'],
});
if (sharedWorkflowData) {
@ -120,7 +109,7 @@ export async function executeErrorWorkflow(
}
}
} else {
workflowData = await Db.collections.Workflow.findOne({ id: Number(workflowId) });
workflowData = await Db.collections.Workflow.findOne({ id: workflowId });
}
if (workflowData === undefined) {
@ -250,11 +239,11 @@ export async function saveStaticData(workflow: Workflow): Promise<void> {
/**
* 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
*/
export async function saveStaticDataById(
workflowId: string | number,
workflowId: string,
newStaticData: IDataObject,
): Promise<void> {
await Db.collections.Workflow.update(workflowId, {
@ -265,10 +254,10 @@ export async function saveStaticDataById(
/**
* 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
export async function getStaticDataById(workflowId: string | number) {
export async function getStaticDataById(workflowId: string) {
const workflowData = await Db.collections.Workflow.findOne(workflowId, {
select: ['staticData'],
});
@ -329,7 +318,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
// if credential name-type combination is unique, use it
if (credentials?.length === 1) {
credentialsByName[nodeCredentialType][name] = {
id: credentials[0].id.toString(),
id: credentials[0].id,
name: credentials[0].name,
};
node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name];
@ -364,7 +353,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
});
if (credentials) {
credentialsById[nodeCredentialType][nodeCredentials.id] = {
id: credentials.id.toString(),
id: credentials.id,
name: credentials.name,
};
node.credentials[nodeCredentialType] =
@ -380,7 +369,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
if (credsByName?.length === 1) {
// add found credential to cache
credentialsById[nodeCredentialType][credsByName[0].id] = {
id: credsByName[0].id.toString(),
id: credsByName[0].id,
name: credsByName[0].name,
};
node.credentials[nodeCredentialType] =
@ -410,9 +399,10 @@ export async function getSharedWorkflowIds(user: User, roles?: string[]): Promis
const sharedWorkflows = await Db.collections.SharedWorkflow.find({
relations: ['workflow', 'role'],
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',
});
const ownedWorkflowsIds = await Db.collections.SharedWorkflow.find({
user,
role: workflowOwnerRole,
}).then((ownedWorkflows) => ownedWorkflows.map((wf) => wf.workflowId));
where: {
user,
role: workflowOwnerRole,
},
select: ['workflowId'],
}).then((ownedWorkflows) => ownedWorkflows.map(({ workflowId }) => workflowId));
if (ownedWorkflowsIds.length > 15) {
belowThreshold = false;
@ -538,7 +531,7 @@ export function validateWorkflowCredentialUsage(
* - 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(
newWorkflowVersion,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -128,7 +128,7 @@ export class ExportCredentialsCommand extends Command {
for (let i = 0; i < credentials.length; 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 plainData = credential.getData(encryptionKey);
(credentials[i] as ICredentialsDecryptedDb).data = plainData;

View file

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

View file

@ -35,13 +35,9 @@ EECredentialsController.get(
});
// eslint-disable-next-line @typescript-eslint/unbound-method
return allCredentials
.map((credential: CredentialsEntity & CredentialWithSharings) =>
EECredentials.addOwnerAndSharings(credential),
)
.map(
(credential): CredentialWithSharings => ({ ...credential, id: credential.id.toString() }),
);
return allCredentials.map((credential: CredentialsEntity & CredentialWithSharings) =>
EECredentials.addOwnerAndSharings(credential),
);
} catch (error) {
LoggerProxy.error('Request to list credentials failed', error as Error);
throw error;
@ -53,16 +49,12 @@ EECredentialsController.get(
* GET /credentials/:id
*/
EECredentialsController.get(
'/:id',
'/:id(\\d+)',
(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) => {
const { id: credentialId } = req.params;
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(
{ id: credentialId },
{ relations: ['shared', 'shared.role', 'shared.user'] },
@ -82,19 +74,12 @@ EECredentialsController.get(
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') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, data: _, ...rest } = credential;
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer
return { id: id.toString(), ...rest };
const { data: _, ...rest } = credential;
return { ...rest };
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, data: _, ...rest } = credential;
const { data: _, ...rest } = credential;
const key = await EECredentials.getEncryptionKey();
const decryptedData = EECredentials.redact(
@ -102,8 +87,7 @@ EECredentialsController.get(
credential,
);
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer
return { id: id.toString(), data: decryptedData, ...rest };
return { data: decryptedData, ...rest };
}),
);
@ -119,9 +103,10 @@ EECredentialsController.post(
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 (!sharing) {
throw new ResponseHelper.UnauthorizedError('Forbidden');
@ -161,7 +146,6 @@ EECredentialsController.put(
}
const { ownsCredential, credential } = await EECredentials.isOwned(req.user, credentialId);
if (!ownsCredential || !credential) {
throw new ResponseHelper.UnauthorizedError('Forbidden');
}
@ -191,7 +175,7 @@ EECredentialsController.put(
void InternalHooksManager.getInstance().onUserSharedCredentials({
credential_type: credential.type,
credential_id: credential.id.toString(),
credential_id: credential.id,
user_id_sharer: req.user.id,
user_ids_sharees_added: newShareeIds,
sharees_removed: amountRemoved,

View file

@ -11,7 +11,7 @@ import { getLogger } from '@/Logger';
import { EECredentialsController } from './credentials.controller.ee';
import { CredentialsService } from './credentials.service';
import type { ICredentialsResponse } from '@/Interfaces';
import type { ICredentialsDb } from '@/Interfaces';
import type { CredentialRequest } from '@/requests';
export const credentialsController = express.Router();
@ -35,14 +35,8 @@ credentialsController.use('/', EECredentialsController);
*/
credentialsController.get(
'/',
ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise<ICredentialsResponse[]> => {
const credentials = await 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;
});
ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise<ICredentialsDb[]> => {
return CredentialsService.getAll(req.user, { roles: ['owner'] });
}),
);
@ -69,15 +63,11 @@ credentialsController.get(
* GET /credentials/:id
*/
credentialsController.get(
'/:id',
'/:id(\\d+)',
ResponseHelper.send(async (req: CredentialRequest.Get) => {
const { id: credentialId } = req.params;
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']);
if (!sharing) {
@ -88,11 +78,10 @@ credentialsController.get(
const { credentials: credential } = sharing;
const { id, data: _, ...rest } = credential;
const { data: _, ...rest } = credential;
if (!includeDecryptedData) {
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer
return { id: id.toString(), ...rest };
return { ...rest };
}
const key = await CredentialsService.getEncryptionKey();
@ -101,8 +90,7 @@ credentialsController.get(
credential,
);
// @TODO_TECH_DEBT: Stringify `id` with entity field transformer
return { id: id.toString(), data: decryptedData, ...rest };
return { data: decryptedData, ...rest };
}),
);
@ -139,15 +127,15 @@ credentialsController.post(
const key = await CredentialsService.getEncryptionKey();
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({
credential_type: rest.type,
credential_id: id.toString(),
credential_type: credential.type,
credential_id: credential.id,
public_api: false,
});
return { id: id.toString(), ...rest };
return credential;
}),
);
@ -155,8 +143,8 @@ credentialsController.post(
* PATCH /credentials/:id
*/
credentialsController.patch(
'/:id',
ResponseHelper.send(async (req: CredentialRequest.Update): Promise<ICredentialsResponse> => {
'/:id(\\d+)',
ResponseHelper.send(async (req: CredentialRequest.Update): Promise<ICredentialsDb> => {
const { id: credentialId } = req.params;
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
const { id, data: _, ...rest } = responseData;
const { data: _, ...rest } = responseData;
LoggerProxy.verbose('Credential updated', { credentialId });
return {
id: id.toString(),
...rest,
};
return { ...rest };
}),
);
@ -209,7 +194,7 @@ credentialsController.patch(
* DELETE /credentials/:id
*/
credentialsController.delete(
'/:id',
'/:id(\\d+)',
ResponseHelper.send(async (req: CredentialRequest.Delete) => {
const { id: credentialId } = req.params;

View file

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

View file

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

View file

@ -1,14 +1,16 @@
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 type { SharedCredentials } from './SharedCredentials';
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import type { ICredentialsDb } from '@/Interfaces';
import { idStringifier } from '../utils/transformers';
@Entity()
export class CredentialsEntity extends AbstractEntity implements ICredentialsDb {
@PrimaryGeneratedColumn()
id: number;
@Generated()
@PrimaryColumn({ transformer: idStringifier })
id: string;
@Column({ length: 128 })
@IsString({ message: 'Credential `name` must be of type string.' })

View file

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

View file

@ -3,6 +3,7 @@ import type { CredentialsEntity } from './CredentialsEntity';
import type { User } from './User';
import type { Role } from './Role';
import { AbstractEntity } from './AbstractEntity';
import { idStringifier } from '../utils/transformers';
@Entity()
export class SharedCredentials extends AbstractEntity {
@ -22,7 +23,7 @@ export class SharedCredentials extends AbstractEntity {
})
credentials: CredentialsEntity;
@PrimaryColumn()
@PrimaryColumn({ transformer: idStringifier })
@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 { Role } from './Role';
import { AbstractEntity } from './AbstractEntity';
import { idStringifier } from '../utils/transformers';
@Entity()
export class SharedWorkflow extends AbstractEntity {
@ -22,7 +23,7 @@ export class SharedWorkflow extends AbstractEntity {
})
workflow: WorkflowEntity;
@PrimaryColumn()
@PrimaryColumn({ transformer: idStringifier })
@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 { IsString, Length } from 'class-validator';
import type { ITagDb } from '@/Interfaces';
import { idStringifier } from '../utils/transformers';
import type { WorkflowEntity } from './WorkflowEntity';
import { AbstractEntity } from './AbstractEntity';
@Entity()
export class TagEntity extends AbstractEntity implements ITagDb {
export class TagEntity extends AbstractEntity {
@Generated()
@PrimaryColumn({
transformer: idStringifier,
})
id: number;
@PrimaryColumn({ transformer: idStringifier })
id: string;
@Column({ length: 24 })
@Index({ unique: true })

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
// @ts-ignore
(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;
}
}
@ -79,7 +79,7 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
// @ts-ignore
(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;
}
}
@ -123,7 +123,7 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
// @ts-ignore
(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;
}
}

View file

@ -42,7 +42,7 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
// @ts-ignore
(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;
}
}
@ -85,7 +85,7 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
// @ts-ignore
(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;
}
}
@ -131,7 +131,7 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
// @ts-ignore
(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;
}
}

View file

@ -39,7 +39,7 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
// @ts-ignore
(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;
}
}
@ -82,7 +82,7 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
// @ts-ignore
(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;
}
}
@ -126,7 +126,7 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
// @ts-ignore
(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;
}
}

View file

@ -1,10 +1,13 @@
import { jsonParse } from 'n8n-workflow';
import { ValueTransformer } from 'typeorm';
import type { ValueTransformer, FindOperator } from 'typeorm';
import config from '@/config';
export const idStringifier = {
from: (value: number): string | number => (typeof value === 'number' ? value.toString() : value),
to: (value: string): number | string => (typeof value === 'string' ? Number(value) : value),
from: (value?: number): string | undefined => value?.toString(),
to: (
value: string | FindOperator<unknown> | undefined,
): number | FindOperator<unknown> | undefined =>
typeof value === 'string' ? Number(value) : value,
};
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 { InternalHooksManager } from '@/InternalHooksManager';
import { StatisticsNames } from '@/databases/entities/WorkflowStatistics';
@ -22,14 +22,8 @@ export async function workflowExecutionCompleted(
}
// Get the workflow id
let workflowId: number;
try {
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;
}
const workflowId = workflowData.id;
if (workflowId === undefined) return;
// Try insertion and if it fails due to key conflicts then update the existing entry instead
try {
@ -62,19 +56,9 @@ export async function workflowExecutionCompleted(
}
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
const response = await Db.collections.Workflow.update(
{ id, dataLoaded: false },
{ id: workflowId, dataLoaded: false },
{ dataLoaded: true },
);
@ -85,7 +69,7 @@ export async function nodeFetchedData(workflowId: string, node: INode): Promise<
const owner = await getWorkflowOwner(workflowId);
let metrics = {
user_id: owner.id,
workflow_id: id,
workflow_id: workflowId,
node_type: node.type,
node_id: node.id,
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { validate as jsonSchemaValidate } from 'jsonschema';
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 { v4 as uuid } from 'uuid';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
@ -16,7 +16,7 @@ import { validateEntity } from '@/GenericHelpers';
import { ExternalHooks } from '@/ExternalHooks';
import * as TagHelpers from '@/TagHelpers';
import { WorkflowRequest } from '@/requests';
import { IWorkflowDb, IWorkflowExecutionDataProcess, IWorkflowResponse } from '@/Interfaces';
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
import { WorkflowRunner } from '@/WorkflowRunner';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
@ -25,7 +25,7 @@ import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper';
export interface IGetWorkflowsQueryFilter {
id?: number | string;
id?: string;
name?: string;
active?: boolean;
}
@ -45,28 +45,20 @@ const allowedWorkflowsQueryFilterFields = Object.keys(schemaGetWorkflowsQueryFil
export class WorkflowsService {
static async getSharing(
user: User,
workflowId: number | string,
workflowId: string,
relations: string[] = ['workflow'],
{ allowGlobalOwner } = { allowGlobalOwner: true },
): Promise<SharedWorkflow | undefined> {
const options: FindOneOptions<SharedWorkflow> & { where: ObjectLiteral } = {
where: {
workflow: { id: workflowId },
},
};
const where: FindConditions<SharedWorkflow> = { workflowId };
// Omit user from where if the requesting user is the global
// owner. This allows the global owner to view and delete
// workflows they don't own.
if (!allowGlobalOwner || user.globalRole.name !== 'owner') {
options.where.user = { id: user.id };
where.userId = user.id;
}
if (relations?.length) {
options.relations = relations;
}
return Db.collections.SharedWorkflow.findOne(options);
return Db.collections.SharedWorkflow.findOne({ where, relations });
}
/**
@ -120,11 +112,6 @@ export class WorkflowsService {
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[]> {
const sharedWorkflowIds = await this.getWorkflowIdsForUser(user, ['owner']);
if (sharedWorkflowIds.length === 0) {
@ -181,16 +168,14 @@ export class WorkflowsService {
relations.push('shared', 'shared.user', 'shared.role');
}
const query: FindManyOptions<WorkflowEntity> = {
return Db.collections.Workflow.find({
select: isSharingEnabled() ? [...fields, 'versionId'] : fields,
relations,
where: {
id: In(sharedWorkflowIds),
...filter,
},
};
return Db.collections.Workflow.find(query);
});
}
static async update(
@ -310,17 +295,11 @@ export class WorkflowsService {
}
}
const options: FindManyOptions<WorkflowEntity> = {
relations: ['tags'],
};
if (config.getEnv('workflowTagsDisabled')) {
delete options.relations;
}
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
// We sadly get nothing back from "update". Neither if it updated a record
// 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) {
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);
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
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) => {
validateMainCredentialData(credential);
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);
expect(member1Credential.data).toBeUndefined();
expect(member1Credential.id).toBe(savedCredential1.id.toString());
expect(member1Credential.id).toBe(savedCredential1.id);
});
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 () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).get('/credentials/789');
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');
expect(responseAbc.statusCode).toBe(400);
expect(responseAbc.statusCode).toBe(404);
});
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);
const response = await authOwnerAgent.get(`/executions`).query({
workflowId: workflow.id.toString(),
workflowId: workflow.id,
});
expect(response.statusCode).toBe(200);
@ -490,7 +490,7 @@ test('GET /executions should retrieve all executions of specific workflow', asyn
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(workflow.id.toString());
expect(workflowId).toBe(workflow.id);
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
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 () => {
@ -690,7 +690,7 @@ test('POST /workflows/:id/activate should set workflow as active', async () => {
expect(sharedWorkflow?.workflow.active).toBe(true);
// 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 () => {
@ -744,7 +744,7 @@ test('POST /workflows/:id/activate should set non-owned workflow as active when
expect(sharedWorkflow?.workflow.active).toBe(true);
// 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 () => {
@ -835,7 +835,7 @@ test('POST /workflows/:id/deactivate should deactivate workflow', async () => {
// check whether the workflow is deactivated in the database
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 () => {
@ -888,7 +888,7 @@ test('POST /workflows/:id/deactivate should deactivate non-owned workflow when o
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 () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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