diff --git a/.github/workflows/docker-images-nightly.yml b/.github/workflows/docker-images-nightly.yml index 2be6e40617c58..ce7a651783469 100644 --- a/.github/workflows/docker-images-nightly.yml +++ b/.github/workflows/docker-images-nightly.yml @@ -35,6 +35,11 @@ on: description: 'URL to call after Docker Image got built successfully.' required: false default: '' + include-arm64: + description: 'Include ARM64 support' + type: boolean + required: true + default: false jobs: build: @@ -76,7 +81,7 @@ jobs: build-args: | N8N_RELEASE_TYPE=nightly file: ./docker/images/n8n-custom/Dockerfile - platforms: linux/amd64 + platforms: ${{ github.event.inputs.include-arm64 == 'true' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} provenance: false push: true tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }} diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index 5d5bbfb11d3d1..597653a5a6508 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -40,7 +40,7 @@ on: containers: description: 'Number of containers to run tests in.' required: false - default: '[1, 2, 3, 4, 5, 6, 7, 8]' + default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]' type: string pr_number: description: 'PR number to run tests for.' diff --git a/CHANGELOG.md b/CHANGELOG.md index 027b5868dda5b..bc07f55eb2feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +# [1.24.0](https://github.com/n8n-io/n8n/compare/n8n@1.23.0...n8n@1.24.0) (2024-01-10) + + +### Bug Fixes + +* **core:** Do not add Authentication header when `authentication` type is `body` ([#8201](https://github.com/n8n-io/n8n/issues/8201)) ([ac1c642](https://github.com/n8n-io/n8n/commit/ac1c642fddfac3b0ed1144c7eccd7c88fbd5a1a5)) +* **core:** Fix test webhook deregistration ([#8247](https://github.com/n8n-io/n8n/issues/8247)) ([5032bf0](https://github.com/n8n-io/n8n/commit/5032bf0e346dccf7cade17a1518b3031118af5e1)) +* **editor:** Items count display in running workflow ([#8148](https://github.com/n8n-io/n8n/issues/8148)) ([8a3c87f](https://github.com/n8n-io/n8n/commit/8a3c87f69c20de7c713dff021e390ea4ea32b103)), closes [/github.com/n8n-io/n8n/pull/7763/files#diff-f5dae80a64b9951bb6691f1b9d439423cf84fa0cc9601b3f2c00904f3135e8deR48](https://github.com//github.com/n8n-io/n8n/pull/7763/files/issues/diff-f5dae80a64b9951bb6691f1b9d439423cf84fa0cc9601b3f2c00904f3135e8deR48) +* **editor:** Only load suggested templates for owners ([#8228](https://github.com/n8n-io/n8n/issues/8228)) ([8f22a26](https://github.com/n8n-io/n8n/commit/8f22a265d607047eff22ba957d627bbec7da7900)) +* **editor:** Tweaking button sizes in execution preview ([#8206](https://github.com/n8n-io/n8n/issues/8206)) ([9d40ae8](https://github.com/n8n-io/n8n/commit/9d40ae8b74594d4368591a62f9b39dde28efc64d)) +* **editor:** Unify canvas button positioning ([#8258](https://github.com/n8n-io/n8n/issues/8258)) ([b6c42cc](https://github.com/n8n-io/n8n/commit/b6c42cc08408d9d7cc49cc84245b4ad515fa3e6a)) +* **editor:** Vertically center workflow preview loading state ([#8231](https://github.com/n8n-io/n8n/issues/8231)) ([2d6e406](https://github.com/n8n-io/n8n/commit/2d6e406e215188dbbbeb593ac09ccad3914aaf81)) +* Fix issue with API key being required for the Qdrant Node ([#8237](https://github.com/n8n-io/n8n/issues/8237)) ([4401db3](https://github.com/n8n-io/n8n/commit/4401db3a2fad3464a5498e9a86fc6bba4f9c9f95)) +* Fix template credential setup for nodes that dont have credentials ([#8208](https://github.com/n8n-io/n8n/issues/8208)) ([cd3f5b5](https://github.com/n8n-io/n8n/commit/cd3f5b5b1f48e42cb6fa5ebcc15527c28502ceb9)) +* Fix user reinvites on FE and BE ([#8261](https://github.com/n8n-io/n8n/issues/8261)) ([0dabe5c](https://github.com/n8n-io/n8n/commit/0dabe5c74e5ad0969d4691b3db4a1e796ed8a08c)) +* **FTP Node:** FTP connection failed due to missing password credential in node ([#8131](https://github.com/n8n-io/n8n/issues/8131)) ([e056aa9](https://github.com/n8n-io/n8n/commit/e056aa9c4dd6c6a7717202029b25f4f65ddecb0d)) +* **Github Trigger Node:** Enforce SSL validation by default ([#8265](https://github.com/n8n-io/n8n/issues/8265)) ([1387541](https://github.com/n8n-io/n8n/commit/1387541e336e7311ba9c43907fa95d3196fae2eb)) +* Make params panel double width for all SQL nodes ([#8236](https://github.com/n8n-io/n8n/issues/8236)) ([048b588](https://github.com/n8n-io/n8n/commit/048b588852f5fed1c976889ba54ef564ca7f4894)) +* **Monday.com Node:** Migrate to api 2023-10 ([#8254](https://github.com/n8n-io/n8n/issues/8254)) ([ccde38a](https://github.com/n8n-io/n8n/commit/ccde38a8a8d65a21bf4d38ef7b09a5ffa3c7ad2d)) +* **MySQL Node:** Only escape table names when needed ([#8246](https://github.com/n8n-io/n8n/issues/8246)) ([3b01eb6](https://github.com/n8n-io/n8n/commit/3b01eb60c98d51d0d7572342b8d6d40763293719)) +* **Nextcloud Node:** Throw an actual error if server responded with Fatal error ([#8234](https://github.com/n8n-io/n8n/issues/8234)) ([b201ff8](https://github.com/n8n-io/n8n/commit/b201ff8f23b2bac6b00d5c16d91b4b2931f45ade)) +* **NocoDB Node:** Download attachments ([#8235](https://github.com/n8n-io/n8n/issues/8235)) ([43e8e5e](https://github.com/n8n-io/n8n/commit/43e8e5e540b9fcbca663fcf17a78a7aba2abb475)) +* **Postgres Node:** Stop marking autogenerated columns as required ([#8230](https://github.com/n8n-io/n8n/issues/8230)) ([bed04ec](https://github.com/n8n-io/n8n/commit/bed04ec122234b4329a5e415bf3627c115b3f32e)), closes [#7084](https://github.com/n8n-io/n8n/issues/7084) +* Resolve expressions in credentials following paired item ([#8250](https://github.com/n8n-io/n8n/issues/8250)) ([ccb2b07](https://github.com/n8n-io/n8n/commit/ccb2b076f8240b0712949b72ec57ae72a36ef62d)) +* **Set Node:** Field not excluded if dot notation disabled and field was set by using drag-and-drop from ui ([#8233](https://github.com/n8n-io/n8n/issues/8233)) ([cda49a4](https://github.com/n8n-io/n8n/commit/cda49a4747ef4369ce7a971872c6fb8a74f4156d)) +* Store workflow settings when saving an execution ([#8288](https://github.com/n8n-io/n8n/issues/8288)) ([8a7c629](https://github.com/n8n-io/n8n/commit/8a7c629ea183f75f9916003edf11cb8aeef445eb)) +* **Webhook Node:** Fix handling of form-data files ([#8256](https://github.com/n8n-io/n8n/issues/8256)) ([fc29030](https://github.com/n8n-io/n8n/commit/fc2903096e6e64e5b2a14593418d5651e07ca9ee)) + + +### Features + +* Add Chat Trigger node ([#7409](https://github.com/n8n-io/n8n/issues/7409)) ([af49e95](https://github.com/n8n-io/n8n/commit/af49e95cc7ccf70f233f9bd1e34fbb57f7f08ccf)) +* **core:** Cache test webhook registrations ([#8176](https://github.com/n8n-io/n8n/issues/8176)) ([22a5f52](https://github.com/n8n-io/n8n/commit/22a5f5258da0a973e1ad44c0d3d4f0fda1d23444)), closes [#8155](https://github.com/n8n-io/n8n/issues/8155) +* **core:** Validate shutdown handlers on startup ([#8260](https://github.com/n8n-io/n8n/issues/8260)) ([3b996a7](https://github.com/n8n-io/n8n/commit/3b996a7da0137a75c3047656a4bc8cc336ebfc1e)) +* **editor:** Add fullscreen view to code editor ([#8084](https://github.com/n8n-io/n8n/issues/8084)) ([071e6d6](https://github.com/n8n-io/n8n/commit/071e6d6b6e32b7196f34043710c23331ad28fac0)) +* **editor:** Update copy: `Execute` --> `Test` ([#8137](https://github.com/n8n-io/n8n/issues/8137)) ([df5d07b](https://github.com/n8n-io/n8n/commit/df5d07bcb8beba760bc17118b36ccd531bc3c755)) +* **Google Sheets Node:** Add "By Name" option to selector for Sheets ([#8241](https://github.com/n8n-io/n8n/issues/8241)) ([dce28f9](https://github.com/n8n-io/n8n/commit/dce28f9cb98db33bf22bcfee181f8e9ca64dd2bc)) +* **HTTP Request Node:** Interval Between Requests option for pagination ([#8224](https://github.com/n8n-io/n8n/issues/8224)) ([270328c](https://github.com/n8n-io/n8n/commit/270328ccf6e5502adc092f6f85d146ffb98e1208)) +* Implement MistralCloud Chat & Embeddings nodes ([#8239](https://github.com/n8n-io/n8n/issues/8239)) ([d37b908](https://github.com/n8n-io/n8n/commit/d37b9084b2c657d8b5b8bae6dbb51b428db26e1e)) +* **MongoDB Node:** Add support for TLS ([#8266](https://github.com/n8n-io/n8n/issues/8266)) ([e796e7f](https://github.com/n8n-io/n8n/commit/e796e7f06d73a74a403000c53942d56cab91781b)) +* **Switch Node:** Overhaul ([#7855](https://github.com/n8n-io/n8n/issues/7855)) ([f4092a9](https://github.com/n8n-io/n8n/commit/f4092a9e49f66845612420ba59a013796ed80d45)) + + +### Performance Improvements + +* **core:** Improve caching service ([#8213](https://github.com/n8n-io/n8n/issues/8213)) ([f53c482](https://github.com/n8n-io/n8n/commit/f53c482939db938c47523ac11a9538e35e1926a9)), closes [#7747](https://github.com/n8n-io/n8n/issues/7747) +* **core:** Optimize workflow activation errors ([#8242](https://github.com/n8n-io/n8n/issues/8242)) ([f293956](https://github.com/n8n-io/n8n/commit/f2939568cf399e67214e89bc7f859323aaeda8dd)) + + + # [1.23.0](https://github.com/n8n-io/n8n/compare/n8n@1.22.0...n8n@1.23.0) (2024-01-03) diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index 3eab7d0d8f8b0..841cb59e22da1 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -37,6 +37,10 @@ describe('Templates', () => { it('should save template id with the workflow', () => { cy.visit(templatesPage.url); + cy.intercept('GET', '**/api/templates/**').as('loadApi'); + cy.get('.el-skeleton.n8n-loading').should('not.exist'); + templatesPage.getters.firstTemplateCard().should('exist'); + cy.wait('@loadApi'); templatesPage.getters.firstTemplateCard().click(); cy.url().should('include', '/templates/'); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 3899b1269aa3f..4fc69f8fde58f 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -71,10 +71,10 @@ describe('NDV', () => { workflowPage.actions.addNodeToCanvas('Manual'); workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Search records'); ndv.getters.container().should('be.visible'); - // cy.get('.has-issues').should('have.length', 0); + cy.get('.has-issues').should('have.length', 0); ndv.getters.parameterInput('table').find('input').eq(1).focus().blur(); ndv.getters.parameterInput('base').find('input').eq(1).focus().blur(); - cy.get('.has-issues').should('have.length', 0); + cy.get('.has-issues').should('have.length', 2); ndv.getters.backToCanvas().click(); workflowPage.actions.openNode('Airtable'); cy.get('.has-issues').should('have.length', 2); @@ -306,7 +306,7 @@ describe('NDV', () => { ndv.getters.parameterInput('remoteOptions').click(); - ndv.getters.parameterInputIssues('remoteOptions').realHover(); + ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false}); // Remote options dropdown should not be visible ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist'); }); @@ -365,6 +365,8 @@ describe('NDV', () => { ndv.actions.openCodeEditorFullscreen(); ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()'); + ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()'); + cy.wait(200); ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()'); }); diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 853877ed0fb25..f3994b4b42f9b 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -174,14 +174,15 @@ export class WorkflowPage extends BasePage { this.getters.nodeCreatorSearchBar().type(nodeDisplayName); this.getters.nodeCreatorSearchBar().type('{enter}'); - cy.wait(500); cy.get('body').then((body) => { if (body.find('[data-test-id=node-creator]').length > 0) { if (action) { cy.contains(action).click(); } else { // Select the first action - cy.get('[data-keyboard-nav-type="action"]').eq(0).click(); + if (body.find('[data-keyboard-nav-type="action"]').length > 0) { + cy.get('[data-keyboard-nav-type="action"]').eq(0).click(); + } } } }); diff --git a/package.json b/package.json index 6233ff77a669d..2e794cfbf216f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.23.0", + "version": "1.24.0", "private": true, "homepage": "https://n8n.io", "engines": { @@ -48,9 +48,9 @@ "@types/supertest": "^2.0.12", "@vitest/coverage-v8": "^1.1.0", "cross-env": "^7.0.3", - "cypress": "^12.17.2", + "cypress": "^13.6.2", "cypress-otp": "^1.0.3", - "cypress-real-events": "^1.9.1", + "cypress-real-events": "^1.11.0", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", "jest-expect-message": "^1.1.3", diff --git a/packages/@n8n/chat/README.md b/packages/@n8n/chat/README.md index 2273d7d5c80d2..2cb9babbf11f7 100644 --- a/packages/@n8n/chat/README.md +++ b/packages/@n8n/chat/README.md @@ -1,6 +1,12 @@ # n8n Chat This is an embeddable Chat widget for n8n. It allows the execution of AI-Powered Workflows through a Chat window. +**Windowed Example** +![n8n Chat Windowed](https://raw.githubusercontent.com/n8n-io/n8n/master/packages/%40n8n/chat/resources/images/windowed.png) + +**Fullscreen Example** +![n8n Chat Fullscreen](https://raw.githubusercontent.com/n8n-io/n8n/master/packages/%40n8n/chat/resources/images/fullscreen.png) + ## Prerequisites Create a n8n workflow which you want to execute via chat. The workflow has to be triggered using a **Chat Trigger** node. diff --git a/packages/@n8n/chat/package.json b/packages/@n8n/chat/package.json index 5700b3273e613..ec913f0fa47ca 100644 --- a/packages/@n8n/chat/package.json +++ b/packages/@n8n/chat/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/chat", - "version": "0.6.0", + "version": "0.7.0", "scripts": { "dev": "pnpm run storybook", "build": "pnpm type-check && pnpm build:vite && pnpm run build:individual && npm run build:prepare", @@ -21,13 +21,13 @@ "build:storybook": "storybook build", "release": "pnpm run build:full && cd dist && pnpm publish" }, - "main": "./chat.umd.cjs", + "main": "./chat.umd.js", "module": "./chat.es.js", "types": "./types/index.d.ts", "exports": { ".": { - "import": "./chat.es.js", - "require": "./chat.umd.cjs" + "import": "./index.mjs", + "require": "./index.js" }, "./style.css": { "import": "./style.css", diff --git a/packages/@n8n/client-oauth2/package.json b/packages/@n8n/client-oauth2/package.json index a379529a9771e..85d24f69c8ad5 100644 --- a/packages/@n8n/client-oauth2/package.json +++ b/packages/@n8n/client-oauth2/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/client-oauth2", - "version": "0.11.0", + "version": "0.12.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 724ec76a6b9ac..aae74d2e4a44d 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": "0.8.0", + "version": "0.9.0", "description": "", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index c947c30d65bdf..db2ae5269f854 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -10,12 +10,29 @@ The flag `N8N_CACHE_ENABLED` was removed. The cache is now always enabled. Additionally, expressions in credentials now follow the paired item, so if you have multiple input items, n8n will try to pair the matching row to fill in the credential details. +In the Monday.com Node, due to API changes, the data structure of entries in `column_values` array has changed + ### When is action necessary? If you are using the flag `N8N_CACHE_ENABLED`, remove it from your settings. In regards to credentials, if you use expression in credentials, you might want to revisit them. Previously, n8n would stick to the first item only, but now it will try to match the proper paired item. +If you are using the Monday.com node and refering to `column_values` property, check in table below if you are using any of the affected properties of its entries. + +| Resource | Operation | Previous | New | +| ---------- | ------------------- | --------------- | ------------------- | +| Board | Get | owner | owners | +| Board | Get All | owner | owners | +| Board Item | Get | title | column.title | +| Board Item | Get All | title | column.title | +| Board Item | Get By Column Value | title | column.title | +| Board Item | Get | additional_info | column.settings_str | +| Board Item | Get All | additional_info | column.settings_str | +| Board Item | Get By Column Value | additional_info | column.settings_str | + +\*column.settings_str is not a complete equivalent additional_info + ## 1.22.0 ### What changed? diff --git a/packages/cli/package.json b/packages/cli/package.json index 5a77fc5172684..0a8b3ba08b419 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.23.0", + "version": "1.24.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 89b032ab065ae..32ddc25f65b59 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -226,10 +226,10 @@ export class ExecutionRepository extends Repository { const { data, workflowData, ...rest } = execution; const { identifiers: inserted } = await this.insert(rest); const { id: executionId } = inserted[0] as { id: string }; - const { connections, nodes, name } = workflowData ?? {}; + const { connections, nodes, name, settings } = workflowData ?? {}; await this.executionDataRepository.insert({ executionId, - workflowData: { connections, nodes, name, id: workflowData?.id }, + workflowData: { connections, nodes, name, settings, id: workflowData?.id }, data: stringify(data), }); return String(executionId); diff --git a/packages/cli/src/sso/saml/samlValidator.ts b/packages/cli/src/sso/saml/samlValidator.ts index ff35c0f4cbdb5..66be4c98e0bbb 100644 --- a/packages/cli/src/sso/saml/samlValidator.ts +++ b/packages/cli/src/sso/saml/samlValidator.ts @@ -88,7 +88,13 @@ export async function validateMetadata(metadata: string): Promise { return true; } else { logger.warn('SAML Validate Metadata: Invalid metadata'); - logger.warn(validationResult ? validationResult.errors.join('\n') : ''); + logger.warn( + validationResult + ? validationResult.errors + .map((error) => `${error.message} - ${error.rawMessage}`) + .join('\n') + : '', + ); } } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument @@ -118,7 +124,13 @@ export async function validateResponse(response: string): Promise { return true; } else { logger.warn('SAML Validate Response: Failed'); - logger.warn(validationResult ? validationResult.errors.join('\n') : ''); + logger.warn( + validationResult + ? validationResult.errors + .map((error) => `${error.message} - ${error.rawMessage}`) + .join('\n') + : '', + ); } } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument diff --git a/packages/cli/test/integration/database/repositories/execution.repository.test.ts b/packages/cli/test/integration/database/repositories/execution.repository.test.ts index 72ebaa95969b5..cfb897d627a56 100644 --- a/packages/cli/test/integration/database/repositories/execution.repository.test.ts +++ b/packages/cli/test/integration/database/repositories/execution.repository.test.ts @@ -20,7 +20,7 @@ describe('ExecutionRepository', () => { describe('createNewExecution', () => { it('should save execution data', async () => { const executionRepo = Container.get(ExecutionRepository); - const workflow = await createWorkflow(); + const workflow = await createWorkflow({ settings: { executionOrder: 'v1' } }); const executionId = await executionRepo.createNewExecution({ workflowId: workflow.id, data: { @@ -48,6 +48,7 @@ describe('ExecutionRepository', () => { connections: workflow.connections, nodes: workflow.nodes, name: workflow.name, + settings: workflow.settings, }); expect(executionData?.data).toEqual('[{"resultData":"1"},{}]'); }); diff --git a/packages/core/package.json b/packages/core/package.json index ef6510f04c32e..2c336663fc3be 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "1.23.0", + "version": "1.24.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/design-system/package.json b/packages/design-system/package.json index ebad20d43d21a..9addb27a53ea0 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "n8n-design-system", - "version": "1.17.0", + "version": "1.18.0", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", "author": { diff --git a/packages/design-system/src/composables/useDeviceSupport.test.ts b/packages/design-system/src/composables/useDeviceSupport.test.ts new file mode 100644 index 0000000000000..167053c67901e --- /dev/null +++ b/packages/design-system/src/composables/useDeviceSupport.test.ts @@ -0,0 +1,80 @@ +import { useDeviceSupport } from '@/composables/useDeviceSupport'; + +describe('useDeviceSupport()', () => { + beforeEach(() => { + global.window = Object.create(window); + global.navigator = { userAgent: 'test-agent', maxTouchPoints: 0 }; + }); + + describe('isTouchDevice', () => { + it('should be true if ontouchstart is in window', () => { + Object.defineProperty(window, 'ontouchstart', {}); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(true); + }); + + it('should be true if navigator.maxTouchPoints > 0', () => { + Object.defineProperty(navigator, 'maxTouchPoints', { value: 1 }); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(true); + }); + + it('should be false if no touch support', () => { + delete window.ontouchstart; + Object.defineProperty(navigator, 'maxTouchPoints', { value: 0 }); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(false); + }); + }); + + describe('isMacOs', () => { + it('should be true for macOS user agent', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' }); + const { isMacOs } = useDeviceSupport(); + expect(isMacOs).toEqual(true); + }); + + it('should be false for non-macOS user agent', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'windows' }); + const { isMacOs } = useDeviceSupport(); + expect(isMacOs).toEqual(false); + }); + }); + + describe('controlKeyCode', () => { + it('should return Meta on macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' }); + const { controlKeyCode } = useDeviceSupport(); + expect(controlKeyCode).toEqual('Meta'); + }); + + it('should return Control on non-macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'windows' }); + const { controlKeyCode } = useDeviceSupport(); + expect(controlKeyCode).toEqual('Control'); + }); + }); + + describe('isCtrlKeyPressed()', () => { + it('should return true for metaKey press on macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' }); + const { isCtrlKeyPressed } = useDeviceSupport(); + const event = new KeyboardEvent('keydown', { metaKey: true }); + expect(isCtrlKeyPressed(event)).toEqual(true); + }); + + it('should return true for ctrlKey press on non-macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'windows' }); + const { isCtrlKeyPressed } = useDeviceSupport(); + const event = new KeyboardEvent('keydown', { ctrlKey: true }); + expect(isCtrlKeyPressed(event)).toEqual(true); + }); + + it('should return true for touch device on MouseEvent', () => { + Object.defineProperty(window, 'ontouchstart', { value: {} }); + const { isCtrlKeyPressed } = useDeviceSupport(); + const mockEvent = new MouseEvent('click'); + expect(isCtrlKeyPressed(mockEvent)).toEqual(true); + }); + }); +}); diff --git a/packages/design-system/src/composables/useDeviceSupport.ts b/packages/design-system/src/composables/useDeviceSupport.ts index 6296304578891..01d52b890beae 100644 --- a/packages/design-system/src/composables/useDeviceSupport.ts +++ b/packages/design-system/src/composables/useDeviceSupport.ts @@ -1,14 +1,7 @@ import { ref } from 'vue'; -interface DeviceSupportHelpers { - isTouchDevice: boolean; - isMacOs: boolean; - controlKeyCode: string; - isCtrlKeyPressed: (e: MouseEvent | KeyboardEvent) => boolean; -} - -export function useDeviceSupport(): DeviceSupportHelpers { - const isTouchDevice = ref('ontouchstart' in window || navigator.maxTouchPoints > 0); +export function useDeviceSupport() { + const isTouchDevice = ref(window.hasOwnProperty('ontouchstart') || navigator.maxTouchPoints > 0); const userAgent = ref(navigator.userAgent.toLowerCase()); const isMacOs = ref( userAgent.value.includes('macintosh') || diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 5e6ff25c21268..7ea5b2b8c496a 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "1.23.0", + "version": "1.24.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/editor-ui/src/components/CanvasControls.vue b/packages/editor-ui/src/components/CanvasControls.vue index 3b6d83ae239ab..b5677e54f30d1 100644 --- a/packages/editor-ui/src/components/CanvasControls.vue +++ b/packages/editor-ui/src/components/CanvasControls.vue @@ -62,13 +62,15 @@ import { onBeforeMount, onBeforeUnmount } from 'vue'; import { storeToRefs } from 'pinia'; import { useCanvasStore } from '@/stores/canvas.store'; import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue'; +import { useDeviceSupport } from 'n8n-design-system'; const canvasStore = useCanvasStore(); const { zoomToFit, zoomIn, zoomOut, resetZoom } = canvasStore; const { nodeViewScale, isDemo } = storeToRefs(canvasStore); +const deviceSupport = useDeviceSupport(); const keyDown = (e: KeyboardEvent) => { - const isCtrlKeyPressed = e.metaKey || e.ctrlKey; + const isCtrlKeyPressed = deviceSupport.isCtrlKeyPressed(e); if ((e.key === '=' || e.key === '+') && !isCtrlKeyPressed) { zoomIn(); } else if ((e.key === '_' || e.key === '-') && !isCtrlKeyPressed) { diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 6c9be87f15573..53a69cd78580a 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -14,7 +14,7 @@ :class="{ 'node-default': true, 'touch-active': isTouchActive, - 'is-touch-device': isTouchDevice, + 'is-touch-device': deviceSupport.isTouchDevice, 'menu-open': isContextMenuOpen, 'disable-pointer-events': disablePointerEvents, }" @@ -187,6 +187,7 @@ import { type ContextMenuTarget, useContextMenu } from '@/composables/useContext import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { usePinnedData } from '@/composables/usePinnedData'; +import { useDeviceSupport } from 'n8n-design-system'; import { useDebounce } from '@/composables/useDebounce'; export default defineComponent({ @@ -218,9 +219,17 @@ export default defineComponent({ const nodeHelpers = useNodeHelpers(); const node = workflowsStore.getNodeByName(props.name); const pinnedData = usePinnedData(node); + const deviceSupport = useDeviceSupport(); const { callDebounced } = useDebounce(); - return { contextMenu, externalHooks, nodeHelpers, pinnedData, callDebounced }; + return { + contextMenu, + externalHooks, + nodeHelpers, + pinnedData, + deviceSupport, + callDebounced, + }; }, computed: { ...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore), @@ -698,7 +707,7 @@ export default defineComponent({ this.pinDataDiscoveryTooltipVisible = false; }, touchStart() { - if (this.isTouchDevice === true && !this.isMacOs && !this.isTouchActive) { + if (this.deviceSupport.isTouchDevice && !this.deviceSupport.isMacOs && !this.isTouchActive) { this.isTouchActive = true; setTimeout(() => { this.isTouchActive = false; diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index 88463dbaf8473..e27744d387320 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -169,7 +169,7 @@ import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useUIStore } from '@/stores/ui.store'; import { useSettingsStore } from '@/stores/settings.store'; -import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; +import { useDeviceSupport } from 'n8n-design-system'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useMessage } from '@/composables/useMessage'; import { useExternalHooks } from '@/composables/useExternalHooks'; diff --git a/packages/editor-ui/src/components/Sticky.vue b/packages/editor-ui/src/components/Sticky.vue index 5c194437d2dcf..1042548f15298 100644 --- a/packages/editor-ui/src/components/Sticky.vue +++ b/packages/editor-ui/src/components/Sticky.vue @@ -11,7 +11,7 @@ :class="{ 'sticky-default': true, 'touch-active': isTouchActive, - 'is-touch-device': isTouchDevice, + 'is-touch-device': deviceSupport.isTouchDevice, 'is-read-only': isReadOnly, }" :style="stickySize" @@ -122,6 +122,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useContextMenu } from '@/composables/useContextMenu'; +import { useDeviceSupport } from 'n8n-design-system'; export default defineComponent({ name: 'Sticky', @@ -135,6 +136,7 @@ export default defineComponent({ }, }, setup() { + const deviceSupport = useDeviceSupport(); const colorPopoverTrigger = ref(); const forceActions = ref(false); const setForceActions = (value: boolean) => { @@ -147,7 +149,7 @@ export default defineComponent({ } }); - return { colorPopoverTrigger, contextMenu, forceActions, setForceActions }; + return { deviceSupport, colorPopoverTrigger, contextMenu, forceActions, setForceActions }; }, computed: { ...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore), @@ -318,7 +320,7 @@ export default defineComponent({ this.workflowsStore.updateNodeProperties(updateInformation); }, touchStart() { - if (this.isTouchDevice === true && !this.isMacOs && !this.isTouchActive) { + if (this.deviceSupport.isTouchDevice === true && !this.isMacOs && !this.isTouchActive) { this.isTouchActive = true; setTimeout(() => { this.isTouchActive = false; diff --git a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts index c7d015c01b2f3..a04342db2319b 100644 --- a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts +++ b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts @@ -1,6 +1,6 @@ import type { INodeUi, XYPosition } from '@/Interface'; -import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; +import { useDeviceSupport } from 'n8n-design-system'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { getMousePosition, getRelativePosition } from '@/utils/nodeViewUtils'; diff --git a/packages/editor-ui/src/composables/useHistoryHelper.ts b/packages/editor-ui/src/composables/useHistoryHelper.ts index b259a812eb2de..edba55b12d3a5 100644 --- a/packages/editor-ui/src/composables/useHistoryHelper.ts +++ b/packages/editor-ui/src/composables/useHistoryHelper.ts @@ -6,7 +6,7 @@ import { useHistoryStore } from '@/stores/history.store'; import { useUIStore } from '@/stores/ui.store'; import { onMounted, onUnmounted, nextTick } from 'vue'; -import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; +import { useDeviceSupport } from 'n8n-design-system'; import { getNodeViewTab } from '@/utils/canvasUtils'; import type { Route } from 'vue-router'; import { useTelemetry } from './useTelemetry'; diff --git a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts b/packages/editor-ui/src/mixins/deviceSupportHelpers.ts deleted file mode 100644 index a8f87f8820aee..0000000000000 --- a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { defineComponent } from 'vue'; - -export const deviceSupportHelpers = defineComponent({ - data() { - return { - // @ts-ignore msMaxTouchPoints is deprecated but must fix tablet bugs before fixing this.. otherwise breaks touchscreen computers - isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints, - isMacOs: /(ipad|iphone|ipod|mac)/i.test(navigator.platform), // TODO: `platform` deprecated - }; - }, - computed: { - // TODO: Check if used anywhere - controlKeyCode(): string { - if (this.isMacOs) { - return 'Meta'; - } - return 'Control'; - }, - }, - methods: { - isCtrlKeyPressed(e: MouseEvent | KeyboardEvent): boolean { - if (this.isTouchDevice === true && e instanceof MouseEvent) { - return true; - } - if (this.isMacOs) { - return e.metaKey; - } - return e.ctrlKey; - }, - }, -}); diff --git a/packages/editor-ui/src/mixins/moveNodeWorkflow.ts b/packages/editor-ui/src/mixins/moveNodeWorkflow.ts index 1ae6d2743a8d8..8e91af0ab018c 100644 --- a/packages/editor-ui/src/mixins/moveNodeWorkflow.ts +++ b/packages/editor-ui/src/mixins/moveNodeWorkflow.ts @@ -1,12 +1,11 @@ import { defineComponent } from 'vue'; import { mapStores } from 'pinia'; -import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { getMousePosition } from '@/utils/nodeViewUtils'; import { useUIStore } from '@/stores/ui.store'; +import { useDeviceSupport } from 'n8n-design-system'; export const moveNodeWorkflow = defineComponent({ - mixins: [deviceSupportHelpers], data() { return { moveLastPosition: [0, 0], @@ -30,7 +29,9 @@ export const moveNodeWorkflow = defineComponent({ this.moveLastPosition[1] = y; }, mouseDownMoveWorkflow(e: MouseEvent, moveButtonPressed: boolean) { - if (!this.isCtrlKeyPressed(e) && !moveButtonPressed) { + const deviceSupport = useDeviceSupport(); + + if (!deviceSupport.isCtrlKeyPressed(e) && !moveButtonPressed) { // We only care about it when the ctrl key is pressed at the same time. // So we exit when it is not pressed. return; diff --git a/packages/editor-ui/src/mixins/nodeBase.ts b/packages/editor-ui/src/mixins/nodeBase.ts index 877c8f2765274..6d9193250c11d 100644 --- a/packages/editor-ui/src/mixins/nodeBase.ts +++ b/packages/editor-ui/src/mixins/nodeBase.ts @@ -3,7 +3,6 @@ import type { PropType } from 'vue'; import { mapStores } from 'pinia'; import type { INodeUi } from '@/Interface'; -import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { NO_OP_NODE_TYPE, NODE_CONNECTION_TYPE_ALLOW_MULTIPLE, @@ -28,6 +27,7 @@ import * as NodeViewUtils from '@/utils/nodeViewUtils'; import { useHistoryStore } from '@/stores/history.store'; import { useCanvasStore } from '@/stores/canvas.store'; import type { EndpointSpec } from '@jsplumb/common'; +import { useDeviceSupport } from 'n8n-design-system'; const createAddInputEndpointSpec = ( connectionName: NodeConnectionType, @@ -56,7 +56,6 @@ const createDiamondOutputEndpointSpec = (): EndpointSpec => ({ }); export const nodeBase = defineComponent({ - mixins: [deviceSupportHelpers], data() { return { inputs: [] as Array, @@ -615,13 +614,16 @@ export const nodeBase = defineComponent({ return createSupplementalConnectionType(connectionType); }, touchEnd(e: MouseEvent) { - if (this.isTouchDevice) { + const deviceSupport = useDeviceSupport(); + if (deviceSupport.isTouchDevice) { if (this.uiStore.isActionActive('dragActive')) { this.uiStore.removeActiveAction('dragActive'); } } }, mouseLeftClick(e: MouseEvent) { + const deviceSupport = useDeviceSupport(); + // @ts-ignore const path = e.path || (e.composedPath && e.composedPath()); for (let index = 0; index < path.length; index++) { @@ -634,11 +636,11 @@ export const nodeBase = defineComponent({ } } - if (!this.isTouchDevice) { + if (!deviceSupport.isTouchDevice) { if (this.uiStore.isActionActive('dragActive')) { this.uiStore.removeActiveAction('dragActive'); } else { - if (!this.isCtrlKeyPressed(e)) { + if (!deviceSupport.isCtrlKeyPressed(e)) { this.$emit('deselectAllNodes'); } diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index ff69d63469d57..f06e4c7e31c22 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -377,6 +377,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks'; import { useClipboard } from '@/composables/useClipboard'; import { usePinnedData } from '@/composables/usePinnedData'; import { useSourceControlStore } from '@/stores/sourceControl.store'; +import { useDeviceSupport } from 'n8n-design-system'; import { useDebounce } from '@/composables/useDebounce'; interface AddNodeOptions { @@ -476,6 +477,7 @@ export default defineComponent({ const clipboard = useClipboard(); const { activeNode } = storeToRefs(ndvStore); const pinnedData = usePinnedData(activeNode); + const deviceSupport = useDeviceSupport(); const { callDebounced } = useDebounce(); return { @@ -486,6 +488,7 @@ export default defineComponent({ externalHooks, clipboard, pinnedData, + deviceSupport, callDebounced, ...useCanvasMouseSelect(), ...useGlobalLinkActions(), @@ -1378,7 +1381,7 @@ export default defineComponent({ this.collaborationStore.notifyWorkflowOpened(workflow.id); }, touchTap(e: MouseEvent | TouchEvent) { - if (this.isTouchDevice) { + if (this.deviceSupport.isTouchDevice) { this.mouseDown(e); } }, @@ -1403,7 +1406,7 @@ export default defineComponent({ this.mouseUpMoveWorkflow(e); }, keyUp(e: KeyboardEvent) { - if (e.key === this.controlKeyCode) { + if (e.key === this.deviceSupport.controlKeyCode) { this.ctrlKeyPressed = false; } if (e.key === ' ') { @@ -1413,10 +1416,10 @@ export default defineComponent({ async keyDown(e: KeyboardEvent) { this.contextMenu.close(); - const ctrlModifier = this.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; - const shiftModifier = e.shiftKey && !e.altKey && !this.isCtrlKeyPressed(e); - const ctrlAltModifier = this.isCtrlKeyPressed(e) && e.altKey && !e.shiftKey; - const noModifierKeys = !this.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; + const ctrlModifier = this.deviceSupport.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; + const shiftModifier = e.shiftKey && !e.altKey && !this.deviceSupport.isCtrlKeyPressed(e); + const ctrlAltModifier = this.deviceSupport.isCtrlKeyPressed(e) && e.altKey && !e.shiftKey; + const noModifierKeys = !this.deviceSupport.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; const readOnly = this.isReadOnlyRoute || this.readOnlyEnv; if (e.key === 's' && ctrlModifier && !readOnly) { @@ -1497,7 +1500,7 @@ export default defineComponent({ void this.onRunWorkflow(); } else if (e.key === 'S' && shiftModifier && !readOnly) { void this.onAddNodes({ nodes: [{ type: STICKY_NODE_TYPE }], connections: [] }); - } else if (e.key === this.controlKeyCode) { + } else if (e.key === this.deviceSupport.controlKeyCode) { this.ctrlKeyPressed = true; } else if (e.key === ' ') { this.moveCanvasKeyPressed = true; diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts index d6c2beba9a529..898fd81402bf4 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts @@ -153,6 +153,16 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => { wf_template_repo_session_id: templatesStore.currentSessionId, }); + telemetry.track( + 'User inserted workflow template', + { + source: 'workflow', + template_id: templateId.value, + wf_template_repo_session_id: templatesStore.currentSessionId, + }, + { withPostHog: true }, + ); + telemetry.track('User closed cred setup', { completed: false, creds_filled: 0, @@ -196,14 +206,20 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => { workflow_id: createdWorkflow.id, }); - const telemetryPayload = { - source: 'workflow', - template_id: template.value.id, + telemetry.track( + 'User inserted workflow template', + { + source: 'workflow', + template_id: templateId.value, + wf_template_repo_session_id: templatesStore.currentSessionId, + }, + { withPostHog: true }, + ); + + telemetry.track('User saved new workflow from template', { + template_id: templateId.value, + workflow_id: createdWorkflow.id, wf_template_repo_session_id: templatesStore.currentSessionId, - }; - - telemetry.track('User inserted workflow template', telemetryPayload, { - withPostHog: true, }); // Replace the URL so back button doesn't come back to this setup view diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 4afd3320d6b5a..bf2a302b43ace 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "1.23.0", + "version": "1.24.0", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/nodes-base/credentials/MondayComApi.credentials.ts b/packages/nodes-base/credentials/MondayComApi.credentials.ts index abd31133fd440..d82677b090828 100644 --- a/packages/nodes-base/credentials/MondayComApi.credentials.ts +++ b/packages/nodes-base/credentials/MondayComApi.credentials.ts @@ -1,4 +1,9 @@ -import type { ICredentialType, INodeProperties } from 'n8n-workflow'; +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; export class MondayComApi implements ICredentialType { name = 'mondayComApi'; @@ -16,4 +21,27 @@ export class MondayComApi implements ICredentialType { default: '', }, ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '=Bearer {{$credentials.apiToken}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + headers: { + 'API-Version': '2023-10', + 'Content-Type': 'application/json', + }, + baseURL: 'https://api.monday.com/v2', + method: 'POST', + body: JSON.stringify({ + query: 'query { me { name }}', + }), + }, + }; } diff --git a/packages/nodes-base/credentials/MongoDb.credentials.ts b/packages/nodes-base/credentials/MongoDb.credentials.ts index f018e67b59932..495e9ed5b83f2 100644 --- a/packages/nodes-base/credentials/MongoDb.credentials.ts +++ b/packages/nodes-base/credentials/MongoDb.credentials.ts @@ -96,5 +96,67 @@ export class MongoDb implements ICredentialType { }, default: 27017, }, + { + displayName: 'Use TLS', + name: 'tls', + type: 'boolean', + default: false, + }, + { + displayName: 'CA Certificate', + name: 'ca', + type: 'string', + typeOptions: { + password: true, + }, + displayOptions: { + show: { + tls: [true], + }, + }, + default: '', + }, + { + displayName: 'Public Client Certificate', + name: 'cert', + type: 'string', + typeOptions: { + password: true, + }, + displayOptions: { + show: { + tls: [true], + }, + }, + default: '', + }, + { + displayName: 'Private Client Key', + name: 'key', + type: 'string', + typeOptions: { + password: true, + }, + displayOptions: { + show: { + tls: [true], + }, + }, + default: '', + }, + { + displayName: 'Passphrase', + name: 'passphrase', + type: 'string', + typeOptions: { + password: true, + }, + displayOptions: { + show: { + tls: [true], + }, + }, + default: '', + }, ]; } diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheetsTrigger.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheetsTrigger.node.ts index 6745b5f84ead8..5686e5dceb3dc 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheetsTrigger.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheetsTrigger.node.ts @@ -12,7 +12,7 @@ import { apiRequest } from './v2/transport'; import { sheetsSearch, spreadSheetsSearch } from './v2/methods/listSearch'; import { GoogleSheet } from './v2/helpers/GoogleSheet'; import { getSheetHeaderRowAndSkipEmpty } from './v2/methods/loadOptions'; -import type { ValueRenderOption } from './v2/helpers/GoogleSheets.types'; +import type { ResourceLocator, ValueRenderOption } from './v2/helpers/GoogleSheets.types'; import { arrayOfArraysToJson, @@ -399,11 +399,21 @@ export class GoogleSheetsTrigger implements INodeType { extractValue: true, }) as string; - let sheetId = this.getNodeParameter('sheetName', undefined, { + const sheetWithinDocument = this.getNodeParameter('sheetName', undefined, { extractValue: true, }) as string; + const { mode: sheetMode } = this.getNodeParameter('sheetName', 0) as { + mode: ResourceLocator; + }; - sheetId = sheetId === 'gid=0' ? '0' : sheetId; + const googleSheet = new GoogleSheet(documentId, this); + const { sheetId, title: sheetName } = await googleSheet.spreadsheetGetSheet( + this.getNode(), + sheetMode, + sheetWithinDocument, + ); + + const options = this.getNodeParameter('options') as IDataObject; // If the documentId or sheetId changed, reset the workflow static data if ( @@ -417,13 +427,6 @@ export class GoogleSheetsTrigger implements INodeType { workflowStaticData.lastIndexChecked = undefined; } - const googleSheet = new GoogleSheet(documentId, this); - const sheetName: string = await googleSheet.spreadsheetGetSheetNameById( - this.getNode(), - sheetId, - ); - const options = this.getNodeParameter('options') as IDataObject; - const previousRevision = workflowStaticData.lastRevision as number; const previousRevisionLink = workflowStaticData.lastRevisionLink as string; diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/router.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/router.ts index b793e5a179149..591567c6f8ca8 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/router.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/router.ts @@ -29,17 +29,25 @@ export async function router(this: IExecuteFunctions): Promise item.properties.sheetId === +sheetId, - ); + const foundItem = response.sheets.find((item) => { + if (mode === 'name') return item.properties.title === value; + return item.properties.sheetId === getSheetId(value); + }); if (!foundItem?.properties?.title) { - throw new NodeOperationError(node, `Sheet with ID ${sheetId} not found`, { - level: 'warning', - }); + throw new NodeOperationError( + node, + `Sheet with ${mode === 'name' ? 'name' : 'ID'} ${value} not found`, + { level: 'warning' }, + ); } - return foundItem.properties.title; + return foundItem.properties; } /** diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts index a55fc71ae4dd7..ef643d9953574 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts @@ -62,12 +62,24 @@ export type GoogleSheetsSheet = Entity; export type SpreadSheetProperties = PropertiesOf; export type SheetProperties = PropertiesOf; -export type ResourceLocator = 'id' | 'url' | 'list'; +export type ResourceLocator = 'id' | 'url' | 'list' | 'name'; export const ResourceLocatorUiNames = { id: 'By ID', url: 'By URL', list: 'From List', + name: 'By Name', +}; + +type SpreadSheetResponseSheet = { + properties: { + title: string; + sheetId: number; + }; +}; + +export type SpreadSheetResponse = { + sheets: SpreadSheetResponseSheet[]; }; export type SheetCellDecoded = { diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.utils.ts b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.utils.ts index 47cc9df9d5de4..be64a34e83e81 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.utils.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.utils.ts @@ -45,6 +45,11 @@ export function getSpreadsheetId( return value; } +export function getSheetId(value: string): number { + if (value === 'gid=0') return 0; + return parseInt(value); +} + // Convert number to Sheets / Excel column name export function getColumnName(colNumber: number): string { const baseChar = 'A'.charCodeAt(0); diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/Google/Sheet/v2/methods/loadOptions.ts index a09f55b736576..90aa11cf90ab9 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/methods/loadOptions.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/methods/loadOptions.ts @@ -47,15 +47,18 @@ export async function getSheetHeaderRow( const spreadsheetId = getSpreadsheetId(this.getNode(), mode as ResourceLocator, value as string); const sheet = new GoogleSheet(spreadsheetId, this); - let sheetWithinDocument = this.getNodeParameter('sheetName', undefined, { + const sheetWithinDocument = this.getNodeParameter('sheetName', undefined, { extractValue: true, }) as string; - - if (sheetWithinDocument === 'gid=0') { - sheetWithinDocument = '0'; - } - - const sheetName = await sheet.spreadsheetGetSheetNameById(this.getNode(), sheetWithinDocument); + const { mode: sheetMode } = this.getNodeParameter('sheetName', 0) as { + mode: ResourceLocator; + }; + + const { title: sheetName } = await sheet.spreadsheetGetSheet( + this.getNode(), + sheetMode, + sheetWithinDocument, + ); const sheetData = await sheet.getData(`${sheetName}!1:1`, 'FORMATTED_VALUE'); if (sheetData === undefined) { diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/methods/resourceMapping.ts b/packages/nodes-base/nodes/Google/Sheet/v2/methods/resourceMapping.ts index f29a2a7844949..40ae402a646a4 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/methods/resourceMapping.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/methods/resourceMapping.ts @@ -20,15 +20,16 @@ export async function getMappingColumns( const spreadsheetId = getSpreadsheetId(this.getNode(), mode as ResourceLocator, value as string); const sheet = new GoogleSheet(spreadsheetId, this); - let sheetWithinDocument = this.getNodeParameter('sheetName', undefined, { + const sheetWithinDocument = this.getNodeParameter('sheetName', undefined, { extractValue: true, }) as string; + const { mode: sheetMode } = this.getNodeParameter('sheetName', 0) as { mode: ResourceLocator }; - if (sheetWithinDocument === 'gid=0') { - sheetWithinDocument = '0'; - } - - const sheetName = await sheet.spreadsheetGetSheetNameById(this.getNode(), sheetWithinDocument); + const { title: sheetName } = await sheet.spreadsheetGetSheet( + this.getNode(), + sheetMode, + sheetWithinDocument, + ); const sheetData = await sheet.getData(`${sheetName}!1:1`, 'FORMATTED_VALUE'); const columns = sheet.testFilter(sheetData || [], 0, 0).filter((col) => col !== ''); diff --git a/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts b/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts index 46b3fe61c9e9c..6bf4e0a98fdb3 100644 --- a/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts @@ -994,7 +994,7 @@ export class HttpRequestV1 implements INodeType { } response = response.value; - delete response.request; + if (response?.request?.constructor.name === 'ClientRequest') delete response.request; const options = this.getNodeParameter('options', itemIndex, {}); diff --git a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts index 82941b01db53b..2c5d78f206ea5 100644 --- a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts @@ -1047,7 +1047,7 @@ export class HttpRequestV2 implements INodeType { } response = response.value; - delete response.request; + if (response?.request?.constructor.name === 'ClientRequest') delete response.request; const options = this.getNodeParameter('options', itemIndex, {}); diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index 0f60254037bd1..c6fabfa41e746 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -1786,7 +1786,8 @@ export class HttpRequestV3 implements INodeType { // eslint-disable-next-line prefer-const for (let [index, response] of Object.entries(responses)) { - delete response.request; + if (response?.request?.constructor.name === 'ClientRequest') delete response.request; + if (this.getMode() === 'manual' && index === '0') { // For manual executions save the first response in the context // so that we can use it in the frontend and so make it easier for diff --git a/packages/nodes-base/nodes/MondayCom/GenericFunctions.ts b/packages/nodes-base/nodes/MondayCom/GenericFunctions.ts index c9d39eb71e820..07f017ff1bf84 100644 --- a/packages/nodes-base/nodes/MondayCom/GenericFunctions.ts +++ b/packages/nodes-base/nodes/MondayCom/GenericFunctions.ts @@ -14,34 +14,31 @@ import get from 'lodash/get'; export async function mondayComApiRequest( this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, - body: any = {}, option: IDataObject = {}, ): Promise { const authenticationMethod = this.getNodeParameter('authentication', 0) as string; - const endpoint = 'https://api.monday.com/v2/'; - let options: OptionsWithUri = { headers: { + 'API-Version': '2023-10', 'Content-Type': 'application/json', }, method: 'POST', body, - uri: endpoint, + uri: 'https://api.monday.com/v2/', json: true, }; + options = Object.assign({}, options, option); - try { - if (authenticationMethod === 'accessToken') { - const credentials = await this.getCredentials('mondayComApi'); - options.headers = { Authorization: `Bearer ${credentials.apiToken}` }; + try { + let credentialType = 'mondayComApi'; - return await this.helpers.request(options); - } else { - return await this.helpers.requestOAuth2.call(this, 'mondayComOAuth2Api', options); + if (authenticationMethod === 'oAuth2') { + credentialType = 'mondayComOAuth2Api'; } + return await this.helpers.requestWithAuthentication.call(this, credentialType, options); } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); } @@ -50,7 +47,6 @@ export async function mondayComApiRequest( export async function mondayComApiRequestAllItems( this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, - body: any = {}, ): Promise { const returnData: IDataObject[] = []; @@ -66,3 +62,41 @@ export async function mondayComApiRequestAllItems( } while (get(responseData, propertyName).length > 0); return returnData; } + +export async function mondayComApiPaginatedRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + itemsPath: string, + fieldsToReturn: string, + body: IDataObject = {}, +) { + const returnData: IDataObject[] = []; + + const initialResponse = await mondayComApiRequest.call(this, body); + const data = get(initialResponse, itemsPath) as IDataObject; + + if (data) { + returnData.push.apply(returnData, data.items as IDataObject[]); + + let cursor: null | string = data.cursor as string; + + while (cursor) { + const responseData = ( + (await mondayComApiRequest.call(this, { + query: `query ( $cursor: String!) { next_items_page (cursor: $cursor, limit: 100) { cursor items ${fieldsToReturn} } }`, + variables: { + cursor, + }, + })) as IDataObject + ).data as { next_items_page: { cursor: string; items: IDataObject[] } }; + + if (responseData && responseData.next_items_page) { + returnData.push.apply(returnData, responseData.next_items_page.items); + cursor = responseData.next_items_page.cursor; + } else { + cursor = null; + } + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts index 1584e2fa693fb..2056616caa593 100644 --- a/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts +++ b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts @@ -10,7 +10,11 @@ import type { import { NodeOperationError } from 'n8n-workflow'; import { snakeCase } from 'change-case'; -import { mondayComApiRequest, mondayComApiRequestAllItems } from './GenericFunctions'; +import { + mondayComApiPaginatedRequest, + mondayComApiRequest, + mondayComApiRequestAllItems, +} from './GenericFunctions'; import { boardFields, boardOperations } from './BoardDescription'; @@ -155,18 +159,17 @@ export class MondayCom implements INodeType { // select them easily async getColumns(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const boardId = parseInt(this.getCurrentNodeParameter('boardId') as string, 10); + const boardId = this.getCurrentNodeParameter('boardId') as string; const body: IGraphqlBody = { - query: `query ($boardId: [Int]) { + query: `query ($boardId: [ID!]) { boards (ids: $boardId){ - columns() { + columns { id title } } }`, variables: { - page: 1, boardId, }, }; @@ -190,11 +193,11 @@ export class MondayCom implements INodeType { // select them easily async getGroups(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const boardId = parseInt(this.getCurrentNodeParameter('boardId') as string, 10); + const boardId = this.getCurrentNodeParameter('boardId') as string; const body = { - query: `query ($boardId: Int!) { + query: `query ($boardId: ID!) { boards ( ids: [$boardId]){ - groups () { + groups { id title } @@ -234,10 +237,10 @@ export class MondayCom implements INodeType { try { if (resource === 'board') { if (operation === 'archive') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const body: IGraphqlBody = { - query: `mutation ($id: Int!) { + query: `mutation ($id: ID!) { archive_board (board_id: $id) { id } @@ -256,7 +259,7 @@ export class MondayCom implements INodeType { const additionalFields = this.getNodeParameter('additionalFields', i); const body: IGraphqlBody = { - query: `mutation ($name: String!, $kind: BoardKind!, $templateId: Int) { + query: `mutation ($name: String!, $kind: BoardKind!, $templateId: ID) { create_board (board_name: $name, board_kind: $kind, template_id: $templateId) { id } @@ -275,10 +278,10 @@ export class MondayCom implements INodeType { responseData = responseData.data.create_board; } if (operation === 'get') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const body: IGraphqlBody = { - query: `query ($id: [Int]) { + query: `query ($id: [ID!]) { boards (ids: $id){ id name @@ -286,7 +289,7 @@ export class MondayCom implements INodeType { state board_folder_id board_kind - owner() { + owners { id } } @@ -311,7 +314,7 @@ export class MondayCom implements INodeType { state board_folder_id board_kind - owner() { + owners { id } } @@ -332,13 +335,13 @@ export class MondayCom implements INodeType { } if (resource === 'boardColumn') { if (operation === 'create') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const title = this.getNodeParameter('title', i) as string; const columnType = this.getNodeParameter('columnType', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i); const body: IGraphqlBody = { - query: `mutation ($boardId: Int!, $title: String!, $columnType: ColumnType, $defaults: JSON ) { + query: `mutation ($boardId: ID!, $title: String!, $columnType: ColumnType!, $defaults: JSON ) { create_column (board_id: $boardId, title: $title, column_type: $columnType, defaults: $defaults) { id } @@ -367,12 +370,12 @@ export class MondayCom implements INodeType { responseData = responseData.data.create_column; } if (operation === 'getAll') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const body: IGraphqlBody = { - query: `query ($boardId: [Int]) { + query: `query ($boardId: [ID!]) { boards (ids: $boardId){ - columns() { + columns { id title type @@ -393,11 +396,11 @@ export class MondayCom implements INodeType { } if (resource === 'boardGroup') { if (operation === 'create') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const name = this.getNodeParameter('name', i) as string; const body: IGraphqlBody = { - query: `mutation ($boardId: Int!, $groupName: String!) { + query: `mutation ($boardId: ID!, $groupName: String!) { create_group (board_id: $boardId, group_name: $groupName) { id } @@ -412,11 +415,11 @@ export class MondayCom implements INodeType { responseData = responseData.data.create_group; } if (operation === 'delete') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const groupId = this.getNodeParameter('groupId', i) as string; const body: IGraphqlBody = { - query: `mutation ($boardId: Int!, $groupId: String!) { + query: `mutation ($boardId: ID!, $groupId: String!) { delete_group (board_id: $boardId, group_id: $groupId) { id } @@ -431,13 +434,13 @@ export class MondayCom implements INodeType { responseData = responseData.data.delete_group; } if (operation === 'getAll') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const body: IGraphqlBody = { - query: `query ($boardId: [Int]) { + query: `query ($boardId: [ID!]) { boards (ids: $boardId, ){ id - groups() { + groups { id title color @@ -457,11 +460,11 @@ export class MondayCom implements INodeType { } if (resource === 'boardItem') { if (operation === 'addUpdate') { - const itemId = parseInt(this.getNodeParameter('itemId', i) as string, 10); + const itemId = this.getNodeParameter('itemId', i); const value = this.getNodeParameter('value', i) as string; const body: IGraphqlBody = { - query: `mutation ($itemId: Int!, $value: String!) { + query: `mutation ($itemId: ID!, $value: String!) { create_update (item_id: $itemId, body: $value) { id } @@ -476,13 +479,13 @@ export class MondayCom implements INodeType { responseData = responseData.data.create_update; } if (operation === 'changeColumnValue') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); - const itemId = parseInt(this.getNodeParameter('itemId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); + const itemId = this.getNodeParameter('itemId', i); const columnId = this.getNodeParameter('columnId', i) as string; const value = this.getNodeParameter('value', i) as string; const body: IGraphqlBody = { - query: `mutation ($boardId: Int!, $itemId: Int!, $columnId: String!, $value: JSON!) { + query: `mutation ($boardId: ID!, $itemId: ID!, $columnId: String!, $value: JSON!) { change_column_value (board_id: $boardId, item_id: $itemId, column_id: $columnId, value: $value) { id } @@ -507,12 +510,12 @@ export class MondayCom implements INodeType { responseData = responseData.data.change_column_value; } if (operation === 'changeMultipleColumnValues') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); - const itemId = parseInt(this.getNodeParameter('itemId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); + const itemId = this.getNodeParameter('itemId', i); const columnValues = this.getNodeParameter('columnValues', i) as string; const body: IGraphqlBody = { - query: `mutation ($boardId: Int!, $itemId: Int!, $columnValues: JSON!) { + query: `mutation ($boardId: ID!, $itemId: ID!, $columnValues: JSON!) { change_multiple_column_values (board_id: $boardId, item_id: $itemId, column_values: $columnValues) { id } @@ -536,13 +539,13 @@ export class MondayCom implements INodeType { responseData = responseData.data.change_multiple_column_values; } if (operation === 'create') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const groupId = this.getNodeParameter('groupId', i) as string; const itemName = this.getNodeParameter('name', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i); const body: IGraphqlBody = { - query: `mutation ($boardId: Int!, $groupId: String!, $itemName: String!, $columnValues: JSON) { + query: `mutation ($boardId: ID!, $groupId: String!, $itemName: String!, $columnValues: JSON) { create_item (board_id: $boardId, group_id: $groupId, item_name: $itemName, column_values: $columnValues) { id } @@ -571,10 +574,10 @@ export class MondayCom implements INodeType { responseData = responseData.data.create_item; } if (operation === 'delete') { - const itemId = parseInt(this.getNodeParameter('itemId', i) as string, 10); + const itemId = this.getNodeParameter('itemId', i); const body: IGraphqlBody = { - query: `mutation ($itemId: Int!) { + query: `mutation ($itemId: ID!) { delete_item (item_id: $itemId) { id } @@ -587,24 +590,27 @@ export class MondayCom implements INodeType { responseData = responseData.data.delete_item; } if (operation === 'get') { - const itemIds = (this.getNodeParameter('itemId', i) as string) - .split(',') - .map((n) => parseInt(n, 10)); + const itemIds = (this.getNodeParameter('itemId', i) as string).split(','); const body: IGraphqlBody = { - query: `query ($itemId: [Int!]){ + query: `query ($itemId: [ID!]){ items (ids: $itemId) { id name created_at state - column_values() { + column_values { id text - title type value - additional_info + column { + + title + archived + description + settings_str + } } } }`, @@ -616,101 +622,128 @@ export class MondayCom implements INodeType { responseData = responseData.data.items; } if (operation === 'getAll') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const groupId = this.getNodeParameter('groupId', i) as string; const returnAll = this.getNodeParameter('returnAll', i); - const body: IGraphqlBody = { - query: `query ($boardId: [Int], $groupId: [String], $page: Int, $limit: Int) { - boards (ids: $boardId) { - groups (ids: $groupId) { - id - items(limit: $limit, page: $page) { - id - name - created_at - state - column_values() { - id - text - title - type - value - additional_info - } - } + const fieldsToReturn = ` + { + id + name + created_at + state + column_values { + id + text + type + value + column { + title + archived + description + settings_str + } + } + } + `; + + const body = { + query: `query ($boardId: [ID!], $groupId: [String], $limit: Int) { + boards(ids: $boardId) { + groups(ids: $groupId) { + id + items_page(limit: $limit) { + cursor + items ${fieldsToReturn} } } - }`, + } + }`, variables: { boardId, groupId, + limit: 100, }, }; if (returnAll) { - responseData = await mondayComApiRequestAllItems.call( + responseData = await mondayComApiPaginatedRequest.call( this, - 'data.boards[0].groups[0].items', - body, + 'data.boards[0].groups[0].items_page', + fieldsToReturn, + body as IDataObject, ); } else { body.variables.limit = this.getNodeParameter('limit', i); responseData = await mondayComApiRequest.call(this, body); - responseData = responseData.data.boards[0].groups[0].items; + responseData = responseData.data.boards[0].groups[0].items_page.items; } } if (operation === 'getByColumnValue') { - const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10); + const boardId = this.getNodeParameter('boardId', i); const columnId = this.getNodeParameter('columnId', i) as string; const columnValue = this.getNodeParameter('columnValue', i) as string; const returnAll = this.getNodeParameter('returnAll', i); - const body: IGraphqlBody = { - query: `query ($boardId: Int!, $columnId: String!, $columnValue: String!, $page: Int, $limit: Int ){ - items_by_column_values (board_id: $boardId, column_id: $columnId, column_value: $columnValue, page: $page, limit: $limit) { - id - name - created_at - state - board { - id - } - column_values() { - id - text - title - type - value - additional_info - } - } - }`, + const fieldsToReturn = `{ + id + name + created_at + state + board { + id + } + column_values { + id + text + type + value + column { + title + archived + description + settings_str + } + } + }`; + const body = { + query: `query ($boardId: ID!, $columnId: String!, $columnValue: String!, $limit: Int) { + items_page_by_column_values( + limit: $limit + board_id: $boardId + columns: [{column_id: $columnId, column_values: [$columnValue]}] + ) { + cursor + items ${fieldsToReturn} + } + }`, variables: { boardId, columnId, columnValue, + limit: 100, }, }; if (returnAll) { - responseData = await mondayComApiRequestAllItems.call( + responseData = await mondayComApiPaginatedRequest.call( this, - 'data.items_by_column_values', - body, + 'data.items_page_by_column_values', + fieldsToReturn, + body as IDataObject, ); } else { body.variables.limit = this.getNodeParameter('limit', i); responseData = await mondayComApiRequest.call(this, body); - responseData = responseData.data.items_by_column_values; + responseData = responseData.data.items_page_by_column_values.items; } } if (operation === 'move') { const groupId = this.getNodeParameter('groupId', i) as string; - const itemId = parseInt(this.getNodeParameter('itemId', i) as string, 10); + const itemId = this.getNodeParameter('itemId', i); const body: IGraphqlBody = { - query: `mutation ($groupId: String!, $itemId: Int!) { + query: `mutation ($groupId: String!, $itemId: ID!) { move_item_to_group (group_id: $groupId, item_id: $itemId) { id } diff --git a/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts b/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts index 1be13325373ad..3dc6be03abfa9 100644 --- a/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts +++ b/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts @@ -8,13 +8,16 @@ import { NodeOperationError } from 'n8n-workflow'; import get from 'lodash/get'; import set from 'lodash/set'; -import { ObjectId } from 'mongodb'; +import { MongoClient, ObjectId } from 'mongodb'; import type { IMongoCredentials, IMongoCredentialsType, IMongoParametricCredentials, } from './mongoDb.types'; +import { createSecureContext } from 'tls'; +import { formatPrivateKey } from '../../utils/utilities'; + /** * Standard way of building the MongoDB connection string, unless overridden with a provided string * @@ -140,3 +143,30 @@ export function stringifyObjectIDs(items: IDataObject[]) { } }); } + +export async function connectMongoClient(connectionString: string, credentials: IDataObject = {}) { + let client: MongoClient; + + if (credentials.tls) { + const ca = credentials.ca ? formatPrivateKey(credentials.ca as string) : undefined; + const cert = credentials.cert ? formatPrivateKey(credentials.cert as string) : undefined; + const key = credentials.key ? formatPrivateKey(credentials.key as string) : undefined; + const passphrase = (credentials.passphrase as string) || undefined; + + const secureContext = createSecureContext({ + ca, + cert, + key, + passphrase, + }); + + client = await MongoClient.connect(connectionString, { + tls: true, + secureContext, + }); + } else { + client = await MongoClient.connect(connectionString); + } + + return client; +} diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index 304abe6c38772..f506b26d5592f 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -17,12 +17,13 @@ import type { UpdateOptions, Sort, } from 'mongodb'; -import { MongoClient, ObjectId } from 'mongodb'; +import { ObjectId } from 'mongodb'; import { generatePairedItemData } from '../../utils/utilities'; import { nodeProperties } from './MongoDbProperties'; import { buildParameterizedConnString, + connectMongoClient, prepareFields, prepareItems, stringifyObjectIDs, @@ -74,7 +75,7 @@ export class MongoDb implements INodeType { ); } - const client: MongoClient = await MongoClient.connect(connectionString); + const client = await connectMongoClient(connectionString, credentials); const { databases } = await client.db().admin().listDatabases(); @@ -100,12 +101,10 @@ export class MongoDb implements INodeType { }; async execute(this: IExecuteFunctions): Promise { - const { database, connectionString } = validateAndResolveMongoCredentials( - this, - await this.getCredentials('mongoDb'), - ); + const credentials = await this.getCredentials('mongoDb'); + const { database, connectionString } = validateAndResolveMongoCredentials(this, credentials); - const client: MongoClient = await MongoClient.connect(connectionString); + const client = await connectMongoClient(connectionString, credentials); const mdb = client.db(database); diff --git a/packages/nodes-base/nodes/MySql/test/v2/utils.test.ts b/packages/nodes-base/nodes/MySql/test/v2/utils.test.ts index 15c7bd2847f28..1f98b9adf2884 100644 --- a/packages/nodes-base/nodes/MySql/test/v2/utils.test.ts +++ b/packages/nodes-base/nodes/MySql/test/v2/utils.test.ts @@ -7,6 +7,7 @@ import { addWhereClauses, addSortRules, replaceEmptyStringsByNulls, + escapeSqlIdentifier, } from '../../v2/helpers/utils'; const mySqlMockNode: INode = { @@ -148,3 +149,29 @@ describe('Test MySql V2, replaceEmptyStringsByNulls', () => { expect(replacedData).toEqual([{ json: { id: 1, name: '' } }]); }); }); + +describe('Test MySql V2, escapeSqlIdentifier', () => { + it('should escape fully qualified identifier', () => { + const input = 'db_name.tbl_name.col_name'; + const escapedIdentifier = escapeSqlIdentifier(input); + expect(escapedIdentifier).toEqual('`db_name`.`tbl_name`.`col_name`'); + }); + + it('should escape table name only', () => { + const input = 'tbl_name'; + const escapedIdentifier = escapeSqlIdentifier(input); + expect(escapedIdentifier).toEqual('`tbl_name`'); + }); + + it('should escape fully qualified identifier with backticks', () => { + const input = '`db_name`.`tbl_name`.`col_name`'; + const escapedIdentifier = escapeSqlIdentifier(input); + expect(escapedIdentifier).toEqual('`db_name`.`tbl_name`.`col_name`'); + }); + + it('should escape identifier with dots', () => { + const input = '`db_name`.`some.dotted.tbl_name`'; + const escapedIdentifier = escapeSqlIdentifier(input); + expect(escapedIdentifier).toEqual('`db_name`.`some.dotted.tbl_name`'); + }); +}); diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/deleteTable.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/deleteTable.operation.ts index 0335a8c0c3f25..3b725739774c0 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/deleteTable.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/deleteTable.operation.ts @@ -13,7 +13,7 @@ import type { WhereClause, } from '../../helpers/interfaces'; -import { addWhereClauses } from '../../helpers/utils'; +import { addWhereClauses, escapeSqlIdentifier } from '../../helpers/utils'; import { optionsCollection, @@ -98,11 +98,11 @@ export async function execute( let values: QueryValues = []; if (deleteCommand === 'drop') { - query = `DROP TABLE IF EXISTS \`${table}\``; + query = `DROP TABLE IF EXISTS ${escapeSqlIdentifier(table)}`; } if (deleteCommand === 'truncate') { - query = `TRUNCATE TABLE \`${table}\``; + query = `TRUNCATE TABLE ${escapeSqlIdentifier(table)}`; } if (deleteCommand === 'delete') { @@ -114,7 +114,7 @@ export async function execute( [query, values] = addWhereClauses( this.getNode(), i, - `DELETE FROM \`${table}\``, + `DELETE FROM ${escapeSqlIdentifier(table)}`, whereClauses, values, combineConditions, diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/insert.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/insert.operation.ts index 23a79d3ab18b4..f3578f8d99644 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/insert.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/insert.operation.ts @@ -14,7 +14,7 @@ import type { import { AUTO_MAP, BATCH_MODE, DATA_MODE } from '../../helpers/interfaces'; -import { replaceEmptyStringsByNulls } from '../../helpers/utils'; +import { escapeSqlIdentifier, replaceEmptyStringsByNulls } from '../../helpers/utils'; import { optionsCollection } from '../common.descriptions'; import { updateDisplayOptions } from '@utils/utilities'; @@ -171,11 +171,13 @@ export async function execute( ]; } - const escapedColumns = columns.map((column) => `\`${column}\``).join(', '); + const escapedColumns = columns.map(escapeSqlIdentifier).join(', '); const placeholder = `(${columns.map(() => '?').join(',')})`; const replacements = items.map(() => placeholder).join(','); - const query = `INSERT ${priority} ${ignore} INTO \`${table}\` (${escapedColumns}) VALUES ${replacements}`; + const query = `INSERT ${priority} ${ignore} INTO ${escapeSqlIdentifier( + table, + )} (${escapedColumns}) VALUES ${replacements}`; const values = insertItems.reduce( (acc: IDataObject[], item) => acc.concat(Object.values(item) as IDataObject[]), @@ -214,10 +216,12 @@ export async function execute( columns = Object.keys(insertItem); } - const escapedColumns = columns.map((column) => `\`${column}\``).join(', '); + const escapedColumns = columns.map(escapeSqlIdentifier).join(', '); const placeholder = `(${columns.map(() => '?').join(',')})`; - const query = `INSERT ${priority} ${ignore} INTO \`${table}\` (${escapedColumns}) VALUES ${placeholder};`; + const query = `INSERT ${priority} ${ignore} INTO ${escapeSqlIdentifier( + table, + )} (${escapedColumns}) VALUES ${placeholder};`; const values = Object.values(insertItem) as QueryValues; diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/select.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/select.operation.ts index 9666a409ef958..7b16574f7510f 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/select.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/select.operation.ts @@ -13,7 +13,7 @@ import type { WhereClause, } from '../../helpers/interfaces'; -import { addSortRules, addWhereClauses } from '../../helpers/utils'; +import { addSortRules, addWhereClauses, escapeSqlIdentifier } from '../../helpers/utils'; import { optionsCollection, @@ -91,10 +91,10 @@ export async function execute( const SELECT = selectDistinct ? 'SELECT DISTINCT' : 'SELECT'; if (outputColumns.includes('*')) { - query = `${SELECT} * FROM \`${table}\``; + query = `${SELECT} * FROM ${escapeSqlIdentifier(table)}`; } else { - const escapedColumns = outputColumns.map((column) => `\`${column}\``).join(', '); - query = `${SELECT} ${escapedColumns} FROM \`${table}\``; + const escapedColumns = outputColumns.map(escapeSqlIdentifier).join(', '); + query = `${SELECT} ${escapedColumns} FROM ${escapeSqlIdentifier(table)}`; } let values: QueryValues = []; diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/update.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/update.operation.ts index a27466680af2a..22bc332548f4a 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/update.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/update.operation.ts @@ -8,7 +8,7 @@ import type { import type { QueryRunner, QueryValues, QueryWithValues } from '../../helpers/interfaces'; import { AUTO_MAP, DATA_MODE } from '../../helpers/interfaces'; -import { replaceEmptyStringsByNulls } from '../../helpers/utils'; +import { escapeSqlIdentifier, replaceEmptyStringsByNulls } from '../../helpers/utils'; import { optionsCollection } from '../common.descriptions'; import { updateDisplayOptions } from '@utils/utilities'; @@ -182,14 +182,16 @@ export async function execute( const updates: string[] = []; for (const column of updateColumns) { - updates.push(`\`${column}\` = ?`); + updates.push(`${escapeSqlIdentifier(column)} = ?`); values.push(item[column] as string); } - const condition = `\`${columnToMatchOn}\` = ?`; + const condition = `${escapeSqlIdentifier(columnToMatchOn)} = ?`; values.push(valueToMatchOn); - const query = `UPDATE \`${table}\` SET ${updates.join(', ')} WHERE ${condition}`; + const query = `UPDATE ${escapeSqlIdentifier(table)} SET ${updates.join( + ', ', + )} WHERE ${condition}`; queries.push({ query, values }); } diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/upsert.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/upsert.operation.ts index 73ccc0f14d0c8..eccc36c0feca8 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/upsert.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/upsert.operation.ts @@ -8,7 +8,7 @@ import type { import type { QueryRunner, QueryValues, QueryWithValues } from '../../helpers/interfaces'; import { AUTO_MAP, DATA_MODE } from '../../helpers/interfaces'; -import { replaceEmptyStringsByNulls } from '../../helpers/utils'; +import { escapeSqlIdentifier, replaceEmptyStringsByNulls } from '../../helpers/utils'; import { optionsCollection } from '../common.descriptions'; import { updateDisplayOptions } from '@utils/utilities'; @@ -177,10 +177,12 @@ export async function execute( const onConflict = 'ON DUPLICATE KEY UPDATE'; const columns = Object.keys(item); - const escapedColumns = columns.map((column) => `\`${column}\``).join(', '); + const escapedColumns = columns.map(escapeSqlIdentifier).join(', '); const placeholder = `${columns.map(() => '?').join(',')}`; - const insertQuery = `INSERT INTO \`${table}\`(${escapedColumns}) VALUES(${placeholder})`; + const insertQuery = `INSERT INTO ${escapeSqlIdentifier( + table, + )}(${escapedColumns}) VALUES(${placeholder})`; const values = Object.values(item) as QueryValues; @@ -189,7 +191,7 @@ export async function execute( const updates: string[] = []; for (const column of updateColumns) { - updates.push(`\`${column}\` = ?`); + updates.push(`${escapeSqlIdentifier(column)} = ?`); values.push(item[column] as string); } diff --git a/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts b/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts index b7ab739b4af2d..4ac1f9b3d5f3a 100644 --- a/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts +++ b/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts @@ -21,6 +21,22 @@ import type { import { BATCH_MODE } from './interfaces'; +export function escapeSqlIdentifier(identifier: string): string { + const parts = identifier.match(/(`[^`]*`|[^.`]+)/g) ?? []; + + return parts + .map((part) => { + const trimmedPart = part.trim(); + + if (trimmedPart.startsWith('`') && trimmedPart.endsWith('`')) { + return trimmedPart; + } + + return `\`${trimmedPart}\``; + }) + .join('.'); +} + export const prepareQueryAndReplacements = (rawQuery: string, replacements?: QueryValues) => { if (replacements === undefined) { return { query: rawQuery, values: [] }; @@ -35,7 +51,7 @@ export const prepareQueryAndReplacements = (rawQuery: string, replacements?: Que for (const match of matches) { if (match.includes(':name')) { const matchIndex = Number(match.replace('$', '').replace(':name', '')) - 1; - query = query.replace(match, `\`${replacements[matchIndex]}\``); + query = query.replace(match, escapeSqlIdentifier(replacements[matchIndex].toString())); } else { const matchIndex = Number(match.replace('$', '')) - 1; query = query.replace(match, '?'); @@ -379,7 +395,9 @@ export function addWhereClauses( const operator = index === clauses.length - 1 ? '' : ` ${combineWith}`; - whereQuery += ` \`${clause.column}\` ${clause.condition}${valueReplacement}${operator}`; + whereQuery += ` ${escapeSqlIdentifier(clause.column)} ${ + clause.condition + }${valueReplacement}${operator}`; }); return [`${query}${whereQuery}`, replacements.concat(...values)]; @@ -398,7 +416,7 @@ export function addSortRules( rules.forEach((rule, index) => { const endWith = index === rules.length - 1 ? '' : ','; - orderByQuery += ` \`${rule.column}\` ${rule.direction}${endWith}`; + orderByQuery += ` ${escapeSqlIdentifier(rule.column)} ${rule.direction}${endWith}`; }); return [`${query}${orderByQuery}`, replacements.concat(...values)]; diff --git a/packages/nodes-base/nodes/MySql/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/MySql/v2/methods/loadOptions.ts index c89f958a02471..7576d7c85cdc2 100644 --- a/packages/nodes-base/nodes/MySql/v2/methods/loadOptions.ts +++ b/packages/nodes-base/nodes/MySql/v2/methods/loadOptions.ts @@ -1,6 +1,7 @@ import type { IDataObject, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; import { Client } from 'ssh2'; import { createPool } from '../transport'; +import { escapeSqlIdentifier } from '../helpers/utils'; export async function getColumns(this: ILoadOptionsFunctions): Promise { const credentials = await this.getCredentials('mySql'); @@ -22,7 +23,9 @@ export async function getColumns(this: ILoadOptionsFunctions): Promise= 6'} dependencies: aws-sign2: 0.7.0 @@ -4798,7 +4798,7 @@ packages: json-stringify-safe: 5.0.1 mime-types: 2.1.35 performance-now: 2.1.0 - qs: 6.10.5 + qs: 6.10.4 safe-buffer: 5.2.1 tough-cookie: 4.1.3 tunnel-agent: 0.6.0 @@ -13920,21 +13920,20 @@ packages: otplib: 12.0.1 dev: true - /cypress-real-events@1.9.1(cypress@12.17.2): - resolution: {integrity: sha512-eDYW6NagNs8+68ugyPbB6U1aIsYF0E0WHR6upXo0PbTXZNqBNc2s9Y0u/N+pbU9HpFh+krl6iMhoz/ENlYBdCg==} + /cypress-real-events@1.11.0(cypress@13.6.2): + resolution: {integrity: sha512-4LXVRsyq+xBh5TmlEyO1ojtBXtN7xw720Pwb9rEE9rkJuXmeH3VyoR1GGayMGr+Itqf11eEjfDewtDmcx6PWPQ==} peerDependencies: - cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x + cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x dependencies: - cypress: 12.17.2 - prettier: 3.1.0 + cypress: 13.6.2 dev: true - /cypress@12.17.2: - resolution: {integrity: sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg==} - engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} + /cypress@13.6.2: + resolution: {integrity: sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==} + engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true dependencies: - '@cypress/request': 2.88.11 + '@cypress/request': 3.0.1 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) '@types/node': 18.16.16 '@types/sinonjs__fake-timers': 8.1.1 @@ -13969,6 +13968,7 @@ packages: minimist: 1.2.8 ospath: 1.2.2 pretty-bytes: 5.6.0 + process: 0.11.10 proxy-from-env: 1.0.0 request-progress: 3.0.0 semver: 7.5.4 @@ -16045,7 +16045,7 @@ packages: engines: {node: '>=14'} dependencies: cross-spawn: 7.0.3 - signal-exit: 4.0.2 + signal-exit: 4.1.0 dev: true /forever-agent@0.6.1: @@ -22873,10 +22873,9 @@ packages: vue: 3.3.4 dev: false - /qs@6.10.5: - resolution: {integrity: sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ==} + /qs@6.10.4: + resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} engines: {node: '>=0.6'} - deprecated: when using stringify with arrayFormat comma, `[]` is appended on single-item arrays. Upgrade to v6.11.0 or downgrade to v6.10.4 to fix. dependencies: side-channel: 1.0.4 dev: true @@ -24016,11 +24015,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - /signal-exit@4.0.2: - resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} - engines: {node: '>=14'} - dev: true - /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'}