mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
feat: add saving new workflow endpoint (#4330) (no-changelog)
* feat: add saving new workflow endpoint
This commit is contained in:
parent
d4b74bd66a
commit
d45bc4999c
|
@ -202,6 +202,7 @@ export async function init(
|
|||
collections.Settings = linkRepository(entities.Settings);
|
||||
collections.InstalledPackages = linkRepository(entities.InstalledPackages);
|
||||
collections.InstalledNodes = linkRepository(entities.InstalledNodes);
|
||||
collections.CredentialUsage = linkRepository(entities.CredentialUsage);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import type { SharedWorkflow } from './databases/entities/SharedWorkflow';
|
|||
import type { TagEntity } from './databases/entities/TagEntity';
|
||||
import type { User } from './databases/entities/User';
|
||||
import type { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
import { CredentialUsage } from './databases/entities/CredentialUsage';
|
||||
|
||||
export interface IActivationError {
|
||||
time: number;
|
||||
|
@ -83,6 +84,7 @@ export interface IDatabaseCollections {
|
|||
Settings: Repository<Settings>;
|
||||
InstalledPackages: Repository<InstalledPackages>;
|
||||
InstalledNodes: Repository<InstalledNodes>;
|
||||
CredentialUsage: Repository<CredentialUsage>;
|
||||
}
|
||||
|
||||
export interface IWebhookDb {
|
||||
|
|
72
packages/cli/src/databases/entities/CredentialUsage.ts
Normal file
72
packages/cli/src/databases/entities/CredentialUsage.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import {
|
||||
BeforeUpdate,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
RelationId,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { IsDate, IsOptional } from 'class-validator';
|
||||
|
||||
import config = require('../../../config');
|
||||
import { DatabaseType } from '../../index';
|
||||
import { WorkflowEntity } from './WorkflowEntity';
|
||||
import { CredentialsEntity } from './CredentialsEntity';
|
||||
|
||||
function getTimestampSyntax() {
|
||||
const dbType = config.get('database.type') as DatabaseType;
|
||||
|
||||
const map: { [key in DatabaseType]: string } = {
|
||||
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
|
||||
postgresdb: 'CURRENT_TIMESTAMP(3)',
|
||||
mysqldb: 'CURRENT_TIMESTAMP(3)',
|
||||
mariadb: 'CURRENT_TIMESTAMP(3)',
|
||||
};
|
||||
|
||||
return map[dbType];
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class CredentialUsage {
|
||||
@ManyToOne(() => WorkflowEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
workflow: WorkflowEntity;
|
||||
|
||||
@ManyToOne(() => CredentialsEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
credential: CredentialsEntity;
|
||||
|
||||
@RelationId((credentialUsage: CredentialUsage) => credentialUsage.workflow)
|
||||
@PrimaryColumn()
|
||||
workflowId: number;
|
||||
|
||||
@PrimaryColumn()
|
||||
nodeId: string;
|
||||
|
||||
@RelationId((credentialUsage: CredentialUsage) => credentialUsage.credential)
|
||||
@PrimaryColumn()
|
||||
credentialId: string;
|
||||
|
||||
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
|
||||
@IsOptional() // ignored by validation because set at DB level
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({
|
||||
precision: 3,
|
||||
default: () => getTimestampSyntax(),
|
||||
onUpdate: getTimestampSyntax(),
|
||||
})
|
||||
@IsOptional() // ignored by validation because set at DB level
|
||||
@IsDate()
|
||||
updatedAt: Date;
|
||||
|
||||
@BeforeUpdate()
|
||||
setUpdateDate(): void {
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import { SharedWorkflow } from './SharedWorkflow';
|
|||
import { SharedCredentials } from './SharedCredentials';
|
||||
import { InstalledPackages } from './InstalledPackages';
|
||||
import { InstalledNodes } from './InstalledNodes';
|
||||
import { CredentialUsage } from './CredentialUsage';
|
||||
|
||||
export const entities = {
|
||||
CredentialsEntity,
|
||||
|
@ -26,4 +27,5 @@ export const entities = {
|
|||
SharedCredentials,
|
||||
InstalledPackages,
|
||||
InstalledNodes,
|
||||
CredentialUsage,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||
|
||||
export class CreateCredentialUsageTable1665484192213 implements MigrationInterface {
|
||||
name = 'CreateCredentialUsageTable1665484192213';
|
||||
|
||||
async up(queryRunner: QueryRunner) {
|
||||
logMigrationStart(this.name);
|
||||
const tablePrefix = getTablePrefix();
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE \`${tablePrefix}credential_usage\` (` +
|
||||
'`workflowId` int NOT NULL,' +
|
||||
'`nodeId` char(200) NOT NULL,' +
|
||||
"`credentialId` int NOT NULL DEFAULT '1'," +
|
||||
`\`createdAt\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,` +
|
||||
`\`updatedAt\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,` +
|
||||
'PRIMARY KEY (`workflowId`, `nodeId`, `credentialId`)' +
|
||||
") ENGINE='InnoDB';",
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`${tablePrefix}credential_usage\` ADD CONSTRAINT \`FK_${tablePrefix}518e1ece107b859ca6ce9ed2487f7e23\` FOREIGN KEY (\`workflowId\`) REFERENCES \`${tablePrefix}workflow_entity\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE \`${tablePrefix}credential_usage\` ADD CONSTRAINT \`FK_${tablePrefix}7ce200a20ade7ae89fa7901da896993f\` FOREIGN KEY (\`credentialId\`) REFERENCES \`${tablePrefix}credentials_entity\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
|
||||
logMigrationEnd(this.name);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner) {
|
||||
const tablePrefix = getTablePrefix();
|
||||
|
||||
await queryRunner.query(`
|
||||
DELETE FROM ${tablePrefix}role WHERE name='user' AND scope='workflow';
|
||||
`);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import { AddNodeIds1658932910559 } from './1658932910559-AddNodeIds';
|
|||
import { AddJsonKeyPinData1659895550980 } from './1659895550980-AddJsonKeyPinData';
|
||||
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
|
||||
import { CreateWorkflowsEditorRole1663755770894 } from './1663755770894-CreateWorkflowsEditorRole';
|
||||
import { CreateCredentialUsageTable1665484192213 } from './1665484192213-CreateCredentialUsageTable';
|
||||
|
||||
export const mysqlMigrations = [
|
||||
InitialMigration1588157391238,
|
||||
|
@ -46,4 +47,5 @@ export const mysqlMigrations = [
|
|||
AddJsonKeyPinData1659895550980,
|
||||
CreateCredentialsUserRole1660062385367,
|
||||
CreateWorkflowsEditorRole1663755770894,
|
||||
CreateCredentialUsageTable1665484192213,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||
|
||||
export class CreateCredentialUsageTable1665484192212 implements MigrationInterface {
|
||||
name = 'CreateCredentialUsageTable1665484192212';
|
||||
|
||||
async up(queryRunner: QueryRunner) {
|
||||
logMigrationStart(this.name);
|
||||
const tablePrefix = getTablePrefix();
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE ${tablePrefix}credential_usage (` +
|
||||
'"workflowId" int NOT NULL,' +
|
||||
'"nodeId" UUID NOT NULL,' +
|
||||
'"credentialId" int NULL,' +
|
||||
'"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
`CONSTRAINT "PK_${tablePrefix}feb7a6545aa714ac6e7f6b14825f0efc9353dd3a" PRIMARY KEY ("workflowId", "nodeId", "credentialId"), ` +
|
||||
`CONSTRAINT "FK_${tablePrefix}518e1ece107b859ca6ce9ed2487f7e23" FOREIGN KEY ("workflowId") REFERENCES ${tablePrefix}workflow_entity ("id") ON DELETE CASCADE ON UPDATE CASCADE, ` +
|
||||
`CONSTRAINT "FK_${tablePrefix}7ce200a20ade7ae89fa7901da896993f" FOREIGN KEY ("credentialId") REFERENCES ${tablePrefix}credentials_entity ("id") ON DELETE CASCADE ON UPDATE CASCADE ` +
|
||||
');',
|
||||
);
|
||||
|
||||
logMigrationEnd(this.name);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner) {
|
||||
const tablePrefix = getTablePrefix();
|
||||
|
||||
await queryRunner.query(`DROP TABLE "${tablePrefix}credential_usage"`);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import { AddNodeIds1658932090381 } from './1658932090381-AddNodeIds';
|
|||
import { AddJsonKeyPinData1659902242948 } from './1659902242948-AddJsonKeyPinData';
|
||||
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
|
||||
import { CreateWorkflowsEditorRole1663755770893 } from './1663755770893-CreateWorkflowsEditorRole';
|
||||
import { CreateCredentialUsageTable1665484192212 } from './1665484192212-CreateCredentialUsageTable';
|
||||
|
||||
export const postgresMigrations = [
|
||||
InitialMigration1587669153312,
|
||||
|
@ -42,4 +43,5 @@ export const postgresMigrations = [
|
|||
AddNodeIds1658932090381,
|
||||
AddJsonKeyPinData1659902242948,
|
||||
CreateWorkflowsEditorRole1663755770893,
|
||||
CreateCredentialUsageTable1665484192212,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||
|
||||
export class CreateCredentialUsageTable1665484192211 implements MigrationInterface {
|
||||
name = 'CreateCredentialUsageTable1665484192211';
|
||||
|
||||
async up(queryRunner: QueryRunner) {
|
||||
logMigrationStart(this.name);
|
||||
|
||||
const tablePrefix = getTablePrefix();
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "${tablePrefix}credential_usage" (` +
|
||||
`"workflowId" integer NOT NULL,` +
|
||||
`"nodeId" varchar NOT NULL,` +
|
||||
`"credentialId" integer NOT NULL,` +
|
||||
`"createdAt" datetime(3) NOT NULL DEFAULT 'STRFTIME(''%Y-%m-%d %H:%M:%f'', ''NOW'')',` +
|
||||
`"updatedAt" datetime(3) NOT NULL DEFAULT 'STRFTIME(''%Y-%m-%d %H:%M:%f'', ''NOW'')',` +
|
||||
`PRIMARY KEY("workflowId", "nodeId", "credentialId"), ` +
|
||||
`CONSTRAINT "FK_${tablePrefix}518e1ece107b859ca6ce9ed2487f7e23" FOREIGN KEY ("workflowId") REFERENCES "${tablePrefix}workflow_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, ` +
|
||||
`CONSTRAINT "FK_${tablePrefix}7ce200a20ade7ae89fa7901da896993f" FOREIGN KEY ("credentialId") REFERENCES "${tablePrefix}credentials_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION ` +
|
||||
`);`,
|
||||
);
|
||||
|
||||
logMigrationEnd(this.name);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner) {
|
||||
const tablePrefix = getTablePrefix();
|
||||
|
||||
await queryRunner.query(`DROP TABLE "${tablePrefix}credential_usage"`);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import { AddNodeIds1658930531669 } from './1658930531669-AddNodeIds';
|
|||
import { AddJsonKeyPinData1659888469333 } from './1659888469333-AddJsonKeyPinData';
|
||||
import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCredentialsUserRole';
|
||||
import { CreateWorkflowsEditorRole1663755770892 } from './1663755770892-CreateWorkflowsUserRole';
|
||||
import { CreateCredentialUsageTable1665484192211 } from './1665484192211-CreateCredentialUsageTable';
|
||||
|
||||
const sqliteMigrations = [
|
||||
InitialMigration1588102412422,
|
||||
|
@ -40,6 +41,7 @@ const sqliteMigrations = [
|
|||
AddJsonKeyPinData1659888469333,
|
||||
CreateCredentialsUserRole1660062385367,
|
||||
CreateWorkflowsEditorRole1663755770892,
|
||||
CreateCredentialUsageTable1665484192211,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import express from 'express';
|
||||
import { Db, ResponseHelper } from '..';
|
||||
import { Db, InternalHooksManager, ResponseHelper, WorkflowHelpers } from '..';
|
||||
import config from '../../config';
|
||||
import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
|
||||
import { validateEntity } from '../GenericHelpers';
|
||||
import type { WorkflowRequest } from '../requests';
|
||||
import { isSharingEnabled, rightDiff } from '../UserManagement/UserManagementHelper';
|
||||
import { EEWorkflowsService as EEWorkflows } from './workflows.services.ee';
|
||||
import { externalHooks } from '../Server';
|
||||
import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import * as TagHelpers from '../TagHelpers';
|
||||
import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee';
|
||||
import { CredentialUsage } from '../databases/entities/CredentialUsage';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const EEWorkflowController = express.Router();
|
||||
|
@ -60,15 +68,10 @@ EEWorkflowController.put('/:workflowId/share', async (req: WorkflowRequest.Share
|
|||
});
|
||||
|
||||
EEWorkflowController.get(
|
||||
'/:id',
|
||||
(req: WorkflowRequest.Get, res, next) => (req.params.id === 'new' ? next('router') : next()), // skip ee router and use free one for naming
|
||||
'/:id(\\d+)',
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Get) => {
|
||||
const { id: workflowId } = req.params;
|
||||
|
||||
if (Number.isNaN(Number(workflowId))) {
|
||||
throw new ResponseHelper.ResponseError(`Workflow ID must be a number.`, undefined, 400);
|
||||
}
|
||||
|
||||
const workflow = await EEWorkflows.get(
|
||||
{ id: parseInt(workflowId, 10) },
|
||||
{ relations: ['shared', 'shared.user', 'shared.role'] },
|
||||
|
@ -92,3 +95,110 @@ EEWorkflowController.get(
|
|||
return EEWorkflows.addOwnerAndSharings(workflow);
|
||||
}),
|
||||
);
|
||||
|
||||
EEWorkflowController.post(
|
||||
'/',
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Create) => {
|
||||
delete req.body.id; // delete if sent
|
||||
|
||||
const newWorkflow = new WorkflowEntity();
|
||||
|
||||
Object.assign(newWorkflow, req.body);
|
||||
|
||||
await validateEntity(newWorkflow);
|
||||
|
||||
await externalHooks.run('workflow.create', [newWorkflow]);
|
||||
|
||||
const { tags: tagIds } = req.body;
|
||||
|
||||
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
|
||||
newWorkflow.tags = await Db.collections.Tag.findByIds(tagIds, {
|
||||
select: ['id', 'name'],
|
||||
});
|
||||
}
|
||||
|
||||
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);
|
||||
|
||||
WorkflowHelpers.addNodeIds(newWorkflow);
|
||||
|
||||
// This is a new workflow, so we simply check if the user has access to
|
||||
// all used workflows
|
||||
|
||||
const allCredentials = await EECredentials.getAll(req.user);
|
||||
|
||||
try {
|
||||
EEWorkflows.validateCredentialPermissionsToUser(newWorkflow, allCredentials);
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
'The workflow contains credentials that you do not have access to',
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
let savedWorkflow: undefined | WorkflowEntity;
|
||||
|
||||
await Db.transaction(async (transactionManager) => {
|
||||
savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
|
||||
|
||||
const role = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'workflow',
|
||||
});
|
||||
|
||||
const newSharedWorkflow = new SharedWorkflow();
|
||||
|
||||
Object.assign(newSharedWorkflow, {
|
||||
role,
|
||||
user: req.user,
|
||||
workflow: savedWorkflow,
|
||||
});
|
||||
|
||||
await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
|
||||
|
||||
const credentialUsage: CredentialUsage[] = [];
|
||||
newWorkflow.nodes.forEach((node) => {
|
||||
if (!node.credentials) {
|
||||
return;
|
||||
}
|
||||
Object.keys(node.credentials).forEach((credentialType) => {
|
||||
const credentialId = node.credentials?.[credentialType].id;
|
||||
if (credentialId) {
|
||||
const newCredentialusage = new CredentialUsage();
|
||||
Object.assign(newCredentialusage, {
|
||||
credentialId,
|
||||
nodeId: node.id,
|
||||
workflowId: savedWorkflow?.id,
|
||||
});
|
||||
credentialUsage.push(newCredentialusage);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (credentialUsage.length) {
|
||||
await transactionManager.save<CredentialUsage>(credentialUsage);
|
||||
}
|
||||
});
|
||||
|
||||
if (!savedWorkflow) {
|
||||
LoggerProxy.error('Failed to create workflow', { userId: req.user.id });
|
||||
throw new ResponseHelper.ResponseError('Failed to save workflow');
|
||||
}
|
||||
|
||||
if (tagIds && !config.getEnv('workflowTagsDisabled') && savedWorkflow.tags) {
|
||||
savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, {
|
||||
requestOrder: tagIds,
|
||||
});
|
||||
}
|
||||
|
||||
await externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
|
||||
|
||||
const { id, ...rest } = savedWorkflow;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -279,7 +279,7 @@ workflowsController.get(
|
|||
* GET /workflows/:id
|
||||
*/
|
||||
workflowsController.get(
|
||||
'/:id',
|
||||
'/:id(\\d+)',
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Get) => {
|
||||
const { id: workflowId } = req.params;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { DeleteResult, EntityManager, In, Not } from 'typeorm';
|
||||
import { Db } from '..';
|
||||
import { Db, ICredentialsDb } from '..';
|
||||
import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
|
||||
import { User } from '../databases/entities/User';
|
||||
import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
|
||||
|
@ -94,4 +94,24 @@ export class EEWorkflowsService extends WorkflowsService {
|
|||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
static validateCredentialPermissionsToUser(
|
||||
workflow: WorkflowEntity,
|
||||
allowedCredentials: ICredentialsDb[],
|
||||
) {
|
||||
workflow.nodes.forEach((node) => {
|
||||
if (!node.credentials) {
|
||||
return;
|
||||
}
|
||||
Object.keys(node.credentials).forEach((credentialType) => {
|
||||
const credentialId = parseInt(node.credentials?.[credentialType].id ?? '', 10);
|
||||
const matchedCredential = allowedCredentials.find(
|
||||
(credential) => credential.id === credentialId,
|
||||
);
|
||||
if (!matchedCredential) {
|
||||
throw new Error('The workflow contains credentials that you do not have access to');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -681,6 +681,18 @@ export async function getWorkflowSharing(workflow: WorkflowEntity) {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// credential usage
|
||||
// ----------------------------------
|
||||
|
||||
export async function getCredentialUsageInWorkflow(workflowId: number) {
|
||||
return Db.collections.CredentialUsage.find({
|
||||
where: {
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// connection options
|
||||
// ----------------------------------
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
ICredentialType,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeTypeData,
|
||||
|
@ -65,6 +66,8 @@ import type {
|
|||
InstalledPackagePayload,
|
||||
PostgresSchemaSection,
|
||||
} from './types';
|
||||
import { WorkflowEntity } from '../../../src/databases/entities/WorkflowEntity';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* Initialize a test server.
|
||||
|
@ -698,3 +701,45 @@ export const emptyPackage = () => {
|
|||
|
||||
return Promise.resolve(installedPackage);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// workflow
|
||||
// ----------------------------------
|
||||
|
||||
export function makeWorkflow({
|
||||
withPinData,
|
||||
withCredential,
|
||||
}: {
|
||||
withPinData: boolean;
|
||||
withCredential?: { id: string; name: string };
|
||||
}) {
|
||||
const workflow = new WorkflowEntity();
|
||||
|
||||
const node: INode = {
|
||||
id: uuid(),
|
||||
name: 'Spotify',
|
||||
type: 'n8n-nodes-base.spotify',
|
||||
parameters: { resource: 'track', operation: 'get', id: '123' },
|
||||
typeVersion: 1,
|
||||
position: [740, 240],
|
||||
};
|
||||
|
||||
if (withCredential) {
|
||||
node.credentials = {
|
||||
spotifyApi: withCredential,
|
||||
};
|
||||
}
|
||||
|
||||
workflow.name = 'My Workflow';
|
||||
workflow.active = false;
|
||||
workflow.connections = {};
|
||||
workflow.nodes = [node];
|
||||
|
||||
if (withPinData) {
|
||||
workflow.pinData = MOCK_PINDATA;
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
export const MOCK_PINDATA = { Spotify: [{ json: { myKey: 'myValue' } }] };
|
||||
|
|
|
@ -8,7 +8,9 @@ import { v4 as uuid } from 'uuid';
|
|||
|
||||
import type { Role } from '../../src/databases/entities/Role';
|
||||
import config from '../../config';
|
||||
import type { AuthAgent } from './shared/types';
|
||||
import type { AuthAgent, SaveCredentialFunction } from './shared/types';
|
||||
import { makeWorkflow } from './shared/utils';
|
||||
import { randomCredentialPayload } from './shared/random';
|
||||
|
||||
jest.mock('../../src/telemetry');
|
||||
|
||||
|
@ -20,7 +22,9 @@ let testDbName = '';
|
|||
|
||||
let globalOwnerRole: Role;
|
||||
let globalMemberRole: Role;
|
||||
let credentialOwnerRole: Role;
|
||||
let authAgent: AuthAgent;
|
||||
let saveCredential: SaveCredentialFunction;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await utils.initTestServer({
|
||||
|
@ -32,6 +36,9 @@ beforeAll(async () => {
|
|||
|
||||
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||
globalMemberRole = await testDb.getGlobalMemberRole();
|
||||
credentialOwnerRole = await testDb.getCredentialOwnerRole();
|
||||
|
||||
saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole);
|
||||
|
||||
authAgent = utils.createAuthAgent(app);
|
||||
|
||||
|
@ -123,21 +130,27 @@ describe('PUT /workflows/:id', () => {
|
|||
});
|
||||
|
||||
describe('GET /workflows/:id', () => {
|
||||
test('GET should fail with invalid id', async () => {
|
||||
test('GET should fail with invalid id due to route rule', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||
|
||||
const response = await authOwnerAgent.get('/workflows/potatoes');
|
||||
const response = await authAgent(owner).get('/workflows/potatoes');
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('GET should return 404 for non existing workflow', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
|
||||
const response = await authAgent(owner).get('/workflows/9001');
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('GET should return a workflow with owner', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||
|
||||
const response = await authOwnerAgent.get(`/workflows/${workflow.id}`);
|
||||
const response = await authAgent(owner).get(`/workflows/${workflow.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.ownedBy).toMatchObject({
|
||||
|
@ -154,10 +167,9 @@ describe('GET /workflows/:id', () => {
|
|||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||
await testDb.shareWorkflowWithUsers(workflow, [member]);
|
||||
|
||||
const response = await authOwnerAgent.get(`/workflows/${workflow.id}`);
|
||||
const response = await authAgent(owner).get(`/workflows/${workflow.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.ownedBy).toMatchObject({
|
||||
|
@ -181,10 +193,9 @@ describe('GET /workflows/:id', () => {
|
|||
const member1 = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const member2 = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||
await testDb.shareWorkflowWithUsers(workflow, [member1, member2]);
|
||||
|
||||
const response = await authOwnerAgent.get(`/workflows/${workflow.id}`);
|
||||
const response = await authAgent(owner).get(`/workflows/${workflow.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.ownedBy).toMatchObject({
|
||||
|
@ -197,3 +208,90 @@ describe('GET /workflows/:id', () => {
|
|||
expect(response.body.data.sharedWith).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /workflows', () => {
|
||||
it('Should create a workflow that uses no credential', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
|
||||
const workflow = makeWorkflow({ withPinData: false });
|
||||
|
||||
const response = await authAgent(owner).post('/workflows').send(workflow);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const usedCredentials = await testDb.getCredentialUsageInWorkflow(response.body.data.id);
|
||||
expect(usedCredentials).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('Should save credential usage when saving a new workflow', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
|
||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||
const workflow = makeWorkflow({
|
||||
withPinData: false,
|
||||
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name },
|
||||
});
|
||||
|
||||
const response = await authAgent(owner).post('/workflows').send(workflow);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const usedCredentials = await testDb.getCredentialUsageInWorkflow(response.body.data.id);
|
||||
expect(usedCredentials).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Should not allow saving a workflow using credential you have no access', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
// Credential belongs to owner, member cannot use it.
|
||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||
const workflow = makeWorkflow({
|
||||
withPinData: false,
|
||||
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name },
|
||||
});
|
||||
|
||||
const response = await authAgent(member).post('/workflows').send(workflow);
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.message).toBe(
|
||||
'The workflow contains credentials that you do not have access to',
|
||||
);
|
||||
});
|
||||
|
||||
it('Should allow owner to save a workflow using credential owned by others', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
// Credential belongs to owner, member cannot use it.
|
||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
|
||||
const workflow = makeWorkflow({
|
||||
withPinData: false,
|
||||
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name },
|
||||
});
|
||||
|
||||
const response = await authAgent(owner).post('/workflows').send(workflow);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const usedCredentials = await testDb.getCredentialUsageInWorkflow(response.body.data.id);
|
||||
expect(usedCredentials).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Should allow saving a workflow using a credential owned by others and shared with you', async () => {
|
||||
const member1 = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const member2 = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
|
||||
await testDb.shareCredentialWithUsers(savedCredential, [member2]);
|
||||
|
||||
const workflow = makeWorkflow({
|
||||
withPinData: false,
|
||||
withCredential: { id: savedCredential.id.toString(), name: savedCredential.name },
|
||||
});
|
||||
|
||||
const response = await authAgent(member2).post('/workflows').send(workflow);
|
||||
expect(response.statusCode).toBe(200);
|
||||
const usedCredentials = await testDb.getCredentialUsageInWorkflow(response.body.data.id);
|
||||
expect(usedCredentials).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import * as UserManagementHelpers from '../../src/UserManagement/UserManagementH
|
|||
|
||||
import type { Role } from '../../src/databases/entities/Role';
|
||||
import type { IPinData } from 'n8n-workflow';
|
||||
import { makeWorkflow, MOCK_PINDATA } from './shared/utils';
|
||||
|
||||
jest.mock('../../src/telemetry');
|
||||
|
||||
|
@ -87,29 +88,3 @@ test('GET /workflows/:id should return pin data', async () => {
|
|||
|
||||
expect(pinData).toMatchObject(MOCK_PINDATA);
|
||||
});
|
||||
|
||||
function makeWorkflow({ withPinData }: { withPinData: boolean }) {
|
||||
const workflow = new WorkflowEntity();
|
||||
|
||||
workflow.name = 'My Workflow';
|
||||
workflow.active = false;
|
||||
workflow.connections = {};
|
||||
workflow.nodes = [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'Spotify',
|
||||
type: 'n8n-nodes-base.spotify',
|
||||
parameters: { resource: 'track', operation: 'get', id: '123' },
|
||||
typeVersion: 1,
|
||||
position: [740, 240],
|
||||
},
|
||||
];
|
||||
|
||||
if (withPinData) {
|
||||
workflow.pinData = MOCK_PINDATA;
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
const MOCK_PINDATA = { Spotify: [{ json: { myKey: 'myValue' } }] };
|
||||
|
|
Loading…
Reference in a new issue