diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 7702fbff92..c3548a0cda 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -128,19 +128,19 @@ jobs: - name: Trigger a release note run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{ needs.publish-to-npm.outputs.release }}"}' - merge-back-into-master: - name: Merge back into master - needs: [publish-to-npm, create-github-release] - if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.1 - with: - fetch-depth: 0 - - run: | - git checkout --track origin/master - git config user.name "github-actions[bot]" - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }} - git push origin master - git push origin :${{github.event.pull_request.base.ref}} + # merge-back-into-master: + # name: Merge back into master + # needs: [publish-to-npm, create-github-release] + # if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }} + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4.1.1 + # with: + # fetch-depth: 0 + # - run: | + # git checkout --track origin/master + # git config user.name "github-actions[bot]" + # git config user.email 41898282+github-actions[bot]@users.noreply.github.com + # git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }} + # git push origin master + # git push origin :${{github.event.pull_request.base.ref}} diff --git a/CHANGELOG.md b/CHANGELOG.md index fba31697f5..219c7b1726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +# [1.61.0](https://github.com/n8n-io/n8n/compare/n8n@1.60.0...n8n@1.61.0) (2024-09-25) + + +### Bug Fixes + +* **core:** Add executionData to expressions in pagination code ([#10926](https://github.com/n8n-io/n8n/issues/10926)) ([eac103e](https://github.com/n8n-io/n8n/commit/eac103e367d59a532b9ba12db78a0dd10aee62fb)) +* **core:** Fix webhook binary data max size configuration ([#10897](https://github.com/n8n-io/n8n/issues/10897)) ([693fb7e](https://github.com/n8n-io/n8n/commit/693fb7e580b7e030c86977bff6d319bbee4fcd62)) +* **core:** Remove subworkflow license check ([#10893](https://github.com/n8n-io/n8n/issues/10893)) ([0290e38](https://github.com/n8n-io/n8n/commit/0290e38f990275074eb7e7ccd0b41f1ae0215dd2)) +* **editor:** Credentials scopes and n8n scopes mix up ([#10930](https://github.com/n8n-io/n8n/issues/10930)) ([e069608](https://github.com/n8n-io/n8n/commit/e0696080227aee7ccb50d51a82873e8a1ba4667d)) +* **editor:** Fix design system form component sizing ([#10961](https://github.com/n8n-io/n8n/issues/10961)) ([cf153ea](https://github.com/n8n-io/n8n/commit/cf153ea085165115ee523fbb1bd32080dde47eda)) +* **editor:** Fix modal overflow when AI is enabled in code node ([#10887](https://github.com/n8n-io/n8n/issues/10887)) ([f9f303f](https://github.com/n8n-io/n8n/commit/f9f303f562084db8c8956da267680b1f935aa2df)) +* **editor:** Fix source control push modal checkboxes ([#10910](https://github.com/n8n-io/n8n/issues/10910)) ([8db8817](https://github.com/n8n-io/n8n/commit/8db88178511749b19a5878816ef062092fd9f2be)) +* **editor:** Fix styling and typography in AI Assistant chat ([#10895](https://github.com/n8n-io/n8n/issues/10895)) ([57ff3cc](https://github.com/n8n-io/n8n/commit/57ff3cc27b9470bfbe2486c3c1831c57f5a4075f)) +* **editor:** Prevent clipboard xss injection ([#10894](https://github.com/n8n-io/n8n/issues/10894)) ([e20ab59](https://github.com/n8n-io/n8n/commit/e20ab59c1dcf9da19a30268ce19930bfa7e38992)) +* **editor:** Prevent node name input in NDV to expand unnecessarily ([#10922](https://github.com/n8n-io/n8n/issues/10922)) ([a2237d1](https://github.com/n8n-io/n8n/commit/a2237d128ff6a4d65cd30325b6b9d9b765ca7be6)) +* **editor:** Update gird size when opening credentials support chat ([#10882](https://github.com/n8n-io/n8n/issues/10882)) ([b86fd80](https://github.com/n8n-io/n8n/commit/b86fd80fc9fe06011367ca04a75e4b52533db1fe)) +* **editor:** Use `:focus-visible` instead for `:focus` for buttons ([#10921](https://github.com/n8n-io/n8n/issues/10921)) ([bf28d09](https://github.com/n8n-io/n8n/commit/bf28d0965c46620a106c87037bafd2cf936f1050)) +* **editor:** Use correct output for connected nodes in schema view ([#10928](https://github.com/n8n-io/n8n/issues/10928)) ([ad60d49](https://github.com/n8n-io/n8n/commit/ad60d49b4251138a7c69cb5e9f00c3ef875486e0)) +* Enable Assistant on other credential views ([#10931](https://github.com/n8n-io/n8n/issues/10931)) ([557db9c](https://github.com/n8n-io/n8n/commit/557db9c170a89447ec9cc14aa1af51e5fd11dd92)) +* Ensure user id for early track events ([#10885](https://github.com/n8n-io/n8n/issues/10885)) ([23c09ea](https://github.com/n8n-io/n8n/commit/23c09eae4223545c717270a5cd305d2e57e1ad5b)) +* **Google Sheets Node:** Insert data if sheet is empty instead of error ([#10942](https://github.com/n8n-io/n8n/issues/10942)) ([c75990e](https://github.com/n8n-io/n8n/commit/c75990e0632c581384542610a886ef89621a9403)) +* Hide assistant button when showing Click to connect ([#10932](https://github.com/n8n-io/n8n/issues/10932)) ([d74cff2](https://github.com/n8n-io/n8n/commit/d74cff20301f285588f93207f29660d25fdbc8da)) +* **HTTP Request Node:** Do not modify request object when sanitizing message for UI ([#10923](https://github.com/n8n-io/n8n/issues/10923)) ([8cc10cc](https://github.com/n8n-io/n8n/commit/8cc10cc2c1869b9abcafd157e41be65ce2b6f499)) +* **MQTT Node:** Close connection if connection attempt fails ([#10873](https://github.com/n8n-io/n8n/issues/10873)) ([ee7147c](https://github.com/n8n-io/n8n/commit/ee7147c6b3b053ac8fc317319ab257204e599f16)) +* **MySQL Node:** Fix "Maximum call stack size exceeded" error when handling a large number of rows ([#10965](https://github.com/n8n-io/n8n/issues/10965)) ([62159bd](https://github.com/n8n-io/n8n/commit/62159bd71c9a0303b597a68113e0ac50473ee8d4)) +* **Notion Node:** Allow UUID v8 in notion id checks ([#10938](https://github.com/n8n-io/n8n/issues/10938)) ([46beda0](https://github.com/n8n-io/n8n/commit/46beda05f6771c31bcf0b6a781976d8261079a66)) + + +### Features + +* **Brandfetch Node:** Update to use new API ([#10877](https://github.com/n8n-io/n8n/issues/10877)) ([08ba9a3](https://github.com/n8n-io/n8n/commit/08ba9a36a43b6c84f69bb04fa4d6419a7a4adddf)) +* **editor:** Setup Sentry integration ([#10945](https://github.com/n8n-io/n8n/issues/10945)) ([6de4dff](https://github.com/n8n-io/n8n/commit/6de4dfff87e4da888567081a9928d9682bdea11d)) +* **editor:** Show a notice before deleting annotated executions ([#10934](https://github.com/n8n-io/n8n/issues/10934)) ([dcc1c72](https://github.com/n8n-io/n8n/commit/dcc1c72fc4b56c3252183541b22da801804d4f79)) +* Page size 1 option ([#10957](https://github.com/n8n-io/n8n/issues/10957)) ([bdc0622](https://github.com/n8n-io/n8n/commit/bdc0622f59e98c9e6c542f5cb59a2dbd9008ba96)) +* **Slack Node:** Add option to hide workflow link on message update ([#10927](https://github.com/n8n-io/n8n/issues/10927)) ([422c946](https://github.com/n8n-io/n8n/commit/422c9463c8d931a728615a1fe5a10f05a96ecaa2)) + + +### Performance Improvements + +* **editor:** Use virtual scrolling in `RunDataJson.vue` ([#10838](https://github.com/n8n-io/n8n/issues/10838)) ([f5474ff](https://github.com/n8n-io/n8n/commit/f5474ff79198a2f5a145d0a9df1bb651ea677ec5)) + + + # [1.60.0](https://github.com/n8n-io/n8n/compare/n8n@1.59.0...n8n@1.60.0) (2024-09-18) diff --git a/package.json b/package.json index 216ca673b6..8253d86dfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.60.0", + "version": "1.61.0", "private": true, "engines": { "node": ">=20.15", @@ -81,7 +81,7 @@ }, "patchedDependencies": { "typedi@0.10.0": "patches/typedi@0.10.0.patch", - "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", + "@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.patch", "pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch", "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", diff --git a/packages/@n8n/api-types/package.json b/packages/@n8n/api-types/package.json index 726b0f908e..ec2bf1bd32 100644 --- a/packages/@n8n/api-types/package.json +++ b/packages/@n8n/api-types/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/api-types", - "version": "0.2.0", + "version": "0.3.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index 9a2451dcc4..f0b11ddeb2 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-benchmark", - "version": "1.4.0", + "version": "1.5.0", "description": "Cli for running benchmark tests for n8n", "main": "dist/index", "scripts": { diff --git a/packages/@n8n/config/package.json b/packages/@n8n/config/package.json index 75be87b674..e1ac104f04 100644 --- a/packages/@n8n/config/package.json +++ b/packages/@n8n/config/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/config", - "version": "1.10.0", + "version": "1.11.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/config/src/configs/sentry.config.ts b/packages/@n8n/config/src/configs/sentry.config.ts new file mode 100644 index 0000000000..d1067f9984 --- /dev/null +++ b/packages/@n8n/config/src/configs/sentry.config.ts @@ -0,0 +1,12 @@ +import { Config, Env } from '../decorators'; + +@Config +export class SentryConfig { + /** Sentry DSN for the backend. */ + @Env('N8N_SENTRY_DSN') + backendDsn: string = ''; + + /** Sentry DSN for the frontend . */ + @Env('N8N_FRONTEND_SENTRY_DSN') + frontendDsn: string = ''; +} diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index c6e456709c..5098093db4 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -8,6 +8,7 @@ import { ExternalStorageConfig } from './configs/external-storage.config'; import { NodesConfig } from './configs/nodes.config'; import { PublicApiConfig } from './configs/public-api.config'; import { ScalingModeConfig } from './configs/scaling-mode.config'; +import { SentryConfig } from './configs/sentry.config'; import { TemplatesConfig } from './configs/templates.config'; import { UserManagementConfig } from './configs/user-management.config'; import { VersionNotificationsConfig } from './configs/version-notifications.config'; @@ -49,6 +50,9 @@ export class GlobalConfig { @Nested workflows: WorkflowsConfig; + @Nested + sentry: SentryConfig; + /** Path n8n is deployed to */ @Env('N8N_PATH') path: string = '/'; diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 1d3e5f971d..11fd97a5db 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -221,6 +221,10 @@ describe('GlobalConfig', () => { }, }, }, + sentry: { + backendDsn: '', + frontendDsn: '', + }, }; it('should use all default values when no env variables are defined', () => { diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts index 07c57a1507..a14e4195c9 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts @@ -85,7 +85,6 @@ function getInputs( '@n8n/n8n-nodes-langchain.lmChatGroq', '@n8n/n8n-nodes-langchain.lmChatOllama', '@n8n/n8n-nodes-langchain.lmChatOpenAi', - '@n8n/n8n-nodes-langchain.lmChatGooglePalm', '@n8n/n8n-nodes-langchain.lmChatGoogleGemini', '@n8n/n8n-nodes-langchain.lmChatGoogleVertex', '@n8n/n8n-nodes-langchain.lmChatMistralCloud', @@ -111,11 +110,13 @@ function getInputs( nodes: [ '@n8n/n8n-nodes-langchain.lmChatAnthropic', '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', + '@n8n/n8n-nodes-langchain.lmChatAwsBedrock', '@n8n/n8n-nodes-langchain.lmChatMistralCloud', '@n8n/n8n-nodes-langchain.lmChatOllama', '@n8n/n8n-nodes-langchain.lmChatOpenAi', '@n8n/n8n-nodes-langchain.lmChatGroq', '@n8n/n8n-nodes-langchain.lmChatGoogleVertex', + '@n8n/n8n-nodes-langchain.lmChatGoogleGemini', ], }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsAwsBedrock/EmbeddingsAwsBedrock.node.ts b/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsAwsBedrock/EmbeddingsAwsBedrock.node.ts index 832ff52f78..58caebe05b 100644 --- a/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsAwsBedrock/EmbeddingsAwsBedrock.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsAwsBedrock/EmbeddingsAwsBedrock.node.ts @@ -1,4 +1,5 @@ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */ +import { BedrockEmbeddings } from '@langchain/aws'; import { NodeConnectionType, type IExecuteFunctions, @@ -6,7 +7,6 @@ import { type INodeTypeDescription, type SupplyData, } from 'n8n-workflow'; -import { BedrockEmbeddings } from '@langchain/community/embeddings/bedrock'; import { logWrapper } from '../../../utils/logWrapper'; import { getConnectionHintNoticeField } from '../../../utils/sharedFields'; diff --git a/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsGooglePalm/EmbeddingsGooglePalm.node.ts b/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsGooglePalm/EmbeddingsGooglePalm.node.ts deleted file mode 100644 index 0e31329f01..0000000000 --- a/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsGooglePalm/EmbeddingsGooglePalm.node.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ -import { - NodeConnectionType, - type IExecuteFunctions, - type INodeType, - type INodeTypeDescription, - type SupplyData, -} from 'n8n-workflow'; -import { GooglePaLMEmbeddings } from '@langchain/community/embeddings/googlepalm'; -import { logWrapper } from '../../../utils/logWrapper'; -import { getConnectionHintNoticeField } from '../../../utils/sharedFields'; - -export class EmbeddingsGooglePalm implements INodeType { - description: INodeTypeDescription = { - displayName: 'Embeddings Google PaLM', - name: 'embeddingsGooglePalm', - icon: 'file:google.svg', - group: ['transform'], - version: 1, - description: 'Use Google PaLM Embeddings', - defaults: { - name: 'Embeddings Google PaLM', - }, - requestDefaults: { - ignoreHttpStatusErrors: true, - baseURL: '={{ $credentials.host }}', - }, - credentials: [ - { - name: 'googlePalmApi', - required: true, - }, - ], - codex: { - categories: ['AI'], - subcategories: { - AI: ['Embeddings'], - }, - resources: { - primaryDocumentation: [ - { - url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.embeddingsgooglepalm/', - }, - ], - }, - }, - // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node - inputs: [], - // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong - outputs: [NodeConnectionType.AiEmbedding], - outputNames: ['Embeddings'], - properties: [ - getConnectionHintNoticeField([NodeConnectionType.AiVectorStore]), - { - displayName: - 'Each model is using different dimensional density for embeddings. Please make sure to use the same dimensionality for your vector store. The default model is using 768-dimensional embeddings.', - name: 'notice', - type: 'notice', - default: '', - }, - { - displayName: 'Model', - name: 'modelName', - type: 'options', - description: - 'The model which will generate the embeddings. Learn more.', - typeOptions: { - loadOptions: { - routing: { - request: { - method: 'GET', - url: '/v1beta3/models', - }, - output: { - postReceive: [ - { - type: 'rootProperty', - properties: { - property: 'models', - }, - }, - { - type: 'filter', - properties: { - pass: "={{ $responseItem.name.startsWith('models/embedding') }}", - }, - }, - { - type: 'setKeyValue', - properties: { - name: '={{$responseItem.name}}', - value: '={{$responseItem.name}}', - description: '={{$responseItem.description}}', - }, - }, - { - type: 'sort', - properties: { - key: 'name', - }, - }, - ], - }, - }, - }, - }, - routing: { - send: { - type: 'body', - property: 'model', - }, - }, - default: 'models/embedding-gecko-001', - }, - ], - }; - - async supplyData(this: IExecuteFunctions, itemIndex: number): Promise { - this.logger.debug('Supply data for embeddings Google PaLM'); - const modelName = this.getNodeParameter( - 'modelName', - itemIndex, - 'models/embedding-gecko-001', - ) as string; - const credentials = await this.getCredentials('googlePalmApi'); - const embeddings = new GooglePaLMEmbeddings({ - apiKey: credentials.apiKey as string, - modelName, - }); - - return { - response: logWrapper(embeddings, this), - }; - } -} diff --git a/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsGooglePalm/google.svg b/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsGooglePalm/google.svg deleted file mode 100644 index 38f3c22592..0000000000 --- a/packages/@n8n/nodes-langchain/nodes/embeddings/EmbeddingsGooglePalm/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts index ee0a481de6..c7b3d8ad95 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts @@ -1,4 +1,5 @@ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */ +import { ChatBedrockConverse } from '@langchain/aws'; import { NodeConnectionType, type IExecuteFunctions, @@ -6,13 +7,8 @@ import { type INodeTypeDescription, type SupplyData, } from 'n8n-workflow'; -import { BedrockChat } from '@langchain/community/chat_models/bedrock'; + import { getConnectionHintNoticeField } from '../../../utils/sharedFields'; -// Dependencies needed underneath the hood. We add them -// here only to track where what dependency is used -import '@aws-sdk/credential-provider-node'; -import '@aws-sdk/client-bedrock-runtime'; -import '@aws-sdk/client-sso-oidc'; import { N8nLlmTracing } from '../N8nLlmTracing'; export class LmChatAwsBedrock implements INodeType { @@ -144,7 +140,7 @@ export class LmChatAwsBedrock implements INodeType { maxTokensToSample: number; }; - const model = new BedrockChat({ + const model = new ChatBedrockConverse({ region: credentials.region as string, model: modelName, temperature: options.temperature, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts deleted file mode 100644 index 6195d4d987..0000000000 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ -import { - NodeConnectionType, - type IExecuteFunctions, - type INodeType, - type INodeTypeDescription, - type SupplyData, -} from 'n8n-workflow'; -import { ChatGooglePaLM } from '@langchain/community/chat_models/googlepalm'; -import { N8nLlmTracing } from '../N8nLlmTracing'; - -export class LmChatGooglePalm implements INodeType { - description: INodeTypeDescription = { - displayName: 'Google PaLM Chat Model', - // eslint-disable-next-line n8n-nodes-base/node-class-description-name-miscased - name: 'lmChatGooglePalm', - icon: 'file:google.svg', - hidden: true, - group: ['transform'], - version: 1, - description: 'Chat Model Google PaLM', - defaults: { - name: 'Google PaLM Chat Model', - }, - codex: { - categories: ['AI'], - subcategories: { - AI: ['Language Models', 'Root Nodes'], - 'Language Models': ['Chat Models (Recommended)'], - }, - resources: { - primaryDocumentation: [ - { - url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatgooglepalm/', - }, - ], - }, - }, - // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node - inputs: [], - // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong - outputs: [NodeConnectionType.AiLanguageModel], - outputNames: ['Model'], - credentials: [ - { - name: 'googlePalmApi', - required: true, - }, - ], - requestDefaults: { - ignoreHttpStatusErrors: true, - baseURL: '={{ $credentials.host }}', - }, - properties: [ - { - displayName: - "Google PaLM API is deprecated. Please use Google Vertex or Google Gemini nodes instead.", - name: 'deprecated', - type: 'notice', - default: '', - }, - { - displayName: 'Model', - name: 'modelName', - type: 'options', - description: - 'The model which will generate the completion. Learn more.', - typeOptions: { - loadOptions: { - routing: { - request: { - method: 'GET', - url: '/v1beta3/models', - }, - output: { - postReceive: [ - { - type: 'rootProperty', - properties: { - property: 'models', - }, - }, - { - type: 'filter', - properties: { - pass: "={{ $responseItem.name.startsWith('models/chat') }}", - }, - }, - { - type: 'setKeyValue', - properties: { - name: '={{$responseItem.name}}', - value: '={{$responseItem.name}}', - description: '={{$responseItem.description}}', - }, - }, - { - type: 'sort', - properties: { - key: 'name', - }, - }, - ], - }, - }, - }, - }, - routing: { - send: { - type: 'body', - property: 'model', - }, - }, - default: 'models/chat-bison-001', - }, - { - displayName: 'Options', - name: 'options', - placeholder: 'Add Option', - description: 'Additional options to add', - type: 'collection', - default: {}, - options: [ - { - displayName: 'Sampling Temperature', - name: 'temperature', - default: 0.7, - typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 }, - description: - 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.', - type: 'number', - }, - { - displayName: 'Top K', - name: 'topK', - default: 40, - typeOptions: { maxValue: 1, minValue: -1, numberPrecision: 1 }, - description: - 'Used to remove "long tail" low probability responses. Defaults to -1, which disables it.', - type: 'number', - }, - { - displayName: 'Top P', - name: 'topP', - default: 0.9, - typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 }, - description: - 'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.', - type: 'number', - }, - ], - }, - ], - }; - - async supplyData(this: IExecuteFunctions, itemIndex: number): Promise { - const credentials = await this.getCredentials('googlePalmApi'); - - const modelName = this.getNodeParameter('modelName', itemIndex) as string; - const options = this.getNodeParameter('options', itemIndex, {}) as object; - - const model = new ChatGooglePaLM({ - apiKey: credentials.apiKey as string, - modelName, - ...options, - callbacks: [new N8nLlmTracing(this)], - }); - - return { - response: model, - }; - } -} diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/google.svg b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/google.svg deleted file mode 100644 index 38f3c22592..0000000000 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts deleted file mode 100644 index e4681803fe..0000000000 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ -import { - NodeConnectionType, - type IExecuteFunctions, - type INodeType, - type INodeTypeDescription, - type SupplyData, -} from 'n8n-workflow'; -import { GooglePaLM } from '@langchain/community/llms/googlepalm'; -import { N8nLlmTracing } from '../N8nLlmTracing'; - -export class LmGooglePalm implements INodeType { - description: INodeTypeDescription = { - displayName: 'Google PaLM Language Model', - // eslint-disable-next-line n8n-nodes-base/node-class-description-name-miscased - name: 'lmGooglePalm', - hidden: true, - icon: 'file:google.svg', - group: ['transform'], - version: 1, - description: 'Language Model Google PaLM', - defaults: { - name: 'Google PaLM Language Model', - }, - codex: { - categories: ['AI'], - subcategories: { - AI: ['Language Models', 'Root Nodes'], - 'Language Models': ['Text Completion Models'], - }, - resources: { - primaryDocumentation: [ - { - url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmgooglepalm/', - }, - ], - }, - }, - // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node - inputs: [], - // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong - outputs: [NodeConnectionType.AiLanguageModel], - outputNames: ['Model'], - credentials: [ - { - name: 'googlePalmApi', - required: true, - }, - ], - requestDefaults: { - ignoreHttpStatusErrors: true, - baseURL: '={{ $credentials.host }}', - }, - properties: [ - { - displayName: - "Google PaLM API is deprecated. Please use Google Vertex or Google Gemini nodes instead.", - name: 'deprecated', - type: 'notice', - default: '', - }, - { - displayName: 'Model', - name: 'modelName', - type: 'options', - description: - 'The model which will generate the completion. Learn more.', - typeOptions: { - loadOptions: { - routing: { - request: { - method: 'GET', - url: '/v1beta3/models', - }, - output: { - postReceive: [ - { - type: 'rootProperty', - properties: { - property: 'models', - }, - }, - { - type: 'filter', - properties: { - pass: "={{ $responseItem.name.startsWith('models/text') }}", - }, - }, - { - type: 'setKeyValue', - properties: { - name: '={{$responseItem.name}}', - value: '={{$responseItem.name}}', - description: '={{$responseItem.description}}', - }, - }, - { - type: 'sort', - properties: { - key: 'name', - }, - }, - ], - }, - }, - }, - }, - routing: { - send: { - type: 'body', - property: 'model', - }, - }, - default: 'models/text-bison-001', - }, - { - displayName: 'Options', - name: 'options', - placeholder: 'Add Option', - description: 'Additional options to add', - type: 'collection', - default: {}, - options: [ - { - displayName: 'Maximum Number of Tokens', - name: 'maxOutputTokens', - default: 1024, - description: 'The maximum number of tokens to generate in the completion', - type: 'number', - }, - { - displayName: 'Sampling Temperature', - name: 'temperature', - default: 0.7, - typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 }, - description: - 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.', - type: 'number', - }, - { - displayName: 'Top K', - name: 'topK', - default: 40, - typeOptions: { maxValue: 1, minValue: -1, numberPrecision: 1 }, - description: - 'Used to remove "long tail" low probability responses. Defaults to -1, which disables it.', - type: 'number', - }, - { - displayName: 'Top P', - name: 'topP', - default: 0.9, - typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 }, - description: - 'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.', - type: 'number', - }, - ], - }, - ], - }; - - async supplyData(this: IExecuteFunctions, itemIndex: number): Promise { - const credentials = await this.getCredentials('googlePalmApi'); - - const modelName = this.getNodeParameter('modelName', itemIndex) as string; - const options = this.getNodeParameter('options', itemIndex, {}) as object; - - const model = new GooglePaLM({ - apiKey: credentials.apiKey as string, - modelName, - ...options, - callbacks: [new N8nLlmTracing(this)], - }); - - return { - response: model, - }; - } -} diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/google.svg b/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/google.svg deleted file mode 100644 index 38f3c22592..0000000000 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index f987d36e88..ec7001286d 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-nodes-langchain", - "version": "1.60.0", + "version": "1.61.0", "description": "", "main": "index.js", "scripts": { @@ -57,7 +57,6 @@ "dist/nodes/embeddings/EmbeddingsCohere/EmbeddingsCohere.node.js", "dist/nodes/embeddings/EmbeddingsAwsBedrock/EmbeddingsAwsBedrock.node.js", "dist/nodes/embeddings/EmbeddingsAzureOpenAi/EmbeddingsAzureOpenAi.node.js", - "dist/nodes/embeddings/EmbeddingsGooglePalm/EmbeddingsGooglePalm.node.js", "dist/nodes/embeddings/EmbeddingsGoogleGemini/EmbeddingsGoogleGemini.node.js", "dist/nodes/embeddings/EmbeddingsHuggingFaceInference/EmbeddingsHuggingFaceInference.node.js", "dist/nodes/embeddings/EmbeddingsMistralCloud/EmbeddingsMistralCloud.node.js", @@ -65,9 +64,7 @@ "dist/nodes/embeddings/EmbeddingsOllama/EmbeddingsOllama.node.js", "dist/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.js", "dist/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.js", - "dist/nodes/llms/LmGooglePalm/LmGooglePalm.node.js", "dist/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.js", - "dist/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.js", "dist/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.js", "dist/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.js", "dist/nodes/llms/LmChatGroq/LmChatGroq.node.js", @@ -131,40 +128,38 @@ "n8n-core": "workspace:*" }, "dependencies": { - "@aws-sdk/client-bedrock-runtime": "3.645.0", - "@aws-sdk/client-sso-oidc": "^3.645.0", - "@aws-sdk/credential-provider-node": "3.645.0", "@getzep/zep-cloud": "1.0.11", "@getzep/zep-js": "0.9.0", - "@google-ai/generativelanguage": "2.5.0", + "@google-ai/generativelanguage": "2.6.0", "@google-cloud/resource-manager": "5.3.0", - "@google/generative-ai": "0.17.1", + "@google/generative-ai": "0.19.0", "@huggingface/inference": "2.8.0", - "@langchain/anthropic": "0.2.16", - "@langchain/cohere": "0.2.2", - "@langchain/community": "0.2.32", + "@langchain/anthropic": "0.3.1", + "@langchain/aws": "^0.1.0", + "@langchain/cohere": "0.3.0", + "@langchain/community": "0.3.2", "@langchain/core": "catalog:", - "@langchain/google-genai": "0.0.26", - "@langchain/google-vertexai": "0.0.27", - "@langchain/groq": "0.0.17", - "@langchain/mistralai": "0.0.29", - "@langchain/ollama": "0.0.4", - "@langchain/openai": "0.2.10", - "@langchain/pinecone": "0.0.9", - "@langchain/qdrant": "0.0.5", - "@langchain/redis": "0.0.5", - "@langchain/textsplitters": "0.0.3", + "@langchain/google-genai": "0.1.0", + "@langchain/google-vertexai": "0.1.0", + "@langchain/groq": "0.1.2", + "@langchain/mistralai": "0.1.1", + "@langchain/ollama": "0.1.0", + "@langchain/openai": "0.3.0", + "@langchain/pinecone": "0.1.0", + "@langchain/qdrant": "0.1.0", + "@langchain/redis": "0.1.0", + "@langchain/textsplitters": "0.1.0", "@mozilla/readability": "^0.5.0", "@n8n/typeorm": "0.3.20-10", "@n8n/vm2": "3.9.25", - "@pinecone-database/pinecone": "3.0.0", - "@qdrant/js-client-rest": "1.9.0", - "@supabase/supabase-js": "2.45.3", + "@pinecone-database/pinecone": "3.0.3", + "@qdrant/js-client-rest": "1.11.0", + "@supabase/supabase-js": "2.45.4", "@types/pg": "^8.11.6", - "@xata.io/client": "0.28.4", + "@xata.io/client": "0.30.0", "basic-auth": "catalog:", "cheerio": "1.0.0-rc.12", - "cohere-ai": "7.13.0", + "cohere-ai": "7.13.2", "d3-dsv": "2.0.0", "epub2": "3.0.2", "form-data": "catalog:", @@ -172,12 +167,12 @@ "html-to-text": "9.0.5", "jsdom": "^23.0.1", "json-schema-to-zod": "2.1.0", - "langchain": "0.2.18", + "langchain": "0.3.2", "lodash": "catalog:", "mammoth": "1.7.2", "n8n-nodes-base": "workspace:*", "n8n-workflow": "workspace:*", - "openai": "4.58.0", + "openai": "4.63.0", "pdf-parse": "1.1.1", "pg": "8.12.0", "redis": "4.6.12", @@ -185,6 +180,6 @@ "temp": "0.9.4", "tmp-promise": "3.0.3", "zod": "catalog:", - "zod-to-json-schema": "3.23.2" + "zod-to-json-schema": "3.23.3" } } diff --git a/packages/cli/bin/n8n b/packages/cli/bin/n8n index 6eca060851..c4b593ccc9 100755 --- a/packages/cli/bin/n8n +++ b/packages/cli/bin/n8n @@ -43,6 +43,13 @@ require('express-async-errors'); require('source-map-support').install(); require('reflect-metadata'); +// Skip loading dotenv in e2e tests. +// Also, do not use `inE2ETests` from constants here, because that'd end up code that might read from `process.env` before the values are loaded from an `.env` file. +if (process.env.E2E_TESTS !== 'true') { + // Loading dotenv early ensures that `process.env` is up-to-date everywhere in code + require('dotenv').config(); +} + if (process.env.NODEJS_PREFER_IPV4 === 'true') { require('dns').setDefaultResultOrder('ipv4first'); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 484727ee41..50101c063d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.60.0", + "version": "1.61.0", "description": "n8n Workflow Automation Tool", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 6661057b9a..0c799aa8a5 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -1,6 +1,5 @@ import { GlobalConfig } from '@n8n/config'; import convict from 'convict'; -import dotenv from 'dotenv'; import { flatten } from 'flat'; import { readFileSync } from 'fs'; import merge from 'lodash/merge'; @@ -22,8 +21,6 @@ if (inE2ETests) { process.env.N8N_PUBLIC_API_DISABLED = 'true'; process.env.SKIP_STATISTICS_EVENTS = 'true'; process.env.N8N_SECURE_COOKIE = 'false'; -} else { - dotenv.config(); } // Load schema after process.env has been overwritten diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 0db300eaf0..e811fe8e10 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -452,14 +452,6 @@ export const schema = { env: 'N8N_DIAGNOSTICS_POSTHOG_API_HOST', }, }, - sentry: { - dsn: { - doc: 'Data source name for error tracking on Sentry', - format: String, - default: '', - env: 'N8N_SENTRY_DSN', - }, - }, frontend: { doc: 'Diagnostics config for frontend.', format: String, diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index 43041a84b1..dc5ab4e6c7 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -34,7 +34,7 @@ import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; import type { ICredentialsDb } from '@/interfaces'; import { Logger } from '@/logger'; -import { userHasScope } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions/check-access'; import type { CredentialRequest, ListQuery } from '@/requests'; import { CredentialsTester } from '@/services/credentials-tester.service'; import { OwnershipService } from '@/services/ownership.service'; @@ -598,7 +598,7 @@ export class CredentialsService { // could actually be testing the credential before saving it, so this should cover // the cases we need it for. if ( - !(await userHasScope(user, ['credential:update'], false, { credentialId: credential.id })) + !(await userHasScopes(user, ['credential:update'], false, { credentialId: credential.id })) ) { mergedCredentials.data = decryptedData; } diff --git a/packages/cli/src/decorators/controller.registry.ts b/packages/cli/src/decorators/controller.registry.ts index 41806cb958..3a22090db1 100644 --- a/packages/cli/src/decorators/controller.registry.ts +++ b/packages/cli/src/decorators/controller.registry.ts @@ -11,7 +11,7 @@ import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants'; import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error'; import type { BooleanLicenseFeature } from '@/interfaces'; import { License } from '@/license'; -import { userHasScope } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions/check-access'; import type { AuthenticatedRequest } from '@/requests'; import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file @@ -151,7 +151,7 @@ export class ControllerRegistry { const { scope, globalOnly } = accessScope; - if (!(await userHasScope(req.user, [scope], globalOnly, req.params))) { + if (!(await userHasScopes(req.user, [scope], globalOnly, req.params))) { return res.status(403).json({ status: 'error', message: RESPONSE_ERROR_MESSAGES.MISSING_SCOPE, diff --git a/packages/cli/src/error-reporting.ts b/packages/cli/src/error-reporting.ts index 64a7696a77..d1ecd39198 100644 --- a/packages/cli/src/error-reporting.ts +++ b/packages/cli/src/error-reporting.ts @@ -1,9 +1,9 @@ +import { GlobalConfig } from '@n8n/config'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { QueryFailedError } from '@n8n/typeorm'; import { createHash } from 'crypto'; import { ErrorReporterProxy, ApplicationError } from 'n8n-workflow'; - -import config from '@/config'; +import Container from 'typedi'; let initialized = false; @@ -14,7 +14,7 @@ export const initErrorHandling = async () => { ErrorReporterProxy.error(error); }); - const dsn = config.getEnv('diagnostics.config.sentry.dsn'); + const dsn = Container.get(GlobalConfig).sentry.backendDsn; if (!dsn) { initialized = true; return; @@ -29,7 +29,7 @@ export const initErrorHandling = async () => { DEPLOYMENT_NAME: serverName, } = process.env; - const { init, captureException, addEventProcessor } = await import('@sentry/node'); + const { init, captureException } = await import('@sentry/node'); const { RewriteFrames } = await import('@sentry/integrations'); const { Integrations } = await import('@sentry/node'); @@ -41,6 +41,8 @@ export const initErrorHandling = async () => { 'OnUnhandledRejection', 'ContextLines', ]; + const seenErrors = new Set(); + init({ dsn, release, @@ -62,34 +64,32 @@ export const initErrorHandling = async () => { }, }), ], - }); + beforeSend(event, { originalException }) { + if (!originalException) return null; - const seenErrors = new Set(); - addEventProcessor((event, { originalException }) => { - if (!originalException) return null; + if ( + originalException instanceof QueryFailedError && + ['SQLITE_FULL', 'SQLITE_IOERR'].some((errMsg) => originalException.message.includes(errMsg)) + ) { + return null; + } - if ( - originalException instanceof QueryFailedError && - ['SQLITE_FULL', 'SQLITE_IOERR'].some((errMsg) => originalException.message.includes(errMsg)) - ) { - return null; - } + if (originalException instanceof ApplicationError) { + const { level, extra, tags } = originalException; + if (level === 'warning') return null; + event.level = level; + if (extra) event.extra = { ...event.extra, ...extra }; + if (tags) event.tags = { ...event.tags, ...tags }; + } - if (originalException instanceof ApplicationError) { - const { level, extra, tags } = originalException; - if (level === 'warning') return null; - event.level = level; - if (extra) event.extra = { ...event.extra, ...extra }; - if (tags) event.tags = { ...event.tags, ...tags }; - } + if (originalException instanceof Error && originalException.stack) { + const eventHash = createHash('sha1').update(originalException.stack).digest('base64'); + if (seenErrors.has(eventHash)) return null; + seenErrors.add(eventHash); + } - if (originalException instanceof Error && originalException.stack) { - const eventHash = createHash('sha1').update(originalException.stack).digest('base64'); - if (seenErrors.has(eventHash)) return null; - seenErrors.add(eventHash); - } - - return event; + return event; + }, }); ErrorReporterProxy.init({ diff --git a/packages/cli/src/events/events.controller.ts b/packages/cli/src/events/events.controller.ts new file mode 100644 index 0000000000..994f803242 --- /dev/null +++ b/packages/cli/src/events/events.controller.ts @@ -0,0 +1,16 @@ +import { Get, RestController } from '@/decorators'; +import { AuthenticatedRequest } from '@/requests'; + +import { EventService } from './event.service'; + +/** This controller holds endpoints that the frontend uses to trigger telemetry events */ +@RestController('/events') +export class EventsController { + constructor(private readonly eventService: EventService) {} + + @Get('/session-started') + sessionStarted(req: AuthenticatedRequest) { + const pushRef = req.headers['push-ref']; + this.eventService.emit('session-started', { pushRef }); + } +} diff --git a/packages/cli/src/permissions/check-access.ts b/packages/cli/src/permissions/check-access.ts index deea44ca99..f4abfcc00f 100644 --- a/packages/cli/src/permissions/check-access.ts +++ b/packages/cli/src/permissions/check-access.ts @@ -10,7 +10,15 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { RoleService } from '@/services/role.service'; -export const userHasScope = async ( +/** + * Check if a user has the required scopes. The check can be: + * + * - only for scopes in the user's global role, or + * - for scopes in the user's global role, else for scopes in the resource roles + * of projects including the user and the resource, else for scopes in the + * project roles in those projects. + */ +export async function userHasScopes( user: User, scopes: Scope[], globalOnly: boolean, @@ -18,15 +26,14 @@ export const userHasScope = async ( credentialId, workflowId, projectId, - }: { credentialId?: string; workflowId?: string; projectId?: string }, -): Promise => { - // Short circuit here since a global role will always have access - if (user.hasGlobalScope(scopes, { mode: 'allOf' })) { - return true; - } else if (globalOnly) { - // The above check already failed so the user doesn't have access - return false; - } + }: { credentialId?: string; workflowId?: string; projectId?: string } /* only one */, +): Promise { + if (user.hasGlobalScope(scopes, { mode: 'allOf' })) return true; + + if (globalOnly) return false; + + // Find which project roles are defined to contain the required scopes. + // Then find projects having this user and having those project roles. const roleService = Container.get(RoleService); const projectRoles = roleService.rolesWithScope('project', scopes); @@ -42,47 +49,29 @@ export const userHasScope = async ( }) ).map((p) => p.id); + // Find which resource roles are defined to contain the required scopes. + // Then find at least one of the above qualifying projects having one of + // those resource roles over the resource being checked. + if (credentialId) { - const exists = await Container.get(SharedCredentialsRepository).find({ - where: { - projectId: In(userProjectIds), - credentialsId: credentialId, - role: In(roleService.rolesWithScope('credential', scopes)), - }, + return await Container.get(SharedCredentialsRepository).existsBy({ + credentialsId: credentialId, + projectId: In(userProjectIds), + role: In(roleService.rolesWithScope('credential', scopes)), }); - - if (!exists.length) { - return false; - } - - return true; } if (workflowId) { - const exists = await Container.get(SharedWorkflowRepository).find({ - where: { - projectId: In(userProjectIds), - workflowId, - role: In(roleService.rolesWithScope('workflow', scopes)), - }, + return await Container.get(SharedWorkflowRepository).existsBy({ + workflowId, + projectId: In(userProjectIds), + role: In(roleService.rolesWithScope('workflow', scopes)), }); - - if (!exists.length) { - return false; - } - - return true; } - if (projectId) { - if (!userProjectIds.includes(projectId)) { - return false; - } - - return true; - } + if (projectId) return userProjectIds.includes(projectId); throw new ApplicationError( - "@ProjectScope decorator was used but does not have a credentialId, workflowId, or projectId in it's URL parameters. This is likely an implementation error. If you're a developer, please check you're URL is correct or that this should be using @GlobalScope.", + "`@ProjectScope` decorator was used but does not have a `credentialId`, `workflowId`, or `projectId` in its URL parameters. This is likely an implementation error. If you're a developer, please check your URL is correct or that this should be using `@GlobalScope`.", ); -}; +} diff --git a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts index 8a49a48093..ed68d4761c 100644 --- a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts +++ b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts @@ -6,7 +6,7 @@ import { Container } from 'typedi'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import type { BooleanLicenseFeature } from '@/interfaces'; import { License } from '@/license'; -import { userHasScope } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions/check-access'; import type { AuthenticatedRequest } from '@/requests'; import type { PaginatedRequest } from '../../../types'; @@ -34,7 +34,7 @@ const buildScopeMiddleware = ( params.credentialId = req.params.id; } } - if (!(await userHasScope(req.user, scopes, globalOnly, params))) { + if (!(await userHasScopes(req.user, scopes, globalOnly, params))) { return res.status(403).json({ message: 'Forbidden' }); } diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 7bdc0ac74d..5afe97f31a 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -49,6 +49,9 @@ export type AuthenticatedRequest< > = Omit, 'user' | 'cookies'> & { user: User; cookies: Record; + headers: express.Request['headers'] & { + 'push-ref': string; + }; }; // ---------------------------------- diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 08f74b2936..27ac3b09a1 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -1,4 +1,3 @@ -import type { FrontendSettings } from '@n8n/api-types'; import cookieParser from 'cookie-parser'; import express from 'express'; import { access as fsAccess } from 'fs/promises'; @@ -21,6 +20,7 @@ import { import { CredentialsOverwrites } from '@/credentials-overwrites'; import { ControllerRegistry } from '@/decorators'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; +import { EventService } from '@/events/event.service'; import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; import type { ICredentialsOverwrite } from '@/interfaces'; import { isLdapEnabled } from '@/ldap/helpers.ee'; @@ -58,12 +58,12 @@ import '@/controllers/user-settings.controller'; import '@/controllers/workflow-statistics.controller'; import '@/credentials/credentials.controller'; import '@/eventbus/event-bus.controller'; +import '@/events/events.controller'; import '@/executions/executions.controller'; import '@/external-secrets/external-secrets.controller.ee'; import '@/license/license.controller'; import '@/workflows/workflow-history/workflow-history.controller.ee'; import '@/workflows/workflows.controller'; -import { EventService } from './events/event.service'; @Service() export class Server extends AbstractServer { @@ -169,10 +169,6 @@ export class Server extends AbstractServer { const { frontendService } = this; if (frontendService) { - frontendService.addToSettings({ - versionCli: N8N_VERSION, - }); - await this.externalHooks.run('frontend.settings', [frontendService.getSettings()]); } @@ -244,11 +240,22 @@ export class Server extends AbstractServer { // Returns the current settings for the UI this.app.get( `/${this.restEndpoint}/settings`, - ResponseHelper.send( - async (req: express.Request): Promise => - frontendService.getSettings(req.headers['push-ref'] as string), - ), + ResponseHelper.send(async () => frontendService.getSettings()), ); + + // Return Sentry config as a static file + this.app.get(`/${this.restEndpoint}/sentry.js`, (_, res) => { + res.type('js'); + res.write('window.sentry='); + res.write( + JSON.stringify({ + dsn: this.globalConfig.sentry.frontendDsn, + environment: process.env.ENVIRONMENT || 'development', + release: N8N_VERSION, + }), + ); + res.end(); + }); } // ---------------------------------------- diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 1693aa939a..6ac3a1863c 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -10,11 +10,10 @@ import path from 'path'; import { Container, Service } from 'typedi'; import config from '@/config'; -import { LICENSE_FEATURES } from '@/constants'; +import { LICENSE_FEATURES, N8N_VERSION } from '@/constants'; import { CredentialTypes } from '@/credential-types'; import { CredentialsOverwrites } from '@/credentials-overwrites'; import { getVariablesLimit } from '@/environments/variables/environment-helpers'; -import { EventService } from '@/events/event.service'; import { getLdapLoginLabel } from '@/ldap/helpers.ee'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; @@ -47,7 +46,6 @@ export class FrontendService { private readonly mailer: UserManagementMailer, private readonly instanceSettings: InstanceSettings, private readonly urlService: UrlService, - private readonly eventService: EventService, ) { loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes()); void this.generateTypes(); @@ -102,7 +100,7 @@ export class FrontendService { urlBaseEditor: instanceBaseUrl, binaryDataMode: config.getEnv('binaryDataManager.mode'), nodeJsVersion: process.version.replace(/^v/, ''), - versionCli: '', + versionCli: N8N_VERSION, concurrency: config.getEnv('executions.concurrency.productionLimit'), authCookie: { secure: config.getEnv('secure_cookie'), @@ -242,9 +240,7 @@ export class FrontendService { this.writeStaticJSON('credentials', credentials); } - getSettings(pushRef?: string): FrontendSettings { - this.eventService.emit('session-started', { pushRef }); - + getSettings(): FrontendSettings { const restEndpoint = this.globalConfig.endpoints.rest; // Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel` @@ -344,10 +340,6 @@ export class FrontendService { return this.settings; } - addToSettings(newSettings: Record) { - this.settings = { ...this.settings, ...newSettings }; - } - private writeStaticJSON(name: string, data: INodeTypeBaseDescription[] | ICredentialType[]) { const { staticCacheDir } = this.instanceSettings; const filePath = path.join(staticCacheDir, `types/${name}.json`); diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 57f46002c2..30ca5c9773 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -404,7 +404,7 @@ export class WorkflowsController { return await this.workflowExecutionService.executeManually( req.body, req.user, - req.headers['push-ref'] as string, + req.headers['push-ref'], req.query.partialExecutionVersion === '-1' ? config.getEnv('featureFlags.partialExecutionVersionDefault') : req.query.partialExecutionVersion, diff --git a/packages/core/package.json b/packages/core/package.json index 0656929291..74e3483943 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "1.60.0", + "version": "1.61.0", "description": "Core functionality of n8n", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/core/src/PartialExecutionUtils/DirectedGraph.ts b/packages/core/src/PartialExecutionUtils/DirectedGraph.ts index 2485b895b0..bd6cf81a2f 100644 --- a/packages/core/src/PartialExecutionUtils/DirectedGraph.ts +++ b/packages/core/src/PartialExecutionUtils/DirectedGraph.ts @@ -125,6 +125,32 @@ export class DirectedGraph { return directChildren; } + private getChildrenRecursive(node: INode, children: Set) { + const directChildren = this.getDirectChildren(node); + + for (const directChild of directChildren) { + // Break out if we found a cycle. + if (children.has(directChild.to)) { + continue; + } + children.add(directChild.to); + this.getChildrenRecursive(directChild.to, children); + } + + return children; + } + + /** + * Returns all nodes that are children of the node that is passed as an + * argument. + * + * If the node being passed in is a child of itself (e.g. is part of a + * cylce), the return set will contain it as well. + */ + getChildren(node: INode) { + return this.getChildrenRecursive(node, new Set()); + } + getDirectParents(node: INode) { const nodeExists = this.nodes.get(node.name) === node; a.ok(nodeExists); diff --git a/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts b/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts index 4049878eb2..93df23de32 100644 --- a/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts +++ b/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts @@ -38,4 +38,52 @@ describe('DirectedGraph', () => { graph, ); }); + + describe('getChildren', () => { + // ┌─────┐ ┌─────┐ ┌─────┐ + // │node1├───►│node2├──►│node3│ + // └─────┘ └─────┘ └─────┘ + test('returns all children', () => { + // ARRANGE + const node1 = createNodeData({ name: 'Node1' }); + const node2 = createNodeData({ name: 'Node2' }); + const node3 = createNodeData({ name: 'Node3' }); + const graph = new DirectedGraph() + .addNodes(node1, node2, node3) + .addConnections({ from: node1, to: node2 }, { from: node2, to: node3 }); + + // ACT + const children = graph.getChildren(node1); + + // ASSERT + expect(children.size).toBe(2); + expect(children).toEqual(new Set([node2, node3])); + }); + + // ┌─────┐ ┌─────┐ ┌─────┐ + // ┌─►│node1├───►│node2├──►│node3├─┐ + // │ └─────┘ └─────┘ └─────┘ │ + // │ │ + // └───────────────────────────────┘ + test('terminates when finding a cycle', () => { + // ARRANGE + const node1 = createNodeData({ name: 'Node1' }); + const node2 = createNodeData({ name: 'Node2' }); + const node3 = createNodeData({ name: 'Node3' }); + const graph = new DirectedGraph() + .addNodes(node1, node2, node3) + .addConnections( + { from: node1, to: node2 }, + { from: node2, to: node3 }, + { from: node3, to: node1 }, + ); + + // ACT + const children = graph.getChildren(node1); + + // ASSERT + expect(children.size).toBe(3); + expect(children).toEqual(new Set([node1, node2, node3])); + }); + }); }); diff --git a/packages/core/src/PartialExecutionUtils/__tests__/cleanRunData.test.ts b/packages/core/src/PartialExecutionUtils/__tests__/cleanRunData.test.ts new file mode 100644 index 0000000000..fabfae0ee3 --- /dev/null +++ b/packages/core/src/PartialExecutionUtils/__tests__/cleanRunData.test.ts @@ -0,0 +1,86 @@ +import type { IRunData } from 'n8n-workflow'; +import { cleanRunData } from '../cleanRunData'; +import { DirectedGraph } from '../DirectedGraph'; +import { createNodeData, toITaskData } from './helpers'; + +describe('cleanRunData', () => { + // ┌─────┐ ┌─────┐ ┌─────┐ + // │node1├───►│node2├──►│node3│ + // └─────┘ └─────┘ └─────┘ + test('deletes all run data of all children and the node being passed in', () => { + // ARRANGE + const node1 = createNodeData({ name: 'Node1' }); + const node2 = createNodeData({ name: 'Node2' }); + const node3 = createNodeData({ name: 'Node3' }); + const graph = new DirectedGraph() + .addNodes(node1, node2, node3) + .addConnections({ from: node1, to: node2 }, { from: node2, to: node3 }); + const runData: IRunData = { + [node1.name]: [toITaskData([{ data: { value: 1 } }])], + [node2.name]: [toITaskData([{ data: { value: 2 } }])], + [node3.name]: [toITaskData([{ data: { value: 3 } }])], + }; + + // ACT + const newRunData = cleanRunData(runData, graph, [node1]); + + // ASSERT + expect(newRunData).toEqual({}); + }); + + // ┌─────┐ ┌─────┐ ┌─────┐ + // │node1├───►│node2├──►│node3│ + // └─────┘ └─────┘ └─────┘ + test('retains the run data of parent nodes of the node being passed in', () => { + // ARRANGE + const node1 = createNodeData({ name: 'Node1' }); + const node2 = createNodeData({ name: 'Node2' }); + const node3 = createNodeData({ name: 'Node3' }); + const graph = new DirectedGraph() + .addNodes(node1, node2, node3) + .addConnections({ from: node1, to: node2 }, { from: node2, to: node3 }); + const runData: IRunData = { + [node1.name]: [toITaskData([{ data: { value: 1 } }])], + [node2.name]: [toITaskData([{ data: { value: 2 } }])], + [node3.name]: [toITaskData([{ data: { value: 3 } }])], + }; + + // ACT + const newRunData = cleanRunData(runData, graph, [node2]); + + // ASSERT + expect(newRunData).toEqual({ [node1.name]: runData[node1.name] }); + }); + + // ┌─────┐ ┌─────┐ ┌─────┐ + // ┌─►│node1├───►│node2├──►│node3├─┐ + // │ └─────┘ └─────┘ └─────┘ │ + // │ │ + // └───────────────────────────────┘ + test('terminates when finding a cycle', () => { + // ARRANGE + const node1 = createNodeData({ name: 'Node1' }); + const node2 = createNodeData({ name: 'Node2' }); + const node3 = createNodeData({ name: 'Node3' }); + const graph = new DirectedGraph() + .addNodes(node1, node2, node3) + .addConnections( + { from: node1, to: node2 }, + { from: node2, to: node3 }, + { from: node3, to: node1 }, + ); + + const runData: IRunData = { + [node1.name]: [toITaskData([{ data: { value: 1 } }])], + [node2.name]: [toITaskData([{ data: { value: 2 } }])], + [node3.name]: [toITaskData([{ data: { value: 3 } }])], + }; + + // ACT + const newRunData = cleanRunData(runData, graph, [node2]); + + // ASSERT + // TODO: Find out if this is a desirable result in milestone 2 + expect(newRunData).toEqual({}); + }); +}); diff --git a/packages/core/src/PartialExecutionUtils/cleanRunData.ts b/packages/core/src/PartialExecutionUtils/cleanRunData.ts new file mode 100644 index 0000000000..945dca1451 --- /dev/null +++ b/packages/core/src/PartialExecutionUtils/cleanRunData.ts @@ -0,0 +1,26 @@ +import type { INode, IRunData } from 'n8n-workflow'; +import type { DirectedGraph } from './DirectedGraph'; + +/** + * Returns new run data that does not contain data for any node that is a child + * of any start node. + * This does not mutate the `runData` being passed in. + */ +export function cleanRunData( + runData: IRunData, + graph: DirectedGraph, + startNodes: INode[], +): IRunData { + const newRunData: IRunData = { ...runData }; + + for (const startNode of startNodes) { + delete newRunData[startNode.name]; + const children = graph.getChildren(startNode); + + for (const child of children) { + delete newRunData[child.name]; + } + } + + return newRunData; +} diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index e0898225e9..a10d8c530c 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -58,6 +58,7 @@ import { findSubgraph, findTriggerForPartialExecution, } from './PartialExecutionUtils'; +import { cleanRunData } from './PartialExecutionUtils/cleanRunData'; export class WorkflowExecute { private status: ExecutionStatus = 'new'; @@ -347,7 +348,8 @@ export class WorkflowExecute { } // 2. Find the Subgraph - const subgraph = findSubgraph(DirectedGraph.fromWorkflow(workflow), destinationNode, trigger); + const graph = DirectedGraph.fromWorkflow(workflow); + const subgraph = findSubgraph(graph, destinationNode, trigger); const filteredNodes = subgraph.getNodes(); // 3. Find the Start Nodes @@ -362,7 +364,7 @@ export class WorkflowExecute { } // 6. Clean Run Data - // TODO: + const newRunData: IRunData = cleanRunData(runData, graph, startNodes); // 7. Recreate Execution Stack const { nodeExecutionStack, waitingExecution, waitingExecutionSource } = @@ -376,7 +378,7 @@ export class WorkflowExecute { runNodeFilter: Array.from(filteredNodes.values()).map((node) => node.name), }, resultData: { - runData, + runData: newRunData, pinData, }, executionData: { diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 18493521f2..526a9e43c4 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "n8n-design-system", - "version": "1.50.0", + "version": "1.51.0", "main": "src/main.ts", "import": "src/main.ts", "scripts": { diff --git a/packages/design-system/src/components/N8nDatatable/Datatable.vue b/packages/design-system/src/components/N8nDatatable/Datatable.vue index 3a28a4d54c..905aef4671 100644 --- a/packages/design-system/src/components/N8nDatatable/Datatable.vue +++ b/packages/design-system/src/components/N8nDatatable/Datatable.vue @@ -31,7 +31,7 @@ const emit = defineEmits<{ }>(); const { t } = useI18n(); -const rowsPerPageOptions = ref([10, 25, 50, 100]); +const rowsPerPageOptions = ref([1, 10, 25, 50, 100]); const $style = useCssModule(); diff --git a/packages/design-system/src/components/N8nFormInput/FormInput.vue b/packages/design-system/src/components/N8nFormInput/FormInput.vue index 5f187611da..b1fbd4b6e3 100644 --- a/packages/design-system/src/components/N8nFormInput/FormInput.vue +++ b/packages/design-system/src/components/N8nFormInput/FormInput.vue @@ -193,6 +193,7 @@ defineExpose({ inputRef }); :label="label" :tooltip-text="tooltipText" :required="required && showRequiredAsterisk" + :size="labelSize" >