From 6cbeb5d78e69e7398ca8d49d86f1c08684fd7b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 21 Dec 2023 14:52:42 +0100 Subject: [PATCH 1/5] fix(core): Handle empty executions table in pruning in migrations (#8121) In case someone manually prunes their executions table before upgrading to 1.x, `MigrateIntegerKeysToString` should gracefully handle that, instead of crashing the application. ## Review / Merge checklist - [x] PR title and summary are descriptive --- ...690000000002-MigrateIntegerKeysToString.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts index 48dfc3ad46f57..83b55787b2f51 100644 --- a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts +++ b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts @@ -208,20 +208,24 @@ const pruneExecutionsData = async ({ queryRunner, tablePrefix, logger }: Migrati } console.time('pruningData'); - const counting = (await queryRunner.query( - `select count(id) as rows from "${tablePrefix}execution_entity";`, - )) as Array<{ rows: number }>; - - const averageExecutionSize = dbFileSize / counting[0].rows; - const numberOfExecutionsToKeep = Math.floor(DESIRED_DATABASE_FILE_SIZE / averageExecutionSize); - - const query = `SELECT id FROM "${tablePrefix}execution_entity" ORDER BY id DESC limit ${numberOfExecutionsToKeep}, 1`; - const idToKeep = await queryRunner - .query(query) - .then((rows: Array<{ id: number }>) => rows[0].id); - - const removalQuery = `DELETE FROM "${tablePrefix}execution_entity" WHERE id < ${idToKeep} and status IN ('success')`; - await queryRunner.query(removalQuery); + const [{ rowCount }] = (await queryRunner.query( + `select count(id) as rowCount from "${tablePrefix}execution_entity";`, + )) as Array<{ rowCount: number }>; + + if (rowCount > 0) { + const averageExecutionSize = dbFileSize / rowCount; + const numberOfExecutionsToKeep = Math.floor( + DESIRED_DATABASE_FILE_SIZE / averageExecutionSize, + ); + + const query = `SELECT id FROM "${tablePrefix}execution_entity" ORDER BY id DESC limit ${numberOfExecutionsToKeep}, 1`; + const idToKeep = await queryRunner + .query(query) + .then((rows: Array<{ id: number }>) => rows[0].id); + + const removalQuery = `DELETE FROM "${tablePrefix}execution_entity" WHERE id < ${idToKeep} and status IN ('success')`; + await queryRunner.query(removalQuery); + } console.timeEnd('pruningData'); } else { logger.debug('Pruning was requested, but was not enabled'); From fd27f738447011b5346e04d6d1d4fddf14d589ba Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 21 Dec 2023 09:06:54 -0500 Subject: [PATCH 2/5] fix: Show public API upgrade CTA when feature is not enabled (#8109) ## Summary > Describe what the PR does and how to test. Photos and videos are recommended. Shows the public API upgrade CTA when the feature is not enabled. Now trialing users in cloud would see the API on the settings menu and can upgrade from there. When public API feature disabled: image When public API feature enabled with no API key: image When public API feature enabled with API key: image ## Related tickets and issues [> Include links to **Linear ticket** or Github issue or Community forum post. Important in order to close *automatically* and provide context to r](https://linear.app/n8n/issue/ADO-1282/feature-api-page-missing-for-trial-users)eviewers. ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --- cypress/e2e/27-cloud.cy.ts | 119 ++++++++++++++++++ cypress/e2e/27-opt-in-trial-banner.cy.ts | 83 ------------ cypress/fixtures/Plan_data_opt_in_trial.json | 3 +- cypress/pages/index.ts | 1 + cypress/pages/settings-public-api.ts | 5 + packages/editor-ui/src/router.ts | 8 +- .../editor-ui/src/views/SettingsApiView.vue | 8 +- 7 files changed, 134 insertions(+), 93 deletions(-) create mode 100644 cypress/e2e/27-cloud.cy.ts delete mode 100644 cypress/e2e/27-opt-in-trial-banner.cy.ts create mode 100644 cypress/pages/settings-public-api.ts diff --git a/cypress/e2e/27-cloud.cy.ts b/cypress/e2e/27-cloud.cy.ts new file mode 100644 index 0000000000000..965bc5bccfedf --- /dev/null +++ b/cypress/e2e/27-cloud.cy.ts @@ -0,0 +1,119 @@ +import { + BannerStack, + MainSidebar, + WorkflowPage, + visitPublicApiPage, + getPublicApiUpgradeCTA, +} from '../pages'; +import planData from '../fixtures/Plan_data_opt_in_trial.json'; +import { INSTANCE_OWNER } from '../constants'; + +const mainSidebar = new MainSidebar(); +const bannerStack = new BannerStack(); +const workflowPage = new WorkflowPage(); + +describe('Cloud', { disableAutoLogin: true }, () => { + before(() => { + const now = new Date(); + const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); + planData.expirationDate = fiveDaysFromNow.toJSON(); + }); + + describe('BannerStack', () => { + it('should render trial banner for opt-in cloud user', () => { + cy.intercept('GET', '/rest/admin/cloud-plan', { + body: planData, + }).as('getPlanData'); + + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + cy.wait('@getPlanData'); + + bannerStack.getters.banner().should('be.visible'); + + mainSidebar.actions.signout(); + + bannerStack.getters.banner().should('not.be.visible'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + bannerStack.getters.banner().should('be.visible'); + + mainSidebar.actions.signout(); + }); + + it('should not render opt-in-trial banner for non cloud deployment', () => { + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { ...res.body.data, deployment: { type: 'default' } }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + bannerStack.getters.banner().should('not.be.visible'); + + mainSidebar.actions.signout(); + }); + }); + + describe('Admin Home', () => { + it('Should show admin button', () => { + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + cy.visit(workflowPage.url); + + mainSidebar.getters.adminPanel().should('be.visible'); + }); + }); + + describe('Public API', () => { + it('Should show upgrade CTA for Public API if user is trialing', () => { + cy.intercept('GET', '/rest/admin/cloud-plan', { + body: planData, + }).as('getPlanData'); + + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { + ...res.body.data, + deployment: { type: 'cloud' }, + n8nMetadata: { userId: 1 }, + }, + }); + }); + }).as('loadSettings'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + visitPublicApiPage(); + + getPublicApiUpgradeCTA().should('be.visible'); + }); + }); +}); diff --git a/cypress/e2e/27-opt-in-trial-banner.cy.ts b/cypress/e2e/27-opt-in-trial-banner.cy.ts deleted file mode 100644 index 6e24343bc770a..0000000000000 --- a/cypress/e2e/27-opt-in-trial-banner.cy.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { BannerStack, MainSidebar, WorkflowPage } from '../pages'; -import planData from '../fixtures/Plan_data_opt_in_trial.json'; -import { INSTANCE_OWNER } from '../constants'; - -const mainSidebar = new MainSidebar(); -const bannerStack = new BannerStack(); -const workflowPage = new WorkflowPage(); - -describe('BannerStack', { disableAutoLogin: true }, () => { - before(() => { - const now = new Date(); - const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); - planData.expirationDate = fiveDaysFromNow.toJSON(); - }); - - it('should render trial banner for opt-in cloud user', () => { - cy.intercept('GET', '/rest/admin/cloud-plan', { - body: planData, - }).as('getPlanData'); - - cy.intercept('GET', '/rest/settings', (req) => { - req.on('response', (res) => { - res.send({ - data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, - }); - }); - }).as('loadSettings'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - cy.wait('@getPlanData'); - - bannerStack.getters.banner().should('be.visible'); - - mainSidebar.actions.signout(); - - bannerStack.getters.banner().should('not.be.visible'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - bannerStack.getters.banner().should('be.visible'); - - mainSidebar.actions.signout(); - }); - - it('should not render opt-in-trial banner for non cloud deployment', () => { - cy.intercept('GET', '/rest/settings', (req) => { - req.on('response', (res) => { - res.send({ - data: { ...res.body.data, deployment: { type: 'default' } }, - }); - }); - }).as('loadSettings'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - bannerStack.getters.banner().should('not.be.visible'); - - mainSidebar.actions.signout(); - }); - - it('Should show admin button', () => { - cy.intercept('GET', '/rest/settings', (req) => { - req.on('response', (res) => { - res.send({ - data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } }, - }); - }); - }).as('loadSettings'); - - cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); - - cy.visit(workflowPage.url); - - mainSidebar.getters.adminPanel().should('be.visible'); - }); -}); diff --git a/cypress/fixtures/Plan_data_opt_in_trial.json b/cypress/fixtures/Plan_data_opt_in_trial.json index 504805de320e1..7a805708c651d 100644 --- a/cypress/fixtures/Plan_data_opt_in_trial.json +++ b/cypress/fixtures/Plan_data_opt_in_trial.json @@ -13,8 +13,7 @@ "feat:advancedExecutionFilters": true, "quota:users": -1, "quota:maxVariables": -1, - "feat:variables": true, - "feat:apiDisabled": true + "feat:variables": true }, "metadata": { "version": "v1", diff --git a/cypress/pages/index.ts b/cypress/pages/index.ts index 6f03962c2ac13..39c9be3b5648b 100644 --- a/cypress/pages/index.ts +++ b/cypress/pages/index.ts @@ -12,3 +12,4 @@ export * from './workflow-executions-tab'; export * from './signin'; export * from './workflow-history'; export * from './workerView'; +export * from './settings-public-api'; diff --git a/cypress/pages/settings-public-api.ts b/cypress/pages/settings-public-api.ts new file mode 100644 index 0000000000000..1a7d668136775 --- /dev/null +++ b/cypress/pages/settings-public-api.ts @@ -0,0 +1,5 @@ +export const getPublicApiUpgradeCTA = () => cy.getByTestId('public-api-upgrade-cta'); + +export const visitPublicApiPage = () => { + cy.visit('/settings/api'); +}; diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index eac3900d0d1e3..b9f9d1c180507 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -517,13 +517,7 @@ export const routes = [ settingsView: SettingsApiView, }, meta: { - middleware: ['authenticated', 'custom'], - middlewareOptions: { - custom: () => { - const settingsStore = useSettingsStore(); - return settingsStore.isPublicApiEnabled; - }, - }, + middleware: ['authenticated'], telemetry: { pageCategory: 'settings', getProperties(route: RouteLocation) { diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue index d09b1d4c4c046..2f6c33f6b1408 100644 --- a/packages/editor-ui/src/views/SettingsApiView.vue +++ b/packages/editor-ui/src/views/SettingsApiView.vue @@ -64,7 +64,8 @@ Date: Thu, 21 Dec 2023 17:37:08 +0100 Subject: [PATCH 3/5] fix(core): Remove circular dependency in WorkflowService and ActiveWorkflowRunner (#8128) A circular dependency between `WorkflowService` and `ActiveWorkflowRunner` is sometimes causing `this.activeWorkflowRunner` to be `undefined` in `WorkflowService`. Breaking this circular dependency should hopefully fix this issue. - [x] PR title and summary are descriptive - [ ] Tests included --- packages/cli/src/ActiveWorkflowRunner.ts | 23 ++++--- packages/cli/src/WebhookHelpers.ts | 4 -- .../cli/src/WorkflowExecuteAdditionalData.ts | 12 ++-- .../repositories/workflow.repository.ts | 27 +++++++- .../src/executions/executions.service.ee.ts | 4 +- .../cli/src/workflows/workflow.service.ee.ts | 4 +- .../cli/src/workflows/workflow.service.ts | 67 ++----------------- .../workflows/workflowStaticData.service.ts | 41 ++++++++++++ .../src/workflows/workflows.controller.ee.ts | 3 +- 9 files changed, 99 insertions(+), 86 deletions(-) create mode 100644 packages/cli/src/workflows/workflowStaticData.service.ts diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 5b4a15f40c39a..61be101f142a6 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ - -import { Container, Service } from 'typedi'; +import { Service } from 'typedi'; import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core'; import type { @@ -59,7 +58,6 @@ import { NodeTypes } from '@/NodeTypes'; import { WorkflowRunner } from '@/WorkflowRunner'; import { ExternalHooks } from '@/ExternalHooks'; import { whereClause } from './UserManagement/UserManagementHelper'; -import { WorkflowService } from './workflows/workflow.service'; import { WebhookNotFoundError } from './errors/response-errors/webhook-not-found.error'; import { In } from 'typeorm'; import { WebhookService } from './services/webhook.service'; @@ -70,6 +68,7 @@ import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee' import { ActivationErrorsService } from '@/ActivationErrors.service'; import type { Scope } from '@n8n/permissions'; import { NotFoundError } from './errors/response-errors/not-found.error'; +import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; @Service() export class ActiveWorkflowRunner implements IWebhookManager { @@ -94,6 +93,8 @@ export class ActiveWorkflowRunner implements IWebhookManager { private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly multiMainSetup: MultiMainSetup, private readonly activationErrorsService: ActivationErrorsService, + private readonly executionService: ExecutionsService, + private readonly workflowStaticDataService: WorkflowStaticDataService, ) {} async init() { @@ -213,10 +214,12 @@ export class ActiveWorkflowRunner implements IWebhookManager { undefined, request, response, - (error: Error | null, data: object) => { + async (error: Error | null, data: object) => { if (error !== null) { return reject(error); } + // Save static data if it changed + await this.workflowStaticDataService.saveStaticData(workflow); resolve(data); }, ); @@ -412,7 +415,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { } await this.webhookService.populateCache(); - await Container.get(WorkflowService).saveStaticData(workflow); + await this.workflowStaticDataService.saveStaticData(workflow); } /** @@ -451,7 +454,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { await workflow.deleteWebhook(webhookData, NodeExecuteFunctions, mode, 'update'); } - await Container.get(WorkflowService).saveStaticData(workflow); + await this.workflowStaticDataService.saveStaticData(workflow); await this.webhookService.deleteWorkflowWebhooks(workflowId); } @@ -524,7 +527,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { donePromise?: IDeferredPromise, ): void => { this.logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`); - void Container.get(WorkflowService).saveStaticData(workflow); + void this.workflowStaticDataService.saveStaticData(workflow); const executePromise = this.runWorkflow( workflowData, node, @@ -579,7 +582,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { donePromise?: IDeferredPromise, ): void => { this.logger.debug(`Received trigger for workflow "${workflow.name}"`); - void Container.get(WorkflowService).saveStaticData(workflow); + void this.workflowStaticDataService.saveStaticData(workflow); const executePromise = this.runWorkflow( workflowData, @@ -814,7 +817,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { await this.activationErrorsService.unset(workflowId); const triggerCount = this.countTriggers(workflow, additionalData); - await Container.get(WorkflowService).updateWorkflowTriggerCount(workflow.id, triggerCount); + await this.workflowRepository.updateWorkflowTriggerCount(workflow.id, triggerCount); } catch (e) { const error = e instanceof Error ? e : new Error(`${e}`); await this.activationErrorsService.set(workflowId, error.message); @@ -824,7 +827,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { // If for example webhooks get created it sometimes has to save the // id of them in the static data. So make sure that data gets persisted. - await Container.get(WorkflowService).saveStaticData(workflow); + await this.workflowStaticDataService.saveStaticData(workflow); } /** diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 0446d5a92b6fa..f218ec11c1b7a 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -60,7 +60,6 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { EventsService } from '@/services/events.service'; import { OwnershipService } from './services/ownership.service'; import { parseBody } from './middlewares'; -import { WorkflowService } from './workflows/workflow.service'; import { Logger } from './Logger'; import { NotFoundError } from './errors/response-errors/not-found.error'; import { InternalServerError } from './errors/response-errors/internal-server.error'; @@ -386,9 +385,6 @@ export async function executeWebhook( }; } - // Save static data if it changed - await Container.get(WorkflowService).saveStaticData(workflow); - const additionalKeys: IWorkflowDataProxyAdditionalKeys = { $executionId: executionId, }; diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 1f6bcc627b4e9..e81d97098273f 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -52,7 +52,6 @@ import * as WebhookHelpers from '@/WebhookHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers'; import { findSubworkflowStart, isWorkflowIdValid } from '@/utils'; import { PermissionChecker } from './UserManagement/PermissionChecker'; -import { WorkflowService } from './workflows/workflow.service'; import { InternalHooks } from '@/InternalHooks'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { EventsService } from '@/services/events.service'; @@ -67,6 +66,8 @@ import { restoreBinaryDataId } from './executionLifecycleHooks/restoreBinaryData import { toSaveSettings } from './executionLifecycleHooks/toSaveSettings'; import { Logger } from './Logger'; import { saveExecutionProgress } from './executionLifecycleHooks/saveExecutionProgress'; +import { WorkflowStaticDataService } from './workflows/workflowStaticData.service'; +import { WorkflowRepository } from './databases/repositories/workflow.repository'; const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); @@ -418,7 +419,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) { // Workflow is saved so update in database try { - await Container.get(WorkflowService).saveStaticDataById( + await Container.get(WorkflowStaticDataService).saveStaticDataById( this.workflowData.id as string, newStaticData, ); @@ -564,7 +565,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { if (isWorkflowIdValid(this.workflowData.id) && newStaticData) { // Workflow is saved so update in database try { - await Container.get(WorkflowService).saveStaticDataById( + await Container.get(WorkflowStaticDataService).saveStaticDataById( this.workflowData.id as string, newStaticData, ); @@ -714,7 +715,10 @@ export async function getWorkflowData( if (workflowInfo.id !== undefined) { const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags']; - workflowData = await Container.get(WorkflowService).get({ id: workflowInfo.id }, { relations }); + workflowData = await Container.get(WorkflowRepository).get( + { id: workflowInfo.id }, + { relations }, + ); if (workflowData === undefined || workflowData === null) { throw new ApplicationError('Workflow does not exist.', { diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index d5b193ff2678c..990b2b829cb45 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -1,5 +1,6 @@ import { Service } from 'typedi'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, Repository, type UpdateResult, type FindOptionsWhere } from 'typeorm'; +import config from '@/config'; import { WorkflowEntity } from '../entities/WorkflowEntity'; @Service() @@ -8,6 +9,13 @@ export class WorkflowRepository extends Repository { super(WorkflowEntity, dataSource.manager); } + async get(where: FindOptionsWhere, options?: { relations: string[] }) { + return this.findOne({ + where, + relations: options?.relations, + }); + } + async getAllActive() { return this.find({ where: { active: true }, @@ -28,4 +36,21 @@ export class WorkflowRepository extends Repository { }); return totalTriggerCount ?? 0; } + + async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise { + const qb = this.createQueryBuilder('workflow'); + return qb + .update() + .set({ + triggerCount, + updatedAt: () => { + if (['mysqldb', 'mariadb'].includes(config.getEnv('database.type'))) { + return 'updatedAt'; + } + return '"updatedAt"'; + }, + }) + .where('id = :id', { id }) + .execute(); + } } diff --git a/packages/cli/src/executions/executions.service.ee.ts b/packages/cli/src/executions/executions.service.ee.ts index 9fbfa8ca71919..662ce3875948f 100644 --- a/packages/cli/src/executions/executions.service.ee.ts +++ b/packages/cli/src/executions/executions.service.ee.ts @@ -6,7 +6,7 @@ import type { IExecutionResponse, IExecutionFlattedResponse } from '@/Interfaces import { EnterpriseWorkflowService } from '../workflows/workflow.service.ee'; import type { WorkflowWithSharingsAndCredentials } from '@/workflows/workflows.types'; import Container from 'typedi'; -import { WorkflowService } from '@/workflows/workflow.service'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; export class EEExecutionsService extends ExecutionsService { /** @@ -26,7 +26,7 @@ export class EEExecutionsService extends ExecutionsService { const relations = ['shared', 'shared.user', 'shared.role']; - const workflow = (await Container.get(WorkflowService).get( + const workflow = (await Container.get(WorkflowRepository).get( { id: execution.workflowId }, { relations }, )) as WorkflowWithSharingsAndCredentials; diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 57c2e88ea8c23..6431a3654c326 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -18,6 +18,7 @@ import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; @Service() export class EnterpriseWorkflowService { @@ -26,6 +27,7 @@ export class EnterpriseWorkflowService { private readonly userService: UserService, private readonly roleService: RoleService, private readonly sharedWorkflowRepository: SharedWorkflowRepository, + private readonly workflowRepository: WorkflowRepository, ) {} async isOwned( @@ -182,7 +184,7 @@ export class EnterpriseWorkflowService { } async preventTampering(workflow: WorkflowEntity, workflowId: string, user: User) { - const previousVersion = await this.workflowService.get({ id: workflowId }); + const previousVersion = await this.workflowRepository.get({ id: workflowId }); if (!previousVersion) { throw new NotFoundError('Workflow not found'); diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index d4854d123dff0..fb2380ccdbf7a 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -1,7 +1,7 @@ import Container, { Service } from 'typedi'; -import type { IDataObject, INode, IPinData } from 'n8n-workflow'; -import { NodeApiError, ErrorReporterProxy as ErrorReporter, Workflow } from 'n8n-workflow'; -import type { FindManyOptions, FindOptionsSelect, FindOptionsWhere, UpdateResult } from 'typeorm'; +import type { INode, IPinData } from 'n8n-workflow'; +import { NodeApiError, Workflow } from 'n8n-workflow'; +import type { FindManyOptions, FindOptionsSelect, FindOptionsWhere } from 'typeorm'; import { In, Like } from 'typeorm'; import pick from 'lodash/pick'; import omit from 'lodash/omit'; @@ -25,7 +25,7 @@ import { whereClause } from '@/UserManagement/UserManagementHelper'; import { InternalHooks } from '@/InternalHooks'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { OwnershipService } from '@/services/ownership.service'; -import { isStringArray, isWorkflowIdValid } from '@/utils'; +import { isStringArray } from '@/utils'; import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; import { BinaryDataService } from 'n8n-core'; import type { Scope } from '@n8n/permissions'; @@ -120,13 +120,6 @@ export class WorkflowService { return pinnedTriggers.find((pt) => pt.name === checkNodeName) ?? null; // partial execution } - async get(workflow: FindOptionsWhere, options?: { relations: string[] }) { - return this.workflowRepository.findOne({ - where: workflow, - relations: options?.relations, - }); - } - async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) { if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; @@ -512,56 +505,4 @@ export class WorkflowService { return sharedWorkflow.workflow; } - - async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise { - const qb = this.workflowRepository.createQueryBuilder('workflow'); - return qb - .update() - .set({ - triggerCount, - updatedAt: () => { - if (['mysqldb', 'mariadb'].includes(config.getEnv('database.type'))) { - return 'updatedAt'; - } - return '"updatedAt"'; - }, - }) - .where('id = :id', { id }) - .execute(); - } - - /** - * Saves the static data if it changed - */ - async saveStaticData(workflow: Workflow): Promise { - if (workflow.staticData.__dataChanged === true) { - // Static data of workflow changed and so has to be saved - if (isWorkflowIdValid(workflow.id)) { - // Workflow is saved so update in database - try { - await this.saveStaticDataById(workflow.id, workflow.staticData); - workflow.staticData.__dataChanged = false; - } catch (error) { - ErrorReporter.error(error); - this.logger.error( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - `There was a problem saving the workflow with id "${workflow.id}" to save changed Data: "${error.message}"`, - { workflowId: workflow.id }, - ); - } - } - } - } - - /** - * Saves the given static data on workflow - * - * @param {(string)} workflowId The id of the workflow to save data on - * @param {IDataObject} newStaticData The static data to save - */ - async saveStaticDataById(workflowId: string, newStaticData: IDataObject): Promise { - await this.workflowRepository.update(workflowId, { - staticData: newStaticData, - }); - } } diff --git a/packages/cli/src/workflows/workflowStaticData.service.ts b/packages/cli/src/workflows/workflowStaticData.service.ts new file mode 100644 index 0000000000000..b569c69a3002d --- /dev/null +++ b/packages/cli/src/workflows/workflowStaticData.service.ts @@ -0,0 +1,41 @@ +import { Service } from 'typedi'; +import { type IDataObject, type Workflow, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; +import { Logger } from '@/Logger'; +import { WorkflowRepository } from '@db/repositories/workflow.repository'; +import { isWorkflowIdValid } from '@/utils'; + +@Service() +export class WorkflowStaticDataService { + constructor( + private readonly logger: Logger, + private readonly workflowRepository: WorkflowRepository, + ) {} + + /** Saves the static data if it changed */ + async saveStaticData(workflow: Workflow): Promise { + if (workflow.staticData.__dataChanged === true) { + // Static data of workflow changed and so has to be saved + if (isWorkflowIdValid(workflow.id)) { + // Workflow is saved so update in database + try { + await this.saveStaticDataById(workflow.id, workflow.staticData); + workflow.staticData.__dataChanged = false; + } catch (error) { + ErrorReporter.error(error); + this.logger.error( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + `There was a problem saving the workflow with id "${workflow.id}" to save changed Data: "${error.message}"`, + { workflowId: workflow.id }, + ); + } + } + } + } + + /** Saves the given static data on workflow */ + async saveStaticDataById(workflowId: string, newStaticData: IDataObject): Promise { + await this.workflowRepository.update(workflowId, { + staticData: newStaticData, + }); + } +} diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts index 900c8d3660249..27a3abc0cfff2 100644 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ b/packages/cli/src/workflows/workflows.controller.ee.ts @@ -28,6 +28,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { WorkflowService } from './workflow.service'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; export const EEWorkflowController = express.Router(); @@ -129,7 +130,7 @@ EEWorkflowController.get( relations.push('tags'); } - const workflow = await Container.get(WorkflowService).get({ id: workflowId }, { relations }); + const workflow = await Container.get(WorkflowRepository).get({ id: workflowId }, { relations }); if (!workflow) { throw new NotFoundError(`Workflow with ID "${workflowId}" does not exist`); From 95ea2f65bf672f9f5aed56d049adf3a504231f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 21 Dec 2023 14:15:46 +0100 Subject: [PATCH 4/5] refactor(core): Move error execution creation to execution service (no-changelog) (#8006) Continue breaking down legacy helpers. Note: `getUserById` is unused. --- packages/cli/src/ActiveWorkflowRunner.ts | 10 +- packages/cli/src/GenericHelpers.ts | 91 ------------------- .../UserManagement/UserManagementHelper.ts | 9 -- .../src/credentials/credentials.service.ts | 5 +- .../cli/src/executions/executions.service.ts | 86 +++++++++++++++++- .../integration/ActiveWorkflowRunner.test.ts | 2 + packages/cli/test/integration/auth.mw.test.ts | 4 + .../integration/publicApi/workflows.test.ts | 2 + .../test/integration/shared/utils/index.ts | 2 + .../cli/test/integration/users.api.test.ts | 4 + 10 files changed, 105 insertions(+), 110 deletions(-) diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 61be101f142a6..bb037590c674e 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -48,7 +48,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData' import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { ActiveExecutions } from '@/ActiveExecutions'; -import { createErrorExecution } from '@/GenericHelpers'; +import { ExecutionsService } from './executions/executions.service'; import { STARTING_NODES, WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, @@ -550,9 +550,11 @@ export class ActiveWorkflowRunner implements IWebhookManager { }; returnFunctions.__emitError = (error: ExecutionError): void => { - void createErrorExecution(error, node, workflowData, workflow, mode).then(() => { - this.executeErrorWorkflow(error, workflowData, mode); - }); + void this.executionService + .createErrorExecution(error, node, workflowData, workflow, mode) + .then(() => { + this.executeErrorWorkflow(error, workflowData, mode); + }); }; return returnFunctions; }; diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index 4d62c05d622f8..4645535332e1e 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -1,21 +1,11 @@ import type express from 'express'; -import type { - ExecutionError, - INode, - IRunExecutionData, - Workflow, - WorkflowExecuteMode, -} from 'n8n-workflow'; import { validate } from 'class-validator'; -import { Container } from 'typedi'; import config from '@/config'; -import type { ExecutionPayload, IWorkflowDb } from '@/Interfaces'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; import type { UserUpdatePayload } from '@/requests'; -import { ExecutionRepository } from '@db/repositories/execution.repository'; import { BadRequestError } from './errors/response-errors/bad-request.error'; /** @@ -58,85 +48,4 @@ export async function validateEntity( } } -/** - * Create an error execution - * - * @param {INode} node - * @param {IWorkflowDb} workflowData - * @param {Workflow} workflow - * @param {WorkflowExecuteMode} mode - * @returns - * @memberof ActiveWorkflowRunner - */ - -export async function createErrorExecution( - error: ExecutionError, - node: INode, - workflowData: IWorkflowDb, - workflow: Workflow, - mode: WorkflowExecuteMode, -): Promise { - const saveDataErrorExecutionDisabled = workflowData?.settings?.saveDataErrorExecution === 'none'; - - if (saveDataErrorExecutionDisabled) return; - - const executionData: IRunExecutionData = { - startData: { - destinationNode: node.name, - runNodeFilter: [node.name], - }, - executionData: { - contextData: {}, - metadata: {}, - nodeExecutionStack: [ - { - node, - data: { - main: [ - [ - { - json: {}, - pairedItem: { - item: 0, - }, - }, - ], - ], - }, - source: null, - }, - ], - waitingExecution: {}, - waitingExecutionSource: {}, - }, - resultData: { - runData: { - [node.name]: [ - { - startTime: 0, - executionTime: 0, - error, - source: [], - }, - ], - }, - error, - lastNodeExecuted: node.name, - }, - }; - - const fullExecutionData: ExecutionPayload = { - data: executionData, - mode, - finished: false, - startedAt: new Date(), - workflowData, - workflowId: workflow.id, - stoppedAt: new Date(), - status: 'error', - }; - - await Container.get(ExecutionRepository).createNewExecution(fullExecutionData); -} - export const DEFAULT_EXECUTIONS_GET_ALL_LIMIT = 20; diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 6b9b1a0eafec6..2a1cdd38fdf39 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -6,7 +6,6 @@ import type { User } from '@db/entities/User'; import config from '@/config'; import { License } from '@/License'; import { getWebhookBaseUrl } from '@/WebhookHelpers'; -import { UserRepository } from '@db/repositories/user.repository'; import type { Scope } from '@n8n/permissions'; export function isSharingEnabled(): boolean { @@ -26,14 +25,6 @@ export function generateUserInviteUrl(inviterId: string, inviteeId: string): str return `${getInstanceBaseUrl()}/signup?inviterId=${inviterId}&inviteeId=${inviteeId}`; } -export async function getUserById(userId: string): Promise { - const user = await Container.get(UserRepository).findOneOrFail({ - where: { id: userId }, - relations: ['globalRole'], - }); - return user; -} - // return the difference between two arrays export function rightDiff( [arr1, keyExtractor1]: [T1[], (item: T1) => string], diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index ab5e2379770ea..f75f1eccd6982 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -35,10 +35,7 @@ export type CredentialsGetSharedOptions = | { allowGlobalScope: false }; export class CredentialsService { - static async get( - where: FindOptionsWhere, - options?: { relations: string[] }, - ): Promise { + static async get(where: FindOptionsWhere, options?: { relations: string[] }) { return Container.get(CredentialsRepository).findOne({ relations: options?.relations, where, diff --git a/packages/cli/src/executions/executions.service.ts b/packages/cli/src/executions/executions.service.ts index 6790636346ddd..4e2d23ec20723 100644 --- a/packages/cli/src/executions/executions.service.ts +++ b/packages/cli/src/executions/executions.service.ts @@ -1,5 +1,13 @@ import { validate as jsonSchemaValidate } from 'jsonschema'; -import type { IWorkflowBase, JsonObject, ExecutionStatus } from 'n8n-workflow'; +import type { + IWorkflowBase, + JsonObject, + ExecutionStatus, + ExecutionError, + INode, + IRunExecutionData, + WorkflowExecuteMode, +} from 'n8n-workflow'; import { ApplicationError, jsonParse, Workflow, WorkflowOperationError } from 'n8n-workflow'; import type { FindOperator } from 'typeorm'; import { In } from 'typeorm'; @@ -7,9 +15,11 @@ import { ActiveExecutions } from '@/ActiveExecutions'; import config from '@/config'; import type { User } from '@db/entities/User'; import type { + ExecutionPayload, IExecutionFlattedResponse, IExecutionResponse, IExecutionsListResponse, + IWorkflowDb, IWorkflowExecutionDataProcess, } from '@/Interfaces'; import { NodeTypes } from '@/NodeTypes'; @@ -18,7 +28,7 @@ import type { ExecutionRequest } from '@/requests'; import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { WorkflowRunner } from '@/WorkflowRunner'; import * as GenericHelpers from '@/GenericHelpers'; -import { Container } from 'typedi'; +import { Container, Service } from 'typedi'; import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; @@ -75,6 +85,7 @@ const schemaGetExecutionsQueryFilter = { const allowedExecutionsQueryFilterFields = Object.keys(schemaGetExecutionsQueryFilter.properties); +@Service() export class ExecutionsService { /** * Function to get the workflow Ids for a User @@ -362,4 +373,75 @@ export class ExecutionsService { }, ); } + + async createErrorExecution( + error: ExecutionError, + node: INode, + workflowData: IWorkflowDb, + workflow: Workflow, + mode: WorkflowExecuteMode, + ): Promise { + const saveDataErrorExecutionDisabled = + workflowData?.settings?.saveDataErrorExecution === 'none'; + + if (saveDataErrorExecutionDisabled) return; + + const executionData: IRunExecutionData = { + startData: { + destinationNode: node.name, + runNodeFilter: [node.name], + }, + executionData: { + contextData: {}, + metadata: {}, + nodeExecutionStack: [ + { + node, + data: { + main: [ + [ + { + json: {}, + pairedItem: { + item: 0, + }, + }, + ], + ], + }, + source: null, + }, + ], + waitingExecution: {}, + waitingExecutionSource: {}, + }, + resultData: { + runData: { + [node.name]: [ + { + startTime: 0, + executionTime: 0, + error, + source: [], + }, + ], + }, + error, + lastNodeExecuted: node.name, + }, + }; + + const fullExecutionData: ExecutionPayload = { + data: executionData, + mode, + finished: false, + startedAt: new Date(), + workflowData, + workflowId: workflow.id, + stoppedAt: new Date(), + status: 'error', + }; + + await Container.get(ExecutionRepository).createNewExecution(fullExecutionData); + } } diff --git a/packages/cli/test/integration/ActiveWorkflowRunner.test.ts b/packages/cli/test/integration/ActiveWorkflowRunner.test.ts index 07a147a0ebd28..d7adb25edeb57 100644 --- a/packages/cli/test/integration/ActiveWorkflowRunner.test.ts +++ b/packages/cli/test/integration/ActiveWorkflowRunner.test.ts @@ -24,12 +24,14 @@ import { setSchedulerAsLoadedNode } from './shared/utils'; import * as testDb from './shared/testDb'; import { createOwner } from './shared/db/users'; import { createWorkflow } from './shared/db/workflows'; +import { ExecutionsService } from '@/executions/executions.service'; import { WorkflowService } from '@/workflows/workflow.service'; mockInstance(ActiveExecutions); mockInstance(ActiveWorkflows); mockInstance(Push); mockInstance(SecretsHelper); +mockInstance(ExecutionsService); mockInstance(WorkflowService); const webhookService = mockInstance(WebhookService); diff --git a/packages/cli/test/integration/auth.mw.test.ts b/packages/cli/test/integration/auth.mw.test.ts index 8cc77968a123f..f958a630d8fee 100644 --- a/packages/cli/test/integration/auth.mw.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -2,8 +2,12 @@ import type { SuperAgentTest } from 'supertest'; import * as utils from './shared/utils/'; import { getGlobalMemberRole } from './shared/db/roles'; import { createUser } from './shared/db/users'; +import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import { mockInstance } from '../shared/mocking'; describe('Auth Middleware', () => { + mockInstance(ActiveWorkflowRunner); + const testServer = utils.setupTestServer({ endpointGroups: ['me', 'auth', 'owner', 'users', 'invitations'], }); diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index e4de396a129f1..47b0c2c15fe1b 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -18,6 +18,7 @@ import { createWorkflow, createWorkflowWithTrigger } from '../shared/db/workflow import { createTag } from '../shared/db/tags'; import { mockInstance } from '../../shared/mocking'; import { Push } from '@/push'; +import { ExecutionsService } from '@/executions/executions.service'; let workflowOwnerRole: Role; let owner: User; @@ -30,6 +31,7 @@ const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] }); const license = testServer.license; mockInstance(Push); +mockInstance(ExecutionsService); beforeAll(async () => { const [globalOwnerRole, globalMemberRole, fetchedWorkflowOwnerRole] = await getAllRoles(); diff --git a/packages/cli/test/integration/shared/utils/index.ts b/packages/cli/test/integration/shared/utils/index.ts index f2b9d4e5e33e3..63fcd48621104 100644 --- a/packages/cli/test/integration/shared/utils/index.ts +++ b/packages/cli/test/integration/shared/utils/index.ts @@ -18,6 +18,7 @@ import { SettingsRepository } from '@db/repositories/settings.repository'; import { mockNodeTypesData } from '../../../unit/Helpers'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { mockInstance } from '../../../shared/mocking'; +import { ExecutionsService } from '@/executions/executions.service'; export { setupTestServer } from './testServer'; @@ -31,6 +32,7 @@ export { setupTestServer } from './testServer'; export async function initActiveWorkflowRunner() { mockInstance(MultiMainSetup); + mockInstance(ExecutionsService); const { ActiveWorkflowRunner } = await import('@/ActiveWorkflowRunner'); const workflowRunner = Container.get(ActiveWorkflowRunner); await workflowRunner.init(); diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 4e18f13d85834..2ca6bb5350bd7 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -18,6 +18,10 @@ import * as testDb from './shared/testDb'; import type { SuperAgentTest } from 'supertest'; import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; +import { ExecutionsService } from '@/executions/executions.service'; +import { mockInstance } from '../shared/mocking'; + +mockInstance(ExecutionsService); const testServer = utils.setupTestServer({ endpointGroups: ['users'], From 675d4b68e4579232574c5097fd778ebdfa93dbf3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:08:04 +0100 Subject: [PATCH 5/5] :rocket: Release 1.22.1 (#8130) ## [1.22.1](https://github.com/n8n-io/n8n/compare/n8n@1.22.0...n8n@1.22.1) (2023-12-21) ### Bug Fixes * **core:** Handle empty executions table in pruning in migrations ([#8121](https://github.com/n8n-io/n8n/issues/8121)) ([6cbeb5d](https://github.com/n8n-io/n8n/commit/6cbeb5d78e69e7398ca8d49d86f1c08684fd7b35)) * **core:** Remove circular dependency in WorkflowService and ActiveWorkflowRunner ([#8128](https://github.com/n8n-io/n8n/issues/8128)) ([b8e72c4](https://github.com/n8n-io/n8n/commit/b8e72c4377b759315c77a134d4791b073f2ff5fa)) * **editor**: Show public API upgrade CTA when feature is not enabled ([#8109](https://github.com/n8n-io/n8n/issues/8109)) ([fd27f73](https://github.com/n8n-io/n8n/commit/fd27f738447011b5346e04d6d1d4fddf14d589ba)) Co-authored-by: ivov --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- packages/cli/package.json | 2 +- packages/editor-ui/package.json | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2033351b7ab4f..186e22783b441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [1.22.1](https://github.com/n8n-io/n8n/compare/n8n@1.22.0...n8n@1.22.1) (2023-12-21) + + +### Bug Fixes + +* **core:** Handle empty executions table in pruning in migrations ([#8121](https://github.com/n8n-io/n8n/issues/8121)) ([6cbeb5d](https://github.com/n8n-io/n8n/commit/6cbeb5d78e69e7398ca8d49d86f1c08684fd7b35)) +* **core:** Remove circular dependency in WorkflowService and ActiveWorkflowRunner ([#8128](https://github.com/n8n-io/n8n/issues/8128)) ([b8e72c4](https://github.com/n8n-io/n8n/commit/b8e72c4377b759315c77a134d4791b073f2ff5fa)) +* Show public API upgrade CTA when feature is not enabled ([#8109](https://github.com/n8n-io/n8n/issues/8109)) ([fd27f73](https://github.com/n8n-io/n8n/commit/fd27f738447011b5346e04d6d1d4fddf14d589ba)) + + + # [1.22.0](https://github.com/n8n-io/n8n/compare/n8n@1.21.0...n8n@1.22.0) (2023-12-21) diff --git a/package.json b/package.json index 90cee43f90170..1e276fa7553a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.22.0", + "version": "1.22.1", "private": true, "homepage": "https://n8n.io", "engines": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 6e6f8de24a874..477a10cbdc10e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.22.0", + "version": "1.22.1", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 7cd132b5d0a01..7d66db3ebb0fb 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "1.22.0", + "version": "1.22.1", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io",